node-red-contrib-ewelink-cube
Version:
Node-RED integration with eWeLink Cube
374 lines (340 loc) • 18.6 kB
JavaScript
const { eventBridge } = require('./utils/event');
const { EVENT_SSE_ON_UPDATE_DEVICE_STATE, EVENT_SSE_SECURITY_CHANGE, EVENT_SSE_ALARM_CHANGE } = require('./utils/const');
const { DeviceStateCache } = require('./utils/state');
const { API_URL_GET_DEVICE_LIST } = require('./utils/const');
const axios = require('axios');
const _ = require('lodash');
const { isFileExist, initCacheDir, getCacheFilepath, readCacheFile, removeCacheFile, writeCacheFile } = require('./utils/fs');
/**
* Determine if payload is updated.
* @param {object} cp cache payload
* @param {object} np new payload
*/
function isDeviceStateUpdated(cp, np) {
let updated = false;
Object.keys(np).forEach((key) => {
const nv = np[key];
const cv = cp[key];
if (!cv || JSON.stringify(cv) !== JSON.stringify(nv)) {
updated = true;
cp[key] = nv;
}
});
return updated;
}
/**
* Determine if device data is duplicated.
* @param {object} stateCache device state cache
* @param {object} deviceData device data
*/
function isDuplicateData(stateCache, deviceData) {
const serialNumber = deviceData.endpoint.serial_number;
const payload = deviceData.payload;
const i = stateCache.findIndex((item) => item.serialNumber === serialNumber);
let duplicated = false;
if (i === -1) {
stateCache.push({
serialNumber,
payload,
});
} else if (!isDeviceStateUpdated(stateCache[i].payload, payload)) {
duplicated = true;
}
return duplicated;
}
module.exports = function (RED) {
function GetStateNode(config) {
RED.nodes.createNode(this, config);
const node = this;
const deviceStateCache = new DeviceStateCache([]);
let trvStateNodeContext = new Map();
// All of devices state cache.
// SAMPLE:
// [
// {
// "serialNumber": "...",
// "payload": {
// "key1": {},
// "key2": {}
// }
// }
// ]
node.deviceStateCache = [];
const stateCacheFile = `${node.id}_devstat`;
const stateCachePath = getCacheFilepath(stateCacheFile);
if (initCacheDir() && isFileExist(stateCachePath)) {
try {
const content = readCacheFile(stateCachePath);
node.deviceStateCache = JSON.parse(content);
} catch (err) {
node.error('set device state cache error:', err);
}
}
async function eventSseOnAllStateDeviceHandler(jsonData) {
try {
// node.log(`NEW SSE COMING :${JSON.stringify(jsonData)}`);
const data = JSON.parse(jsonData);
/* TODO: release the following code after next version
if (isDuplicateData(node.deviceStateCache, JSON.parse(data.msg.data))) {
return;
}
*/
if (config.server === data.srcNodeId) {
const deviceData = JSON.parse(data.msg.data);
const playload = JSON.parse(JSON.stringify(deviceData.payload));
const stateList = (config.state + ',' + config.list).split(',');
if (config.device === 'all' || !config.device) {
node.send({ payload: data.msg.data });
return;
}
if (config.device === deviceData.endpoint.serial_number && (config.state || config.list)) {
const deviceId = deviceData.endpoint.serial_number;
/** if device category is trv ,stoping here */
if (config.category && config.category === 'thermostat') {
// node.log('playload ===> '+JSON.stringify(playload,null,2));
// node.log('stateList ===> '+JSON.stringify(stateList,null,2));
// node.log('trvDevcieId ===> '+trvDevcieId);
sendMsgRuleOfTrv(playload, data, stateList, deviceId);
return;
}
//旧版本选择了toggle能力的数据 (The old version selected data with toggle capability)
const equalToggle = stateList.some((item) => item === 'toggle');
if (equalToggle && Object.keys(playload).join('') === 'toggle') {
const tempData = JSON.parse(JSON.stringify(deviceStateCache.getData()));
//When initialized and entered, it is empty and directly outputs and returns.
if (tempData.length === 0) {
node.send({ payload: data.msg.data });
deviceStateCache.add({ id: deviceData.endpoint.serial_number, playload: playload });
return;
}
if (deviceStateCache.has(deviceId)) {
const deviceStateData = deviceStateCache.getNodeData(deviceId);
if (JSON.stringify(Object.values(deviceStateData.playload)) == JSON.stringify(Object.values(playload))) {
deviceStateCache.clean();
deviceStateCache.add({ id: deviceId, playload: playload });
} else {
node.send({ payload: data.msg.data });
deviceStateCache.modify({ id: deviceId, playload: playload });
}
return;
} else {
node.send({ payload: data.msg.data });
deviceStateCache.add({ id: deviceId, playload: playload });
return;
}
}
/** 普通设备和区分通道的设备 */
const printFlag = spacialDeviceHandler(stateList, playload);
const name = Object.keys(playload).join('');
const specialChanel = stateList.some((item) => item.indexOf('toggle') !== -1 && item.length > 6);
if (name === 'toggle' && specialChanel && printFlag) {
const tempData = JSON.parse(JSON.stringify(deviceStateCache.getData()));
//When initialized and entered, it is empty and directly outputs and returns.
if (tempData.length === 0) {
node.send({ payload: data.msg.data });
deviceStateCache.add({ id: deviceData.endpoint.serial_number, playload: playload });
return;
}
/** Contains toggle capability and the device ID exists in the memory
* and the jsonized data will not be printed if there is no change,
* but will be printed if it changes.
*/
if (deviceStateCache.has(deviceId)) {
const deviceStateData = deviceStateCache.getNodeData(deviceId);
if (JSON.stringify(Object.values(deviceStateData.playload)) == JSON.stringify(Object.values(playload))) {
deviceStateCache.clean();
deviceStateCache.add({ id: deviceId, playload: playload });
} else {
node.send({ payload: data.msg.data });
deviceStateCache.modify({ id: deviceId, playload: playload });
}
return;
} else {
node.send({ payload: data.msg.data });
deviceStateCache.add({ id: deviceId, playload: playload });
return;
}
}
if (printFlag || config.state === 'all') {
node.send({ payload: data.msg.data });
}
}
}
} catch (error) {
node.log('error:' + JSON.stringify(error));
}
}
//Multi-channel devices are channel specific
function spacialDeviceHandler(stateList, playload) {
const KeyName = Object.keys(playload).join('');
if (KeyName !== 'toggle') {
// node.log(`stateList.includes(KeyName) : ${stateList.includes(KeyName)}`);
return stateList.includes(KeyName);
}
let flag = false;
for (const item of stateList) {
// Channel equipment distinguishes specific channels
if (KeyName === 'toggle' && item.indexOf(KeyName) !== -1) {
let tempName = item.substring('toggle'.length);
if (tempName == Object.keys(playload.toggle)) {
flag = true;
}
}
}
return flag;
}
// Send message rule of special trv device
function sendMsgRuleOfTrv(playload, data, stateList, deviceId) {
try {
if (['thermostat', 'toggle'].includes(Object.keys(playload).join(''))) {
/** thermostat(mode 、 status) toggle(child_lock 、 open_windows_detect) , Need to filter duplicate values */
// "payload":{
// "toggle":{
// "child-lock":{
// "toggleState":"off"
// }
// "open-window-detect":{
// "toggleState":"off"
// }
// }
// }
// "payload":{
// "thermostat":{
// "thermostat-mode":{
// "thermostatMode":"MANUAL"
// },
// "adaptive-recovery-status":{
// "adaptiveRecoveryStatus":"INACTIVE",//"HEATING"(加热中)、"INACTIVE"(未活动)
// }
// }
// }
const playloadValueObject = Object.values(playload);
const playloadleve2CapabilityName = Object.keys(playloadValueObject[0])[0];
const playloadLeve2capailityValue = Object.values(playloadValueObject[0])[0];
const deviceStateMap = trvStateNodeContext.get(deviceId);
node.log('before deviceStateMap ===> ' + JSON.stringify(deviceStateMap));
if (deviceStateMap == undefined) {
if (stateList.includes(playloadleve2CapabilityName) || stateList.includes('all')) {
node.send({ payload: data.msg.data });
}
trvStateNodeContext.set(deviceId, playload);
node.log('trvStateNodeContext ====> ' + JSON.stringify(trvStateNodeContext.get(deviceId)));
} else {
// including ability
if (stateList.includes(playloadleve2CapabilityName) || stateList.includes('all')) {
if (_.has(playload, 'toggle')) {
const capabilityValue = JSON.stringify(_.get(deviceStateMap, ['toggle', playloadleve2CapabilityName], ''));
// 当前toggle下的两个能力是否变化(Whether the two abilities under the current toggle have changed)
if (JSON.stringify(playloadLeve2capailityValue) !== capabilityValue) {
node.send({ payload: data.msg.data });
}
if (!deviceStateMap.toggle) {
deviceStateMap.toggle = {};
}
_.merge(deviceStateMap.toggle, playload.toggle);
}
if (_.has(playload, 'thermostat')) {
const capabilityValue = JSON.stringify(_.get(deviceStateMap, ['thermostat', playloadleve2CapabilityName], ''));
// 当前thermostat下的两个能力是否变化(Whether the two abilities under the current thermostat have changed)
if (JSON.stringify(playloadLeve2capailityValue) !== capabilityValue) {
node.send({ payload: data.msg.data });
}
if (!deviceStateMap.thermostat) {
deviceStateMap.thermostat = {};
}
_.merge(deviceStateMap.thermostat, playload.thermostat);
}
node.log('after deviceStateMap ===> ' + JSON.stringify(deviceStateMap, null, 2));
trvStateNodeContext.set(deviceId, deviceStateMap);
node.log('after trvStateNodeContext ===> ' + JSON.stringify(trvStateNodeContext.get(deviceId)));
}
}
return;
}
/** 三个目标温度,特殊处理 (Three target temperatures, special handling)*/
if (Object.keys(playload)[0] === 'thermostat-target-setpoint') {
const capaValue = Object.values(playload);
if (stateList.includes(Object.keys(capaValue[0])[0]) || stateList.includes('all')) {
node.send({ payload: data.msg.data });
return;
}
}
// General abaility of trv;
if (stateList.includes(Object.keys(playload).join('')) || config.state === 'all') {
node.send({ payload: data.msg.data });
}
return;
} catch (error) {
console.log(error);
}
}
// 设备模块 (device module)
eventBridge.on(EVENT_SSE_ON_UPDATE_DEVICE_STATE, eventSseOnAllStateDeviceHandler);
node.on('close', (done) => {
eventBridge.off(EVENT_SSE_ON_UPDATE_DEVICE_STATE, eventSseOnAllStateDeviceHandler);
done();
});
// 安防模块 (Security module)
function eventSseSecurityChangeHandler(jsonData) {
try {
node.log('jsondata' + JSON.stringify(jsonData));
const data = JSON.parse(jsonData);
if (config.server === data.srcNodeId) {
if (config.device === 'all' || (config.device === 'ihost' && config.state === 'all')) {
node.send({ payload: data.msg.data });
return;
}
const deviceData = JSON.parse(data.msg.data);
const playload = JSON.parse(JSON.stringify(deviceData.payload));
const stateList = (config.state + ',' + config.list).split(',');
const KeyName = Object.keys(playload)[0];
if (stateList.indexOf(KeyName) !== -1 || (config.state === 'all' && config.device === 'ihost')) {
node.send({ payload: data.msg.data });
}
}
} catch (error) {
node.log('ihost security sse event error' + JSON.stringify(error));
}
}
eventBridge.on(EVENT_SSE_SECURITY_CHANGE, eventSseSecurityChangeHandler);
node.on('close', (done) => {
eventBridge.off(EVENT_SSE_SECURITY_CHANGE, eventSseSecurityChangeHandler);
done();
});
// 报警模块 (alarm module)
function eventSseAlarmChangeHandler(jsonData) {
try {
node.log('jsondata' + JSON.stringify(jsonData));
const data = JSON.parse(jsonData);
if (config.server === data.srcNodeId) {
if (config.device === 'all' || (config.device === 'ihost' && config.state === 'all')) {
node.send({ payload: data.msg.data });
return;
}
const deviceData = JSON.parse(data.msg.data);
const playload = JSON.parse(JSON.stringify(deviceData.payload));
const stateList = (config.state + ',' + config.list).split(',');
const KeyName = Object.keys(playload)[0];
if (stateList.indexOf(KeyName) !== -1 || (config.state === 'all' && config.device === 'ihost')) {
node.send({ payload: data.msg.data });
}
}
} catch (error) {
node.log('ihost alarm sse event error' + JSON.stringify(error));
}
}
eventBridge.on(EVENT_SSE_ALARM_CHANGE, eventSseAlarmChangeHandler);
node.on('close', (done) => {
eventBridge.off(EVENT_SSE_ALARM_CHANGE, eventSseAlarmChangeHandler);
done();
});
// Remove cache file.
removeCacheFile(stateCachePath);
// Save context to cache file.
node.on('close', (done) => {
writeCacheFile(stateCachePath, JSON.stringify(node.deviceStateCache));
done();
});
}
RED.nodes.registerType('event-state', GetStateNode);
};