react-native-ali-smartliving
Version:
Component implementation for smartliving WiFi SDK of Ali fy platform
741 lines (619 loc) • 24.5 kB
JavaScript
const {
DeviceEventEmitter,
} = require('react-native');
const NetInfo = require("@react-native-community/netinfo").default;
var pako = require('pako');
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_MASK = 0x00FF;
static HUE_MIN = 0;
static HUE_MAX = 360;
static SATURATION_MIN = 0;
static SATURATION_MAX = 100;
static BRIGHTNESS_MIN = 42;
static BRIGHTNESS_MAX = 100;
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;
static DELAY_MS_AFTER_UPDATE_MESH_COMPLETED = 500;
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;
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;
static isOfflineStatusNotifyVeryLate = true;
static isConnectedDeterminedByApp = true;
static needRefreshMeshNodesBeforeConfig = true;
static canConfigEvenDisconnected = true;
static needClaimedBeforeConnect = true;
static isClaiming = false;
static del4GroupStillSendOriginGroupAddress = true;
static isSetNodeGroupAddrReturnAddresses = false;
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 = DeviceEventEmitter;
static unsubscribeNetInfo = undefined;
static directWiFiSocket = undefined;
static lastNetType = 'none';
static doInit() {
this.emitter.emit('serviceConnected')
// direct WiFi mode need this to controll device of `soft_ap start` (WiFi 直连)
this.unsubscribeNetInfo = NetInfo.addEventListener(state => {
if (this.lastNetType !== state.type) {
if (this.directWiFiSocket === undefined || this.directWiFiSocket.readyState === WebSocket.CLOSED) {
let host = this.directWifiIpv4Byte3 + '1';
let socketOptions = {
port: this.directWifiPort,
host,
}
this.connectWebSocket(socketOptions);
} else if (!(state.isConnected && this.directWiFiSocket.readyState === WebSocket.OPEN)) { // debug on macbook, when change WiFi to `soft_ap`, found isConnected will be false first, then true after some minuter, so I connectWebSocket just when isConnected is false
this.needConnectAfterClosed = true;
this.directWiFiSocket.close();
}
}
this.lastNetType = state.type;
});
}
static connectWebSocket(socketOptions) {
this.directWiFiSocket = new WebSocket(`ws://${socketOptions.host}:${socketOptions.port}`,
'local-ali-smartliving');
this.directWiFiSocket.onopen = () => {
this.directWiFiSocket.send('directWiFiSocket connected');
// 如果将直连 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,
}),
});
};
this.directWiFiSocket.onmessage = (message) => {
try {
const json = JSON.parse(message.data);
// console.warn('directWiFiSocket received->', 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,
}),
});
if (this.needConnectAfterClosed) {
this.connectWebSocket(socketOptions);
} else {
this.directWiFiSocket.close();
this.directWiFiSocket = undefined;
}
this.needConnectAfterClosed = false;
console.warn('directWiFiSocket connection closed');
};
}
static doDestroy() {
this.unsubscribeNetInfo();
if (this.directWiFiSocket) {
this.directWiFiSocket.close();
}
}
static addListener(eventName, handler) {
DeviceEventEmitter.addListener(eventName, handler);
}
static removeListener(eventName, handler) {
DeviceEventEmitter.removeListener(eventName, handler);
}
static enableBluetooth() {}
static enableSystemLocation() {}
static notModeAutoConnectMesh() {
return new Promise((resolve, reject) => {
reject();
});
}
static autoConnect({
userMeshPwd,
}) {}
static async postConnected({}) {}
static autoRefreshNotify({
repeatCount,
Interval
}) {}
static idleMode({
disconnect
}) {}
static async login() {}
static async logout() {}
static async isLogin() {}
static startScan({
timeoutSeconds,
isSingleNode,
}) {}
static async getDeviceToken(pk, dn, timeout) {}
static async startAliSocketListener() {}
static async stopAliSocketListener() {}
static async getAliSocketListenerState() {}
static async getCurrentAccountMessage() {}
static subscribe(topic) {}
static unsubscribe(topic) {}
static async ayncSendPublishRequest(topic, jsonParams) {}
static async send(path, params, version, iotAuth) {}
static async setNodeName({}) {}
static async loadUserDeviceList() {}
static async loadNodes({}) {}
static getNodesProperties({}) {}
static onDirectWiFiSocketReceive(data) {}
static onDirectWiFiSocketOpen() {}
static parseVendorResponse(resRaw) {
return resRaw;
}
static sendCommand({
opcode,
meshAddress,
valueArray
}) {}
static remind({
meshAddress,
}) {
let params = {
iotId: meshAddress,
items: {
flicker: 1,
},
};
if (this.directWiFiSocket) {
this.directWiFiSocket.send(JSON.stringify(params.items));
}
}
static targetAddress(data) {}
static getBulbsNumber(data) {}
static getBulbAddrBitsLength(data) {}
static getBulbColorBitsLength(data) {}
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') {
if (params.code === 200) {
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.code === 200) {
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' && 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.code === 200) {
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;
}
}
}
}
}
static isResCheckFailedData(data) {}
static changePower({
meshAddress,
value,
type,
}) {
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) {
this.directWiFiSocket.send(JSON.stringify(params.items));
this.emitter.emit('notificationVendorResponse', {
meshAddress: this.dummyMeshAddress,
opcode: 'getProperties',
params: JSON.stringify({
code: 200,
data: {
PowerSwitch: {
time: 0,
value,
},
},
}),
});
}
changed = true;
break;
}
}
}
}
static isResChangeBrightness(data) {}
static changeBrightness({
meshAddress,
value
}) {
let params = {
iotId: meshAddress,
items: {
luminance: value,
},
};
if (this.directWiFiSocket) {
this.directWiFiSocket.send(JSON.stringify(params.items));
}
}
static changeColorTemp({
meshAddress,
value
}) {}
static changeColor({
meshAddress,
hue = 0,
saturation = 0,
value,
type,
}) {}
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));
}
static ledFilter3040(value) {
if (value <= 0x30) {
return 0;
}
if (value < 0x40) {
return 0x40;
}
return value;
}
static ledFilterBurnGreen({}) {}
static changeScene({
meshAddress,
scene,
hue = 0,
saturation = 0,
value,
colorIds = [1, 2, 3, 4, 5],
data = [],
speed = 3,
type,
}) {
if (scene === 0x80) {
let rawData = [];
data.map(subdata => {
let bulbsMode = subdata[0];
if (bulbsMode === 0 || bulbsMode === 1 || bulbsMode === 2) {
let subdataLength = 7;
let bulbsStart = subdata[1];
let bulbsLength = subdata[2];
let bulbsColorR = this.ledFilter3040(subdata[3] >> 16 & 0xFF);
let bulbsColorG = this.ledFilter3040(subdata[3] >> 8 & 0xFF);
let bulbsColorB = this.ledFilter3040(subdata[3] & 0xFF);
rawData = rawData.concat([
subdataLength,
bulbsMode,
bulbsStart,
bulbsLength,
bulbsColorR,
bulbsColorG,
bulbsColorB,
])
}
});
let dataType = 0;
let dataLengthLowByte = rawData.length & 0xFF;
let dataLengthHightByte = rawData.length >> 8 & 0xFF;
// TODO: dataType = 1;
let compressedData = pako.deflateRaw(new Uint8Array(rawData), {
strategy: pako.Z_HUFFMAN_ONLY,
}); // 可被 https://github.com/jibsen/tinf/tree/master/test/test_tinf.c 里的 inflate_huffman_only 解压缩
// console.warn(rawData.length, compressedData.length)
console.warn(rawData, compressedData)
console.warn([0, 0, scene, speed, dataType, dataLengthLowByte, dataLengthHightByte, ...rawData])
}
}
static saveCustomScene({
meshAddress,
immediate = false,
}) {
let params = {
iotId: meshAddress,
items: {
custom_scene: {
save_flag: 1,
}
},
};
if (this.directWiFiSocket) {
this.directWiFiSocket.send(JSON.stringify(params.items));
}
}
static getTypeFromUuid = productKey => productKey;
static async provisionDeviceInfo(productKey) {}
static async getCurrentSsid() {}
static configNode({
node,
// cfg,
isToClaim,
}) {}
static stopAddDevice() {}
static toggleProvision(wifiName,wifiPassword,timeout) {}
static getTotalOfGroupIndex({
meshAddress,
}) {}
static setNodeGroupAddr({
meshAddress,
groupIndex,
groupAddress,
}) {}
static setTime({
meshAddress,
year,
month,
day,
hour,
minute,
second = 0,
}) {}
static getTime({
meshAddress,
relayTimes,
}) {}
static setAlarm({
meshAddress,
crud,
alarmId,
status,
action,
type,
month = 1,
dayOrweek,
hour,
minute,
second = 0,
sceneId = 0,
}) {}
static getAlarm({
meshAddress,
relayTimes,
alarmId,
}) {}
static cascadeLightStringGroup({
meshAddress,
}) {}
static flashWriteAttr({
meshAddress,
timeSequence = 1,
nodeBulbs = 96,
collideCenter = 40,
flagPercent = 100,
gammaEnable = 0,
immediate = false,
}) {
let params = {
iotId: meshAddress,
items: {
ltstr_info: {
ltstr_length: nodeBulbs,
timing_sequence: timeSequence,
crash_point: collideCenter,
flag_gain_ratio: flagPercent,
gamma_enable: gammaEnable,
}
},
};
if (this.directWiFiSocket) {
this.directWiFiSocket.send(JSON.stringify(params.items));
}
}
static getNodeInfoWithNewType({
nodeInfo = {},
}) {
return nodeInfo;
}
static getFwVerInNodeInfo({
nodeInfo = {},
}) {
return nodeInfo.v;
}
static getNodeInfoWithNewFwVer({
nodeInfo = '',
newFwVer = '',
}) {
return {
...nodeInfo,
v: newFwVer,
};
}
static getFirmwareVersion({
meshAddress,
relayTimes = 7,
immediate = false,
}) {}
static async getOtaUpgradeableList() {}
static isTestFw({
fwVer,
}) {
return false;
}
static getOtaState({}) {}
static setOtaMode({}) {}
static stopMeshOta({}) {}
static async getOtaProgressList() {}
static startOta({
meshAddresses,
}) {}
static pauseMeshOta() {}
static continueMeshOta() {}
static isValidFirmware(firmware) {
return false;
}
}
module.exports = AliLiving;