react-native-ali-smartliving
Version:
Component implementation for smartliving WiFi SDK of Ali fy platform
1,298 lines (1,175 loc) • 100 kB
JavaScript
const {
NativeModules,
DeviceEventEmitter,
NativeEventEmitter,
Platform,
} = require('react-native');
const NativeModule = NativeModules.UgenAliLiving;
const tinycolor = require("tinycolor2");
const Location = require('expo-location');
const NetInfo = require("@react-native-community/netinfo").default;
const TcpSocketCreateConnection = require("react-native-tcp-socket").createConnection;
// ref to our close source project device firmware ali-smartliving-device-alios-things/Products/example/lightstring/property_report.h
const property_msg = {
MSG_NONE : (0),
MSG_POWERSWITCH : (1 << 0),
MSG_SCENE_STATUS : (1 << 1),
MSG_LTSTR_INFO : (1 << 2),
MSG_DEVICE_INFO : (1 << 3),
};
function sliceArray (arr, size) {
var arr2 = [];
for(let i = 0; i < arr.length; i = i + size){
arr2.push(arr.slice(i, i + size));
}
return arr2;
}
class AliLiving {
static MESH_ADDRESS_MIN = 0x0001;
static MESH_ADDRESS_MAX = 0x00FF;
static GROUP_ADDRESS_MIN = 0xC001;
static GROUP_ADDRESS_MAX = 0xC0FF;
// static GROUP_ADDRESS_MAX = 0xFEFF;
static GROUP_ADDRESS_MASK = 0x00FF;
static HUE_MIN = 0;
static HUE_MAX = 360;
static SATURATION_MIN = 0;
static SATURATION_MAX = 100;
static BRIGHTNESS_MIN = 42; // 实测灯串不会随着亮度变化而改变颜色的最低亮度,比如 30 的话就代表 30%
static BRIGHTNESS_MAX = 100;
// 如果 LED 中绿色灯珠容易烧坏,此处记录着实际(也就是经过下面的 whiteBalance 之后)发给灯珠不会烧坏的最大值,比如 65 代表 65/255
// 纯绿色情况下的 g 值
// 100% 153
// 95% 135
// 90% 119
// 85% 103
// 80% 89
// 75% 76
// 70% 65
// 白色情况下的 r g b 值
// 100% 255, 153, 61
// 95% 225, 135, 54
// 90% 199, 119, 47
// 85% 173, 103, 41
// 80% 149, 89, 35
// 75% 127, 76, 30
// 70% 109, 65, 26
// static LED_GREEN_MAX = 103;
static LED_GREEN_MAX = 255;
static COLOR_TEMP_MIN = 5;
static COLOR_TEMP_MAX = 100;
static NODE_STATUS_OFF = 0;
static NODE_STATUS_ON = 1;
static onTime = {};
static sceneSyncTime = {};
static NODE_STATUS_INACTIVE = 0; // 未激活
static NODE_STATUS_ONLINE = 1; // 上线
static NODE_STATUS_OFFLINE = 3; // 离线
static NODE_STATUS_FORBIDDEN = 8; // 禁用
static onlineTime = {};
static RELAY_TIMES_MAX = 16;
// 向同一个设备连续发送命令之间的延时,这是为了避免比如连续两个 setAlarm 时第二个
// setAlarm 的 getProperties 在第一个 setAlarm 的 setProperties 之前执行
// 导致的逻辑混乱,虽然说根本的解决办法是将 setAlarm 自身设为 promise ,但为了
// 兼容其它比如 react-native-btsig-telink 组件,以及设备本身执行 setProperties
// 也需要时间来完成,所以还是使用延时了
static DELAY_MS_AFTER_UPDATE_MESH_COMPLETED = 500;
// 向不同设备连续发送命令之间的延时
// ali smartliving 的 SDK 中没有自带命令队列然后自动在命令间加入延时,不过也没
// 有关系,它发送的 WiFi 信号,是不用像蓝牙信号那样需要加入几百毫秒延时的, 1 即可
static DELAY_MS_COMMAND = 1;
static ALARM_CREATE = 0;
static ALARM_REMOVE = 1;
static ALARM_UPDATE = 2;
static ALARM_ENABLE = 3;
static ALARM_DISABLE = 4;
static ALARM_ACTION_TURN_OFF = 0;
static ALARM_ACTION_TURN_ON = 1;
static ALARM_ACTION_SCENE = 2;
static ALARM_TYPE_DAY = 0;
static ALARM_TYPE_WEEK = 1;
static passthroughMode = undefined; // 以前在 react-native-bt-oe 中用于表明通过串口或者说自定义发送数据来控制蓝牙节点,现在只是为了兼容而保留
static gamma = [ // gamma 2.4 ,normal color ,据说较暗时颜色经 gamma 校正后会比较准
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, // 16
2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, // 32
5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, // 48
9, 10, 10, 10, 11, 11, 11, 12, 12, 13, 13, 14, 14, 14, 15, 15, // 64
16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 22, 22, 23, 23, 24, // 80
24, 25, 26, 26, 27, 28, 28, 29, 30, 30, 31, 32, 32, 33, 34, 35, // 96
35, 36, 37, 38, 39, 39, 40, 41, 42, 43, 43, 44, 45, 46, 47, 48, // 112
49, 50, 51, 52, 53, 53, 54, 55, 56, 57, 58, 59, 60, 62, 63, 64, // 128
65, 66, 67, 68, 69, 70, 71, 73, 74, 75, 76, 77, 78, 80, 81, 82, // 144
83, 85, 86, 87, 88, 90, 91, 92, 94, 95, 96, 98, 99, 100, 102, 103, // 160
105, 106, 108, 109, 111, 112, 114, 115, 117, 118, 120, 121, 123, 124, 126, 127, // 176
129, 131, 132, 134, 136, 137, 139, 141, 142, 144, 146, 148, 149, 151, 153, 155, // 192
156, 158, 160, 162, 164, 166, 167, 169, 171, 173, 175, 177, 179, 181, 183, 185, // 208
187, 189, 191, 193, 195, 197, 199, 201, 203, 205, 207, 210, 212, 214, 216, 218, // 224
220, 223, 225, 227, 229, 232, 234, 236, 239, 241, 243, 246, 248, 250, 253, 255 // 240
];
// static gamma = [ // gamma 2.8 ,vivid color ,据说较明亮时颜色经 gamma 校正后会比较准
// 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 16
// 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, // 32
// 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, // 48
// 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, // 64
// 10, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, 17, // 80
// 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25, 25, // 96
// 26, 27, 27, 28, 29, 29, 30, 31, 31, 32, 33, 34, 34, 35, 36, 37, // 112
// 38, 38, 39, 40, 41, 42, 43, 43, 44, 45, 46, 47, 48, 49, 50, 51, // 128
// 52, 53, 54, 55, 56, 57, 58, 59, 60, 62, 63, 64, 65, 66, 67, 68, // 144
// 70, 71, 72, 73, 75, 76, 77, 78, 80, 81, 82, 84, 85, 87, 88, 89, // 160
// 91, 92, 94, 95, 97, 98, 100, 101, 103, 104, 106, 108, 109, 111, 112, 114, // 176
// 116, 117, 119, 121, 123, 124, 126, 128, 130, 131, 133, 135, 137, 139, 141, 143, // 192
// 145, 147, 149, 151, 153, 155, 157, 159, 161, 163, 165, 167, 169, 171, 173, 176, // 208
// 178, 180, 182, 185, 187, 189, 192, 194, 196, 199, 201, 203, 206, 208, 211, 213, // 224
// 216, 218, 221, 223, 226, 228, 231, 234, 236, 239, 242, 244, 247, 250, 253, 255 // 240
// ];
static whiteBalance = { // 为了让灯串能够在低电压下(就可以低温了)保持足够亮度,需要不再按照 1:6:3 的比例来设置 rgb 的白平衡
r: 1,
g: 0.85,
b: 0.4,
};
// static whiteBalance = { // cooler
// r: 1,
// g: 0.6,
// b: 0.24,
// };
// static whiteBalance = { // warmer
// r: 1,
// g: 0.5,
// b: 0.18,
// };
static longCommandParams = true; // 可以在一条命令中发送大量数据,无需拆分成许多小命令
static MESH_CMD_ACCESS_LEN_MAX = 380;
// 逻辑上能否通过蓝牙模块返回的在线状态或者开关灯等状态推理出在线状态
static hasOnlineStatusNotify = true;
// 物理上蓝牙模块是否支持返回在线状态
static hasOnlineStatusNotifyRaw = true;
// WiFi 设备断电后需要 3 分钟才由阿里生活物联网平台向 APP 发送离线事件。
// 在设备端开启 debug 后,每隔 2 秒会从串口打印一下
// [010728]<I> topic:/sys/device/info/notify
// [010730]<D> payload:{"id":"5","version":"1.0","method":"device.info.notify","params":...
// [010768]<D> Cancel message id 6 from list, cur count 2
// [010780]<D> The uri is /sys/device/info/notify
// [010782]<D> The message id 7 len 519 send success, add to the list
// [010784]<D> The message 7 need keep
// [010796]<I> coap send notify success
// 曾问阿里人员, APP 能否收到该消息,还是说只有云端能够收到?因为云端目前
// 只能在设备离线后 3 分钟才通知 APP 设备离线了,而我们需求在几秒内获知设备
// 离线,所以如果 APP 能收到该消息的话,就能达成我们的需求了,否则我们还得
// 想办法实现自己的心跳机制
// 阿里人员的回复是:
// 1. APP 能收到,但是不是作为维持心跳用的,也不会一直发,可以不用关心这个 log 打印;
// 2. 我们跟云端通信基于 mqtt 协议, mqtt 是依靠心跳来维持连接的,所以 3 分钟离线是正常行为,这样会对网络不好的环境有一定的兼容性;
// 3. 根据云端的规则,心跳间隔时间是不支持设置成几秒的;
// 4. 心跳频率过高会导致在网络不稳定的情况下,设备频繁地上下线。
static isOfflineStatusNotifyVeryLate = true;
// Let APP to determine if connected by online devices number > 0
// To compatible with react-native-bt-telink which connected is determined by sdk react-native-bt-telink itself
static isConnectedDeterminedByApp = true;
static needRefreshMeshNodesBeforeConfig = true;
static canConfigEvenDisconnected = true;
static needClaimedBeforeConnect = true;
static isClaiming = false;
static del4GroupStillSendOriginGroupAddress = true;
static isSetNodeGroupAddrReturnAddresses = false;
// 需要如下两个变量来保证韵律期间频发音量转亮度 changeBrightness() 的情况下也能正常 changeScene() ,
// 否则在两串灯时, All 组,开启韵律后,切换效果很多时候只有一盏灯能切换效果,另外一盏没变
static isSceneCadenceBusy = false;
static allowSceneCadence = true;
static shareType = 'aliSmartlivingShare';
static devices = [];
static tempLocalTimer = [];
static dummyMeshAddress = 'z';
static dummyMacAddress = 'f00baa';
static directWifiPort = 8821;
static directWifiIpv4Byte3 = '192.88.21.';
static otaFileVersionOffset = 4; // 把二进制固件作为一个字节数组看待的话,描述着版本号的第一个字节的数组地址
static otaFileVersionLength = 2; // 二进制固件中描述版本号用了几个字节
static lastSceneSyncMeshAddress = undefined;
static emitter = Platform.OS === 'ios' ? (NativeModule ? new NativeEventEmitter(NativeModule) : {}) : DeviceEventEmitter;
static unsubscribeNetInfo = undefined;
static directWiFiSocket = undefined;
static lastNetType = 'none';
static useWebSocket = true; // need firmware add [设备端添加 WebSocket Server 支持](https://github.com/flyskywhy/g/blob/master/i%E4%B8%BB%E8%A7%82%E7%9A%84%E4%BD%93%E9%AA%8C%E6%96%B9%E5%BC%8F/t%E5%BF%AB%E4%B9%90%E7%9A%84%E4%BD%93%E9%AA%8C/%E7%94%B5%E4%BF%A1/Net/%E7%89%A9%E8%81%94%E7%BD%91/SmartLiving/AliSmartLiving%E4%BD%BF%E7%94%A8%E8%AF%A6%E8%A7%A3.md#%E8%AE%BE%E5%A4%87%E7%AB%AF%E6%B7%BB%E5%8A%A0-websocket-server-%E6%94%AF%E6%8C%81)
static isNativeModuleInited = false;
static async connectThenDoInit() {
try {
let result = await fetch('https://eu-central-1.api-iot.aliyuncs.com');
// indirect WiFi mode need this to controll device of e.g. `ble_awss start` (蓝牙配网)
NativeModule.doInit();
this.isNativeModuleInited = true;
} catch (err) {
// 如果当初用户是用德国(默认)服务器注册的帐号,则当处于登录状态,此时杀死 APP ,将手机连接到不能
// 上网的路由器或是直连设备的 WiFi,再打开 APP ,则 ali smartliving 的 Android SDK 会在
// NativeModule.doInit() 中让 APP 卡死 60 秒,所以这里使用 await fetch 尝试测试网络以免卡死
this.connectThenDoInit();
}
}
static async doInit() {
this.connectThenDoInit();
// to [get ssid on ios](https://github.com/react-native-netinfo/react-native-netinfo/issues/263#issuecomment-808727622)
// in this.getCurrentSsid() later
await Location.requestPermissionsAsync();
// direct WiFi mode need this to controll device of `soft_ap start` (WiFi 直连)
// need firmware add code from your module vendor
this.unsubscribeNetInfo = NetInfo.addEventListener(state => {
if (
state.isWifiEnabled &&
state.isConnected
&& this.lastNetType !== state.type
&& state.type === 'wifi'
&& state.details.ipAddress
&& state.details.ipAddress.match(this.directWifiIpv4Byte3) // to match unique not usual 192.168, otherwise socket 30s connect timeout will interfere indirect WiFi device
) {
if (this.directWiFiSocket === undefined) {
let host = this.directWifiIpv4Byte3 + '1';
let socketOptions = {
port: this.directWifiPort,
host,
}
if (this.useWebSocket) {
this.directWiFiSocket = new WebSocket(`ws://${socketOptions.host}:${socketOptions.port}`,
'local-ali-smartliving');
this.emitter.emit('serviceConnected', {});
this.directWiFiSocket.onopen = () => this.onDirectWiFiSocketOpen();
this.directWiFiSocket.onmessage = (message) => {
try {
const json = JSON.parse(message.data);
this.onDirectWiFiSocketReceive(json);
} catch (err) {
console.warn('directWiFiSocket received->', err);
}
};
this.directWiFiSocket.onerror = (error) => {
console.warn('directWiFiSocket error->' + JSON.stringify(error));
};
this.directWiFiSocket.onclose = () => {
// APP 刚开启时 this.emitter 还没有来得及初始化好,所以需要 &&
this.emitter && this.emitter.emit('notificationVendorResponse', {
meshAddress: this.dummyMeshAddress,
opcode: 'BoneThingLocalConnectionChange',
params: JSON.stringify({
localConnectionState: this.NODE_STATUS_OFFLINE,
}),
});
this.directWiFiSocket = undefined;
console.warn('directWiFiSocket connection closed');
};
} else {
this.directWiFiSocket = TcpSocketCreateConnection(socketOptions, () => this.onDirectWiFiSocketOpen());
this.directWiFiSocket.setEncoding('utf8');
this.directWiFiSocket.on('data', (data) => {
try {
const json = JSON.parse(data);
this.onDirectWiFiSocketReceive(json);
} catch (err) {
console.warn('directWiFiSocket received->', err);
}
});
this.directWiFiSocket.on('error', (error) => {
console.warn('directWiFiSocket error->' + error);
});
this.directWiFiSocket.on('close', () => {
// APP 刚开启时 this.emitter 还没有来得及初始化好,所以需要 &&
this.emitter && this.emitter.emit('notificationVendorResponse', {
meshAddress: this.dummyMeshAddress,
opcode: 'BoneThingLocalConnectionChange',
params: JSON.stringify({
localConnectionState: this.NODE_STATUS_OFFLINE,
}),
});
this.directWiFiSocket.destroy();
this.directWiFiSocket = undefined;
console.warn('directWiFiSocket connection closed');
});
}
}
}
this.lastNetType = state.type;
});
}
static doDestroy() {
// NativeModule.doDestroy();
this.unsubscribeNetInfo();
if (this.directWiFiSocket) {
if (this.useWebSocket) {
} else {
this.directWiFiSocket.destroy();
}
}
}
static addListener(eventName, handler) {
if (Platform.OS === 'ios') {
const nativeEventEmitter = new NativeEventEmitter(NativeModule);
nativeEventEmitter.addListener(eventName, handler);
} else {
DeviceEventEmitter.addListener(eventName, handler);
}
}
static removeListener(eventName, handler) {
if (Platform.OS === 'ios') {
const nativeEventEmitter = new NativeEventEmitter(NativeModule);
nativeEventEmitter.removeListener(eventName, handler);
} else {
DeviceEventEmitter.removeListener(eventName, handler);
}
}
static enableBluetooth() {
NativeModule.enableBluetooth();
}
static enableSystemLocation() {
// NativeModule.enableSystemLocation();
}
static notModeAutoConnectMesh() {
return new Promise((resolve, reject) => {
reject();
});
}
static autoConnect({
userMeshPwd,
}) {}
static async postConnected({
meshAddress,
type,
immediate = false,
}) {
// 需要此处,否则在 1 个设备的情况下,走过
// 设置效果、删除设备、认领设备、设置效果
// 这 4 个步骤时,除非重启 APP ,否则因为
// 无法符合 selectNodeToResponseSceneId()
// 中 sceneSyncMeshAddress !== this.lastSceneSyncMeshAddress
// 的条件而导致 APP 一直没有发出设置同步的命令
this.lastSceneSyncMeshAddress = undefined;
this.remind({
meshAddress,
})
}
static autoRefreshNotify({
repeatCount,
Interval
}) {}
static idleMode({
disconnect
}) {
// return NativeModule.idleMode(disconnect);
}
// 登录
static async login() {
if (this.isNativeModuleInited) {
return NativeModule.login();
} else {
return new Promise((resolve, reject) => {
reject(new TypeError('NativeModule is not inited'));
});
}
}
// 退出登录
static async logout() {
return NativeModule.logout();
}
// 获取登录状态
static async isLogin() {
if (this.isNativeModuleInited) {
return NativeModule.isLogin();
} else {
return new Promise((resolve, reject) => {
reject(new TypeError('NativeModule is not inited'));
});
}
}
static scanTimer = undefined;
static startScan({
meshName, // to compatible with react-native-bt-telink which use meshName
timeoutSeconds,
isSingleNode,
}) {
if (this.directWiFiSocket) {
} else {
this.isClaiming = false;
if (this.scanTimer) {
clearTimeout(this.scanTimer);
this.scanTimer = undefined;
NativeModule.stopScanLocalDevice();
}
// in my use case, I use `unclaimed = meshName === 'sysin_mesh'`, maybe you need change it
let unclaimed = meshName === 'sysin_mesh';
NativeModule.startScanLocalDevice(unclaimed);
this.scanTimer = setTimeout(() => {
this.scanTimer = undefined;
NativeModule.stopScanLocalDevice();
}, timeoutSeconds * 1000);
}
}
// 获取设备token
static async getDeviceToken(pk, dn, timeout) {
return NativeModule.getDeviceToken(pk, dn, timeout);
}
// 开启飞燕长连接
static async startAliSocketListener() {
return NativeModule.startAliSocketListener();
}
// 关闭飞燕长连接
static async stopAliSocketListener() {
return NativeModule.stopAliSocketListener();
}
// 获取长连接状态
static async getAliSocketListenerState() {
return NativeModule.getAliSocketListenerState();
}
// 获取账号信息
static async getCurrentAccountMessage() {
if (this.isNativeModuleInited) {
return NativeModule.getCurrentAccountMessage();
} else {
return new Promise((resolve, reject) => {
reject(new TypeError('NativeModule is not inited'));
});
}
}
// 对 topic 进行订阅
static subscribe(topic) {
topic && NativeModule.subscribe(topic);
}
// 对 topic 取消订阅
static unsubscribe(topic) {
topic && NativeModule.unsubscribe(topic);
}
// 在 topic 上发布消息
static async ayncSendPublishRequest(topic, jsonParams) {
if (topic) {
// if (topic.includes('#')) {
// reject('send publish error: The topic name MUST NOT contain any wildcard characters (#+)');
// } else {
return NativeModule.ayncSendPublishRequest(topic, json);
// }
} else {
reject('ayncSendPublishRequest The topic name can not be empty');
}
}
// 发送客户端 API
static async send(path, params, version, iotAuth) {
return NativeModule.send(path, params, version, iotAuth);
}
static async setNodeName({
meshAddress,
name,
}) {
try {
let res = await NativeModule.send(
'/uc/setDeviceNickName',
JSON.stringify({
iotId: meshAddress,
nickName: name,
}),
'1.0.6',
true,
);
let resJson = JSON.parse(res);
return resJson.data;
} catch (error) {
console.warn(error);
}
}
static async loadUserDeviceList() {
try {
let res = await NativeModule.send(
'/uc/listBindingByAccount',
JSON.stringify({pageNo: 1, pageSize: 100}), // TODO: more than 100?
'1.0.8',
true,
);
let resJson = JSON.parse(res);
return resJson.data.data;
} catch (error) {
console.warn(error);
}
}
// Load devices info from `https://living.aliyun.com` and convert them
// into our nodes format, then init them in native code and return nodes.
// Some use case: Old user install new APP and want get old devices;
// New user shared by others and want get shared devices.
// This function can be invoked before connected any device node.
static async loadNodes({
oldNodes = [],
forceRefresh = false,
}) {
let devices = await this.loadUserDeviceList();
let nodes = oldNodes.filter((node) => {
return node.a === this.dummyMeshAddress;
});
if (devices) {
for (let device of devices) {
let oldNode = oldNodes.find((node) => node.a === device.iotId);
if (oldNode === undefined || oldNode.n === device.deviceName || forceRefresh) {
try {
let detailRes = await NativeModule.send(
'/thing/info/get',
JSON.stringify({iotId: device.iotId}),
'1.0.4',
true,
);
let detailJson = JSON.parse(detailRes);
let deviceDetail = detailJson.data;
nodes.push({
n: device.nickName || deviceDetail.name,
m: deviceDetail.mac,
a: deviceDetail.iotId,
i: {
v: deviceDetail.firmwareVersion,
},
t: {
d: 0, // discoveryType 0 means LOCAL_ONLINE_DEVICE
p: deviceDetail.productKey,
},
})
} catch (error) {
console.warn(error);
if (oldNode) {
nodes.push(oldNode);
}
}
} else {
nodes.push(oldNode);
}
}
} else {
nodes = oldNodes;
}
if (nodes.length) {
this.devices = nodes.map((node) => {
return {
meshAddress: node.a,
macAddress: node.m,
};
})
NativeModule.setDevices(this.devices);
}
if (devices && nodes.length) {
return nodes;
}
}
// This function should be invoked after connected any device node.
// The properties will be emitted later.
static getNodesProperties({
meshAddresses = [],
}) {
let addresses = [];
if (this.directWiFiSocket) {
addresses = meshAddresses.filter((meshAddress) => {
return meshAddress === this.dummyMeshAddress;
});
} else {
addresses = meshAddresses.filter((meshAddress) => {
return meshAddress !== this.dummyMeshAddress;
});
}
addresses.map((meshAddress) => {
let params = {
iotId: meshAddress,
items: {
// here MSG_POWERSWITCH is for whitelist described in react-native-ali-smartliving/android/src/main/java/ugen/fy/plugin/AliLiving.java
property_report: property_msg.MSG_POWERSWITCH | property_msg.MSG_LTSTR_INFO | property_msg.MSG_DEVICE_INFO,
},
};
if (this.directWiFiSocket) {
if (this.useWebSocket) {
this.directWiFiSocket.send(JSON.stringify(params.items));
} else {
this.directWiFiSocket.write(JSON.stringify(params.items));
}
} else {
// because NativeModule.getProperties() is for get all properties
// and sometimes actually not return "all" properties, so I have
// to set custom "property_report" property to device firmware to
// let it return some other individual property
//
// because NativeModule.getProperties() will get offline device's
// properties from cloud, that will confuse this.isOnline(), so
// getProperties() in insertDevice() in AliLiving.java and AliLiving.m
// is disabled, just use setProperties "property_report" here instead
NativeModule.setProperties(meshAddress, JSON.stringify(params));
}
});
}
static onDirectWiFiSocketReceive(data) {
// console.warn('directWiFiSocket received->', data);
if (data.hasOwnProperty('sceneSyncId')) {
// {
// "PowerSwitch": 1,
// "sceneSyncId": 27
// }
this.emitter.emit('notificationVendorResponse', {
opcode: 'SCENE_SYNC',
meshAddress: this.dummyMeshAddress,
isOnline: true,
isOn: data.hasOwnProperty('PowerSwitch') ? (data.PowerSwitch !== 0) : true,
sceneID: data.sceneSyncId,
sceneSyncTime: new Date().getTime(),
});
}
if (data.ltstr_info || data.device_info) {
// {
// "CommonServiceResponse": {
// "seq": "\ufffd"
// },
// "PowerSwitch": 1,
// "device_info": {
// "addr_bits_length": 8,
// "color_mode": 3,
// "ltstr_scene": 0,
// "rev": 0,
// "vendor_id": 28
// },
// "ltstr_info": {
// "crash_point": 40,
// "flag_gain_ratio": 100,
// "gamma_enable": 0,
// "ltstr_length": 200,
// "timing_sequence": 1
// }
// }
let params = {
data: {
PowerSwitch: {
time: 0,
value: 1,
},
},
};
if (data.ltstr_info) {
params.data.ltstr_info = {
time: 0,
value: data.ltstr_info,
}
}
if (data.device_info) {
params.data.device_info = {
time: 0,
value: data.device_info,
}
}
this.emitter.emit('notificationVendorResponse', {
meshAddress: this.dummyMeshAddress,
opcode: 'getProperties',
params: JSON.stringify(params),
});
}
}
static onDirectWiFiSocketOpen() {
// To use LocalTimer in direct WiFi mode, need modify firmware code to add
// timer_service_ntp_update(str);
// in ntp_timer_update(const char *str) of e.g.
// ali-smartliving-device-alios-things/Products/example/smart_outlet/property_report.c
// and make sure ntp_timer_update() will be invoked when `this.setTime()` here,
// and modify timer_service_init() of
// ali-smartliving-device-alios-things/Living_SDK/framework/protocol/linkkit/sdk/iotx-sdk-c_clone/components/timer_service/timer_service.c
// to move below code
// if (ret != 0) {
// // EXAMPLE_TRACE("ERR:ntp_time_request failed=%d!", ret);
// // return -1;
// ret = 2;
// goto err;
// }
// down to just before
// return 0;
this.setTime({
meshAddress: this.dummyMeshAddress,
});
// 如果将直连 WiFi 设备也按照扫描、认领的方式进行,则无法简单做到
// 让多人简单通过 WiFi 直连共享同一个设备的目的,而且用户在连接 WiFi
// 时已经输入了一次密码,相当于已经认证了一次,再需要认领操作的话
// 就显得罗嗦了,所以还是像这里这样连接上 WiFi 就自动认领并上线。
this.emitter.emit('deviceStatusUpdateMeshCompleted', {
meshAddress: this.dummyMeshAddress,
macAddress: this.dummyMacAddress,
name: "(((<----->)))",
type: {p: 'directWifi'},
});
this.emitter.emit('notificationVendorResponse', {
meshAddress: this.dummyMeshAddress,
opcode: 'BoneThingLocalConnectionChange',
params: JSON.stringify({
localConnectionState: this.NODE_STATUS_ONLINE,
}),
});
}
static parseVendorResponse(resRaw) {
let res = resRaw;
switch (resRaw.opcode) {
case '/app/down/thing/properties': {
let params = JSON.parse(resRaw.params);
let sceneSyncId;
if (params.items) {
if (params.items.sceneSyncId) {
sceneSyncId = params.items.sceneSyncId;
} else if (params.checkFailedData && params.checkFailedData.sceneSyncId) {
sceneSyncId = params.checkFailedData.sceneSyncId;
}
}
if (sceneSyncId) {
let oldTime = this.sceneSyncTime[resRaw.meshAddress];
let newTime = sceneSyncId.time;
this.sceneSyncTime[resRaw.meshAddress] = newTime;
// [Mon Sep 27 2021 16:44:58.801] WARN {"meshAddress": "vvssa4DEzVglIGxDl777000000", "opcode": "/app/down/thing/properties", "params": "{\"iotId\":\"vvssa4DEzVglIGxDl777000000\",\"productKey\":\"a17hN4W4777\",\"deviceName\":\"testDeviceName1\",\"items\":{\"PowerSwitch\":{\"time\":1632732300192,\"value\":1},\"sceneSyncId\":{\"time\":1632732300192,\"value\":11}}}"}
// [Mon Sep 27 2021 16:44:58.930] WARN {"meshAddress": "vvssa4DEzVglIGxDl777000000", "opcode": "/app/down/thing/properties", "params": "{\"iotId\":\"vvssa4DEzVglIGxDl777000000\",\"productKey\":\"a17hN4W4777\",\"deviceName\":\"testDeviceName1\",\"items\":{\"PowerSwitch\":{\"time\":1632732300207,\"value\":1},\"sceneSyncId\":{\"time\":1632732300207,\"value\":11}}}"}
// [Mon Sep 27 2021 16:44:58.993] WARN {"meshAddress": "vvssa4DEzVglIGxDl777000000", "opcode": "/app/down/thing/properties", "params": "{\"iotId\":\"vvssa4DEzVglIGxDl777000000\",\"productKey\":\"a17hN4W4777\",\"deviceName\":\"testDeviceName1\",\"items\":{\"PowerSwitch\":{\"time\":1632732300218,\"value\":1},\"sceneSyncId\":{\"time\":1632732300218,\"value\":11}}}"
// LAN will cause above and below, WAN only will cause below
// [Mon Sep 27 2021 16:44:59.650] WARN {"meshAddress": "vvssa4DEzVglIGxDl777000000", "opcode": "/app/down/thing/properties", "params": "{\"checkFailedData\":{\"sceneSyncId\":{\"code\":5092,\"time\":1632732298504,\"message\":\"property not found\",\"value\":11}},\"groupIdList\":[\"a103ToHAxVJe1777\"],\"groupId\":\"a103ToHAxVJe1777\",\"categoryKey\":\"light\",\"batchId\":\"e80a059fd1d9426d924187f290e0a777\",\"gmtCreate\":1632732298510,\"productKey\":\"a17hN4W4777\",\"deviceName\":\"testDeviceName1\",\"iotId\":\"vvssa4DEzVglIGxDl777000000\",\"checkLevel\":0,\"namespace\":\"TmallGenie\",\"tenantId\":\"19A31790C888469BAFE4C384FFD60777\",\"thingType\":\"DEVICE\",\"items\":{\"PowerSwitch\":{\"time\":1632732300218,\"value\":1}},\"tenantInstanceId\":\"iotx-oxssharez200\"}"}
let debounceTime = 2000; // bigger than sceneSyncId.time gap between LAN and WAN
if (oldTime === undefined || newTime - oldTime > debounceTime) {
res = {
opcode: 'SCENE_SYNC',
meshAddress: resRaw.meshAddress,
isOnline: true,
isOn: params.items.PowerSwitch ? (params.items.PowerSwitch.value !== 0) : true,
sceneID: sceneSyncId.value,
sceneSyncTime: newTime,
};
}
}
}
break;
default:
break;
}
return res;
}
static sendCommand({
opcode,
meshAddress,
valueArray,
immediate = false,
}) {
// NativeModule.sendCommand(opcode, meshAddress, valueArray, immediate);
}
static remind({
meshAddress,
immediate = false,
}) {
let params = {
iotId: meshAddress,
items: {
flicker: 1,
},
};
if (this.directWiFiSocket) {
if (this.useWebSocket) {
this.directWiFiSocket.send(JSON.stringify(params.items));
} else {
this.directWiFiSocket.write(JSON.stringify(params.items));
}
} else {
NativeModule.setProperties(meshAddress, JSON.stringify(params));
}
}
// get the real address of the data.params
//
// because when there are 2 device, bind or unbind device A,
// the success event will be sent by device B ...
// see what happened in '/app/down/_thing/event/notify'
static targetAddress(data) {
let opcode = data.opcode;
let params = JSON.parse(data.params);
if (opcode === '/app/down/thing/status') {
return params.iotId;
} else if (opcode === '/app/down/_thing/event/notify') {
// data: {
// "meshAddress": "z0TeBIF5sGUKJB0K2tvu000000",
// "opcode": "/app/down/_thing/event/notify",
// "params": "{\"identifier\":\"awss.BindNotify\",\"value\":{\"iotId\":\"yCO6Qjp6medLPB9QVDXT000000\",\"identityId\":\"5022op8fb006f88b482b3f54d8541e0c1dfd0254\",\"owned\":1,\"productKey\":\"a17hN4W4777\",\"deviceName\":\"testDeviceName2\",\"operation\":\"Bind\"}}"
// }
// data: {
// "meshAddress": "z0TeBIF5sGUKJB0K2tvu000000",
// "opcode": "/app/down/_thing/event/notify",
// "params": "{\"identifier\":\"awss.BindNotify\",\"value\":{\"iotId\":\"yCO6Qjp6medLPB9QVDXT000000\",\"identityId\":\"5022op8fb006f88b482b3f54d8541e0c1dfd0254\",\"owned\":1,\"productKey\":\"a17hN4W4777\",\"deviceName\":\"testDeviceName2\",\"operation\":\"Unbind\"}}"
// }
return params.value.iotId;
} else if (opcode === '/app/down/thing/properties') {
return params.iotId;
} else {
return data.meshAddress;
}
}
static getBulbsNumber(data) {
let opcode = data.opcode;
let params = JSON.parse(data.params);
let realData = opcode === 'getProperties' ? params.data : params.items;
if (realData) {
if (realData.ltstr_info) {
return realData.ltstr_info.value.ltstr_length;
}
}
}
static getBulbAddrBitsLength(data) {
let opcode = data.opcode;
let params = JSON.parse(data.params);
let realData = opcode === 'getProperties' ? params.data : params.items;
if (realData) {
if (realData.device_info) {
return realData.device_info.value.addr_bits_length;
}
}
}
static getBulbColorBitsLength(data) {
let opcode = data.opcode;
let params = JSON.parse(data.params);
let realData = opcode === 'getProperties' ? params.data : params.items;
if (realData) {
if (realData.device_info) {
switch (realData.device_info.value.color_mode) {
case 3: // our project use 3 as 24 bits (RGB) color
return 24;
case 2: // our project use 2 as 32 bits (RGBW) color
return 32;
default:
return 24;
}
}
}
}
static isOnline(data) {
let opcode = data.opcode;
let params = JSON.parse(data.params);
if (opcode === '/app/down/thing/status') {
let time = this.onlineTime[data.meshAddress];
if (time === undefined || params.status.time > time) { // sometimes the seq of '/app/down/thing/status' is not ordered by status.time, so need this
this.onlineTime[data.meshAddress] = params.status.time;
return params.status.value === this.NODE_STATUS_ONLINE;
}
}
if (opcode === 'getStatus') {
let time = this.onlineTime[data.meshAddress];
if (time === undefined || params.data.time > time) {
this.onlineTime[data.meshAddress] = params.data.time;
return params.data.status === this.NODE_STATUS_ONLINE;
}
}
if (opcode === 'BoneThingLocalConnectionChange') {
// 好像只在设备与 APP 之前已经通过在线路由器互相连接然后让路由器离线的情况下才有用,
// 否则只能使用下面的 time === 0
return params.localConnectionState === this.NODE_STATUS_ONLINE;
}
if (opcode === 'getProperties') {
if (params.data.PowerSwitch) {
// 手机与设备连着同一路由器,且该路由器离线时,启动 APP 会得到
// 当前值比如 PowerSwitch.value 的开始时间 time === 0
// if (params.data.PowerSwitch.time === 0) {
return true;
// }
}
}
if (opcode === '/app/down/_thing/event/notify') {
if (params.identifier === 'awss.BindNotify') {
if (params.value.operation === 'Bind') {
return true;
}
if (params.value.operation === 'Unbind') {
return false;
}
}
}
}
static isOn(data) {
let opcode = data.opcode;
let params = JSON.parse(data.params);
if (opcode === '/app/down/thing/properties') {
if (params.items && params.items.PowerSwitch) {
let time = this.onTime[data.meshAddress];
if (time === undefined || params.items.PowerSwitch.time > time) {
this.onTime[data.meshAddress] = params.items.PowerSwitch.time;
return params.items.PowerSwitch.value === this.NODE_STATUS_ON;
}
}
}
if (opcode === 'getProperties') {
if (params.data.PowerSwitch) {
let time = this.onTime[data.meshAddress];
if (time === undefined || params.data.PowerSwitch.time > time) {
this.onTime[data.meshAddress] = params.data.PowerSwitch.time;
return params.data.PowerSwitch.value === this.NODE_STATUS_ON;
} else if (params.data.PowerSwitch.time === 0) {
// 刚认领设备的那一瞬间获取属性时得到的 time === 0
return params.data.PowerSwitch.value === this.NODE_STATUS_ON;
}
}
}
}
// recommend native version e.g. isResCheckFailedData() in
// android/src/main/java/ugen/fy/plugin/AliLiving.java
static isResCheckFailedData(data) {
let opcode = data.opcode;
let params = JSON.parse(data.params);
if (opcode === '/app/down/thing/properties') {
if (params.checkFailedData !== undefined &&
Object.keys(params.checkFailedData).length) {
if (params.items === undefined) {
return true;
} else if (Object.keys(params.items).length === 0) {
return true;
}
}
}
}
static changePower({
meshAddress,
value,
type,
delaySec = 0,
immediate = false,
}) {
let changed = false;
if (this.passthroughMode) {
for (let mode in this.passthroughMode) {
if (this.passthroughMode[mode].includes(type.p)) {
let params = {
iotId: meshAddress,
items: {},
};
if (mode === 'silan') {
params.items.PowerSwitch = value;
}
if (this.directWiFiSocket) {
if (this.useWebSocket) {
this.directWiFiSocket.send(JSON.stringify(params.items));
} else {
this.directWiFiSocket.write(JSON.stringify(params.items));
}
this.emitter.emit('notificationVendorResponse', {
meshAddress: this.dummyMeshAddress,
opcode: 'getProperties',
params: JSON.stringify({
data: {
PowerSwitch: {
time: 0,
value,
},
},
}),
});
} else {
NativeModule.setProperties(meshAddress, JSON.stringify(params));
}
changed = true;
break;
}
}
}
if (!changed) {
// NativeModule.changePower(meshAddress, value);
}
}
// recommend native version e.g. isResChangeBrightness() in
// android/src/main/java/ugen/fy/plugin/AliLiving.java
static isResChangeBrightness(data) {
let opcode = data.opcode;
let params = JSON.parse(data.params);
if (opcode === '/app/down/thing/properties') {
if (params.items &&
params.items.luminance !== undefined) {
return true;
}
}
}
static changeBrightness({
meshAddress,
hue = 0,
saturation = 0,
value,
type,
immediate = false,
}) {
let params = {
iotId: meshAddress,
items: {
luminance: value,
},
};
if (this.directWiFiSocket) {
if (this.useWebSocket) {
this.directWiFiSocket.send(JSON.stringify(params.items));
} else {
this.directWiFiSocket.write(JSON.stringify(params.items));
}
} else {
if (meshAddress) {
NativeModule.setProperties(meshAddress, JSON.stringify(params));
} else { // defaultAllGroupAddress is undefined
this.devices.map(async (node) => {
if (node.macAddress !== this.dummyMacAddress) {
params.iotId = node.meshAddress;
NativeModule.setProperties(node.meshAddress, JSON.stringify(params));
await this.sleepMs(100); // 需要这个以避免多个设备时,有时有些设备无法更改亮度的问题
}
});
}
}
}
static changeColorTemp({
meshAddress,
value
}) {
// NativeModule.changeColorTemp(meshAddress, value);
}
static changeColor({
meshAddress,
hue = 0,
saturation = 0,
value,
type,
immediate = false,
}) {
const h = parseInt(65535 * hue / this.HUE_MAX, 10);
const s = parseInt(65535 * saturation / this.SATURATION_MAX, 10);
const v = parseInt(65535 * value / this.BRIGHTNESS_MAX, 10);
// NativeModule.changeColor(meshAddress, h, s, v);
}
static padHexString(string) {
if (string.length === 1) {
return '0' + string;
} else {
return string;
}
}
static hexString2ByteArray(string) {
let array = [];
[].map.call(string, (value, index, str) => {
if (index % 2 === 0) {
array.push(parseInt(value + str[index + 1], 16));
}
});
return array;
}
static byteArray2HexString(bytes) {
return bytes.map(byte => this.padHexString((byte & 0xFF).toString(16))).toString().replace(/,/g, '').toUpperCase();
}
static sleepMs(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 为了解决颜色不准的问题,最终发现原因是灯串硬件亮度级别是 8 位与手机估计至少 12 位亮度级别
// 存在矛盾,也就是说是灯串硬件等级(成本)较低造成的,光靠调灯串硬件固件是不行的。现在的解决方
// 法是查看 RGB 中的某个分量,如果低于某个值(视灯串 LED 的 gamma 表而定)的就变为 0 之类的方法。
// 具体来说,低于等于 0x30 的就变为 0 ,位于 0x30 和 0x40 之间的就变为 0x40,其它不变。
//
// 后来发现颜色不准主要是由于固件中效果变化时 gamma 重复操作导致的,所以下面代码暂时简单禁用 ledFilter3040
static ledFilter3040(value) {
if (value <= 0x30) {
return 0;
}
if (value < 0x40) {
return 0x40;
}
return value;
}
static ledFilterBurnGreen({
r,
g,
b,
scene,
}) {
if (g > this.LED_GREEN_MAX) {
return {
r: parseInt(r * this.LED_GREEN_MAX / g, 10),
g: this.LED_GREEN_MAX,
b: parseInt(b * this.LED_GREEN_MAX / g, 10),
}
} else if (r === 0 && g === 0 && scene !== 45) { // 因为当前使用的灯串的蓝色偏暗,而又受到白平衡和 LED_GREEN_MAX 的限制,所以这里单独将纯蓝亮度提高到客户满意的 2 倍
let newB = b * 2;
if (newB > 122) {
newB = 122;
}
return {
r,
g,
b: newB,
}
} else {
return {
r,
g,
b,
}
}
}
static async changeScene({
meshAddress,
sceneSyncMeshAddress,
scene,
hue = 0,
saturation = 0,
value,
reserve = 0,
color,
reserveBg = 0,
colorBg,
reserves = [],
colors = [],
colorsLength = 1,
colorSequence = 1,
colorIds = [1, 2, 3, 4, 5],
colorBgId = 2,
colorId = 1,
data,
isEditingCustom = false,
speed = -2,
type,
immediate = false,
}) {
// if (this.isSceneCadenceBusy) {
// this.allowSceneCadence = false;
// await this.sleepMs(1000);
// this.isSceneCadenceBusy = false;
// }
let changed = false;
if (this.passthroughMode) {
let color3 = color && tinycolor(color).toRgb();
if (!color3) {
color3 = tinycolor.fromRatio({
h: hue / this.HUE_MAX,
s: saturation / this.SATURATION_MAX,
v: value / this.BRIGHTNESS_MAX,
}).toRgb();
} else {
// color3.r = this.ledFilter3040(color3.r);
// color3.g = this.ledFilter3040(color3.g);
// color3.b = this.ledFilter3040(color3.b);
color3.r = parseInt(this.gamma[color3.r] * this.whiteBalance.r, 10);
color3.g = parseInt(this.gamma[color3.g] * this.whiteBalance.g, 10);
color3.b = parseInt(this.gamma[color3.b] * this.whiteBalance.b, 10);
let safeColor = this.ledFilterBurnGreen(color3);
color3.r = safeColor.r;
color3.g = safeColor.g;
color3.b = safeColor.b;
}
let color3Bg = colorBg && tinycolor(colorBg).toRgb();
if (color3Bg) {
// color3Bg.r = this.ledFilter3040(color3Bg.r);
// color3Bg.g = this.ledFilter3040(color3Bg.g);
// color3Bg.b = this.ledFilter3040(color3Bg.b);
color3Bg.r = parseInt(this.gamma[color3Bg.r] * this.whiteBalance.r, 10);
color3Bg.g = parseInt(this.gamma[color3Bg.g] * this.whiteBalance.g, 10);
color3Bg.b = parseInt(this.gamma[color3Bg.b] * this.whiteBalance.b, 10);
let safeColor = this.ledFilterBurnGreen(color3Bg);
color3Bg.r = safeColor.r;
color3Bg.g = safeColor.g;
color3Bg.b = safeColor.b;
}
let colors3 = [];
colors.map((colour, index) => {
colors3.push(reserves[index] || 0); // reserve 是固件代码中某个颜色的保留字节(固件代码中每个颜色有 4 个字节)对应固件代码中的 ltstr_scene_status_t
let rgb = tinycolor(colour).toRgb();
// rgb.r = this.ledFilter3040(rgb.r);
// rgb.g = this.ledFilter3040(rgb.g);
// rgb.b = this.ledFilter3040(rgb.b);
rgb.r = parseInt(this.gamma[rgb.r] * this.whiteBalance.r, 10);
rgb.g = parseInt(this.gamma[rgb.g] * this.whiteBalance.g, 10);
rgb.b = parseInt(this.gamma[rgb.b] * this.whiteBalance.b, 10);
let safeColor = this.ledFilterBurnGreen({
...rgb,
scene,
});
rgb.r = safeColor.r;
rgb.g = safeColor.g;
rgb.b = safeColor.b;
colo