UNPKG

node-red-contrib-tuya-smart-device

Version:
632 lines (607 loc) 20.8 kB
const TuyaDevice = require('tuyapi'); const packageInfo = require('../package.json'); const utils = require('./utils'); const CLIENT_STATUS = { DISCONNECTED: 'DISCONNECTED', CONNECTED: 'CONNECTED', CONNECTING: 'CONNECTING', ERROR: 'ERROR', }; const EVENT_MODES = { BOTH: 'event-both', DATA: 'event-data', DP_REFRESH: 'event-dp-refresh', }; module.exports = function (RED) { function TuyaSmartDeviceNode(config) { RED.nodes.createNode(this, config); config.disableAutoStart = config.disableAutoStart || false; let node = this; let isConnected = false; let shouldTryReconnect = true; let shouldSubscribeData = true; let shouldSubscribeRefreshData = true; this.name = config.deviceName; this.deviceName = config.deviceName; this.storeAsCreds = config.storeAsCreds || false; if (this.storeAsCreds) { // If the deviceId and key are stored in the creds take it from there const secretConfig = this.credentials.secretConfig || '{}'; const secret = JSON.parse(secretConfig); console.log( 'loading the deviceId and deviceKey from secret credentials ...' ); this.deviceId = secret.deviceId; this.deviceKey = secret.deviceKey; } else { console.log('loading the deviceId and deviceKey from config ...'); this.deviceId = config.deviceId; this.deviceKey = config.deviceKey; } this.deviceIp = config.deviceIp; this.disableAutoStart = config.disableAutoStart; this.eventMode = config.eventMode || EVENT_MODES.BOTH; this.logLevel = config.logLevel || 'log-level-debug'; const logLevelNum = utils.UI_LOG_LEVELS[this.logLevel] || 3; // default incase of any error is debug node.log(`Setting custom log level as ${this.logLevel}`); node.logger = utils.createLogger(this, logLevelNum); // To be used for local debugging. // need to find a log method control in the node. /*node.logger.log( `Recieved the config ${JSON.stringify({ ...config, credentials: this.credentials, moduleVersion: packageInfo.version, })}` );*/ this.retryTimeout = config.retryTimeout == null || typeof config.retryTimeout == 'undefined' || (typeof config.retryTimeout == 'string' && config.retryTimeout.trim() == '') || (typeof config.retryTimeout == 'number' && config.retryTimeout <= 0) || isNaN(config.retryTimeout) ? 1000 : config.retryTimeout; this.findTimeout = config.findTimeout == null || typeof config.findTimeout == 'undefined' || (typeof config.findTimeout == 'string' && config.findTimeout.trim() == '') || (typeof config.findTimeout == 'number' && config.findTimeout <= 0) || isNaN(config.findTimeout) ? 1000 : config.findTimeout; this.tuyaVersion = config.tuyaVersion == null || typeof config.tuyaVersion == 'undefined' || (typeof config.tuyaVersion == 'string' && config.tuyaVersion.trim() == '') || (typeof config.tuyaVersion == 'number' && config.tuyaVersion <= 0) || isNaN(config.tuyaVersion) ? '3.1' : config.tuyaVersion.trim(); this.deviceStatus = null; // Variable definition ends here if (this.eventMode == EVENT_MODES.DATA) { shouldSubscribeData = true; shouldSubscribeRefreshData = false; } else if (this.eventMode == EVENT_MODES.DP_REFRESH) { shouldSubscribeData = false; shouldSubscribeRefreshData = true; } else { // both case or default case shouldSubscribeData = true; shouldSubscribeRefreshData = true; } node.logger.debug( `Event subscription : shouldSubscribeData=>${shouldSubscribeData} , shouldSubscribeRefreshData=>${shouldSubscribeRefreshData}` ); if (this.retryTimeout <= 0) { this.retryTimeout = 1000; } if (this.findTimeout <= 0) { this.findTimeout = 1000; } let findTimeoutHandler = null; let retryTimerHandler = null; node.on('input', function (msg) { try { node.logger.debug(`Recieved input : ${JSON.stringify(msg)}`); let operation = msg.payload.operation || 'SET'; delete msg.payload.operation; if (['GET', 'SET', 'REFRESH'].indexOf(operation) != -1) { // the device has to be connected. if (!tuyaDevice.isConnected()) { // error device not connected let errText = `Device not connected. Can't send the ${operation} commmand`; node.logger.log(errText); setStatusOnError(errText, 'Device not connected !', { context: { message: errText, deviceVirtualId: node.deviceId, deviceIp: node.deviceIp, deviceName: node.deviceName, deviceKey: node.deviceKey, }, }); return; } } switch (operation) { case 'SET': tuyaDevice.set(msg.payload).catch((error) => { setStatusOnError( error.message, 'Uncaught error : ' + error.message, { context: { message: error.message, deviceVirtualId: node.deviceId, deviceIp: node.deviceIp, deviceKey: node.deviceKey, }, } ); }); break; case 'REFRESH': tuyaDevice.refresh(msg.payload).catch((error) => { setStatusOnError( error.message, 'Uncaught error : ' + error.message, { context: { message: error.message, deviceVirtualId: node.deviceId, deviceIp: node.deviceIp, deviceKey: node.deviceKey, }, } ); }); break; case 'GET': tuyaDevice.get(msg.payload).catch((error) => { setStatusOnError( error.message, 'Uncaught error : ' + error.message, { context: { message: error.message, deviceVirtualId: node.deviceId, deviceIp: node.deviceIp, deviceKey: node.deviceKey, }, } ); }); break; case 'CONTROL': if (msg.payload.action == 'CONNECT') { if (!tuyaDevice.isConnected()) { // Connect only when disconnected startComm(); } } else if (msg.payload.action == 'DISCONNECT') { //Disconnect only when connected. // Make disconnect force closeComm(); } else if (msg.payload.action == 'SET_FIND_TIMEOUT') { if (!isNaN(msg.payload.value) && msg.payload.value > 0) { setFindTimeout(msg.payload.value); } else { node.logger.error( 'Invalid find timeout ! - ' + msg.payload.value ); } } else if (msg.payload.action == 'SET_RETRY_TIMEOUT') { if (!isNaN(msg.payload.value) && msg.payload.value > 0) { setRetryTimeout(msg.payload.value); } else { node.logger.error( 'Invalid retry timeout ! - ' + msg.payload.value ); } } else if (msg.payload.action == 'RECONNECT') { if (tuyaDevice.isConnected) { closeComm(); } startComm(); } else if (msg.payload.action == 'SET_EVENT_MODE') { shouldSubscribeData = true; shouldSubscribeRefreshData = true; // if any incorrect value set the event mode as BOTH node.eventMode = EVENT_MODES.BOTH; if (msg.payload.value === EVENT_MODES.DATA) { shouldSubscribeRefreshData = false; node.eventMode = EVENT_MODES.DATA; } else if (msg.payload.value === EVENT_MODES.DP_REFRESH) { shouldSubscribeData = false; node.eventMode = EVENT_MODES.DP_REFRESH; } node.logger.debug( `SET_EVENT_MODE : shouldSubscribeData=>${shouldSubscribeData} , shouldSubscribeRefreshData=>${shouldSubscribeRefreshData}` ); } break; } } catch (error) { setStatusOnError(error.message, 'Uncaught error : ' + error.message, { context: { message: error.message, deviceVirtualId: node.deviceId, deviceIp: node.deviceIp, deviceKey: node.deviceKey, }, }); } }); const enableNode = () => { console.log('enableNode(): enabling the node', node.id); startComm(); }; const disableNode = () => { console.log('disableNode(): disabling the node', node.id); closeComm(); }; const setFindTimeout = (newTimeout) => { node.logger.debug( 'setFindTimeout(): Setting new find timeout :' + newTimeout ); node.findTimeout = newTimeout; }; const setRetryTimeout = (newTimeout) => { node.logger.debug( 'setRetryTimeout(): Setting new retry timeout :' + newTimeout ); node.retryTimeout = newTimeout; }; const closeComm = () => { try { node.logger.log('closeComm(): Cleaning up the state'); node.logger.debug('closeComm(): Clearing the find timeout handler'); clearTimeout(findTimeoutHandler); shouldTryReconnect = false; node.logger.debug('closeComm(): Disconnecting from Tuya Device'); tuyaDevice.disconnect(); setStatusDisconnected(); } catch (error) { setStatusOnError(error.message, 'Uncaught error : ' + error.message, { context: { message: error.message, deviceVirtualId: node.deviceId, deviceIp: node.deviceIp, deviceKey: node.deviceKey, }, }); } }; const startComm = () => { // This 1 sec timeout will make sure that the diconnect happens .. // otherwise connect will not hanppen as the state is not changed findTimeoutHandler = setTimeout(() => { shouldTryReconnect = true; node.logger.log(`startComm(): Connecting to Tuya`); node.logger.debug( `startComm(): Connecting to Tuya with params ${JSON.stringify( utils.maskSensitiveData(connectionParams) )} , findTimeout : ${node.findTimeout} , retryTimeout: ${ node.retryTimeout } ` ); findDevice(); }, 1000); }; const sendDeviceConnectStatus = (data) => { return { payload: { state: node.deviceStatus, ...data, }, }; }; const setStatusConnecting = function () { if (node.deviceStatus != CLIENT_STATUS.CONNECTING) { node.deviceStatus = CLIENT_STATUS.CONNECTING; node.send([null, sendDeviceConnectStatus()]); } return node.status({ fill: 'yellow', shape: 'ring', text: 'connecting' }); }; const setStatusConnected = function () { if (node.deviceStatus != CLIENT_STATUS.CONNECTED) { node.deviceStatus = CLIENT_STATUS.CONNECTED; node.send([null, sendDeviceConnectStatus()]); } return node.status({ fill: 'green', shape: 'ring', text: 'connected' }); }; const setStatusDisconnected = function () { if (node.deviceStatus != CLIENT_STATUS.DISCONNECTED) { node.deviceStatus = CLIENT_STATUS.DISCONNECTED; node.send([null, sendDeviceConnectStatus()]); } return node.status({ fill: 'red', shape: 'ring', text: 'disconnected' }); }; const setStatusOnError = function ( errorText, errorShortText = 'error', data ) { node.logger.error(errorText, data); if (node.deviceStatus != CLIENT_STATUS.ERROR) { node.deviceStatus = CLIENT_STATUS.ERROR; node.send([null, sendDeviceConnectStatus()]); } return node.status({ fill: 'red', shape: 'ring', text: errorShortText }); }; const connectionParams = { id: node.deviceId, key: node.deviceKey, ip: node.deviceIp, issueGetOnConnect: false, nullPayloadOnJSONError: false, version: node.tuyaVersion, issueRefreshOnConnect: false, }; let tuyaDevice = new TuyaDevice(connectionParams); let retryConnection = () => { clearTimeout(retryTimerHandler); retryTimerHandler = setTimeout(() => { node.logger.log('Retrying connection...'); connectDevice(); }, node.retryTimeout); node.logger.debug( `Will try to reconnect after ${node.retryTimeout} milliseconds` ); }; node.on('close', function () { // tidy up any state // clearInterval(int); closeComm(); }); // Add event listeners tuyaDevice.on('connected', () => { try { node.logger.log( 'Connected to device! name : ' + node.deviceName + ', ip : ' + node.deviceIp ); setStatusConnected(); } catch (error) { setStatusOnError(error.message, 'Uncaught error : ' + error.message, { context: { message: error.message, deviceVirtualId: node.deviceId, deviceIp: node.deviceIp, deviceKey: node.deviceKey, }, }); } }); tuyaDevice.on('disconnected', () => { try { node.logger.log( 'Disconnected from tuyaDevice. shouldTryReconnect = ' + shouldTryReconnect ); setStatusDisconnected(); if (shouldTryReconnect) { retryConnection(); } } catch (error) { setStatusOnError(error.message, 'Uncaught error : ' + error.message, { context: { message: error.message, deviceVirtualId: node.deviceId, deviceIp: node.deviceIp, deviceKey: node.deviceKey, }, }); } }); tuyaDevice.on('error', (error) => { try { node.logger.error( 'Error from tuyaDevice. shouldTryReconnect = ' + shouldTryReconnect + ', error = ' + JSON.stringify(error) ); // Anonymize setStatusOnError(error, 'Error : ' + JSON.stringify(error), { context: { message: error, deviceVirtualId: node.deviceId, deviceIp: node.deviceIp, deviceKey: node.deviceKey, }, }); if ( typeof error === 'string' && error.startsWith('Timeout waiting for status response') ) { node.logger.error( 'This error can be due to invalid DPS values. Please check the dps values in the payload !!!!' ); } if (shouldTryReconnect) { retryConnection(); } } catch (error) { setStatusOnError(error.message, 'Uncaught error : ' + error.message, { context: { message: error.message, deviceVirtualId: node.deviceId, deviceIp: node.deviceIp, deviceKey: node.deviceKey, }, }); } }); tuyaDevice.on('dp-refresh', (data) => { try { if (shouldSubscribeRefreshData) { node.logger.debug( `Data from device [event:dp-refresh]: ${JSON.stringify(data)}` ); setStatusConnected(); node.send([ { payload: { data: data, deviceId: node.deviceId, deviceName: node.deviceName, }, }, null, ]); } } catch (error) { setStatusOnError(error.message, 'Uncaught error : ' + error.message, { context: { message: error.message, deviceVirtualId: node.deviceId, deviceIp: node.deviceIp, deviceKey: node.deviceKey, }, }); } }); tuyaDevice.on('data', (data) => { try { if (shouldSubscribeData) { node.logger.debug( `Data from device [event:data]: ${JSON.stringify(data)}` ); setStatusConnected(); node.send([ { payload: { data: data, deviceId: node.deviceId, deviceName: node.deviceName, }, }, null, ]); } } catch (error) { setStatusOnError(error.message, 'Uncaught error : ' + error.message, { context: { message: error.message, deviceVirtualId: node.deviceId, deviceIp: node.deviceIp, deviceKey: node.deviceKey, }, }); } }); let connectDevice = () => { try { clearTimeout(findTimeoutHandler); if (tuyaDevice.isConnected() === false) { setStatusConnecting(); const connectHandle = tuyaDevice.connect(); connectHandle.catch((e) => { setStatusDisconnected(); node.logger.error( `connectDevice(): An error had occurred with tuya API on connect method : ${JSON.stringify( e )}` ); if (shouldTryReconnect) { node.logger.log('connectDevice(): retrying the connect'); if (findTimeoutHandler) { clearTimeout(findTimeoutHandler); } findTimeoutHandler = setTimeout(findDevice, node.retryTimeout); } else { node.logger.debug( 'connectDevice(): not retrying the find as shouldTryReconnect = false' ); } }); } else { node.logger.debug( 'connectDevice() : already connected. skippig the connect call' ); setStatusConnected(); } } catch (error) { setStatusOnError(error.message, 'Uncaught error : ' + error.message, { context: { message: error.message, deviceVirtualId: node.deviceId, deviceIp: node.deviceIp, deviceKey: node.deviceKey, }, }); } }; let findDevice = () => { try { setStatusConnecting(); node.logger.log('findDevice(): Initiating the find command'); tuyaDevice .find({ timeout: parseInt(node.findTimeout / 1000), }) .then(() => { node.logger.log('findDevice(): Found device, going to connect'); // Connect to device connectDevice(); }) .catch((e) => { // We need to retry setStatusOnError(e.message, "Can't find device", { context: { message: e, deviceVirtualId: node.deviceId, deviceIp: node.deviceIp, deviceKey: node.deviceKey, deviceName: node.deviceName, }, }); setStatusDisconnected(); if (shouldTryReconnect) { node.logger.error( 'findDevice(): Cannot find the device, re-trying...' ); findTimeoutHandler = setTimeout(findDevice, node.retryTimeout); } else { node.logger.debug( 'findDevice(): not retrying the find as shouldTryReconnect = false' ); } }); } catch (error) { setStatusOnError(error.message, 'Uncaught error : ' + error.message, { context: { message: error.message, deviceVirtualId: node.deviceId, deviceIp: node.deviceIp, deviceKey: node.deviceKey, }, }); } }; // Initial state setTimeout(() => { setStatusDisconnected(); }, 500); // Start probing if (!node.disableAutoStart) { node.logger.log('Auto start probe on connect...'); startComm(); } else { node.logger.log('Auto start probe is disabled '); } } RED.nodes.registerType('tuya-smart-device', TuyaSmartDeviceNode, { credentials: { secretConfig: { type: 'text' }, }, }); };