UNPKG

node-red-contrib-zwave-js

Version:

The most powerful, high performing and highly polished Z-Wave node for Node-RED based on Z-Wave JS. If you want a fully featured Z-Wave framework in your Node-RED instance, you have found it.

2,018 lines (1,819 loc) 58.6 kB
module.exports = function (RED) { const Path = require('path'); const ModulePackage = require('../package.json'); const { NodeEventEmitter } = require('./events'); const ZWaveJS = require('zwave-js'); const { UIServer, SetupGlobals } = require('./ui/server.js'); const { createDefaultTransportFormat, CommandClasses, ZWaveErrorCodes, getCCName } = require('@zwave-js/core'); const Winston = require('winston'); const { Pin2LogTransport } = require('./Pin2LogTransport'); class SanitizedEventName { constructor(event) { this.zwaveName = event; this.redName = event.replace(/ /g, '_').toUpperCase(); this.statusName = event.charAt(0).toUpperCase() + event.substr(1).toLowerCase() + '.'; this.statusNameWithNode = (Node) => { return 'Node: ' + Node.id + ' ' + this.statusName; }; } } const event_DriverReady = new SanitizedEventName('driver ready'); const event_AllNodesReady = new SanitizedEventName('all nodes ready'); const event_NodeAdded = new SanitizedEventName('node added'); const event_NodeRemoved = new SanitizedEventName('node removed'); const event_InclusionStarted = new SanitizedEventName('inclusion started'); const event_InclusionFailed = new SanitizedEventName('inclusion failed'); const event_InclusionStopped = new SanitizedEventName('inclusion stopped'); const event_ExclusionStarted = new SanitizedEventName('exclusion started'); const event_ExclusionFailed = new SanitizedEventName('exclusion failed'); const event_ExclusionStopped = new SanitizedEventName('exclusion stopped'); const event_NetworkHealDone = new SanitizedEventName('rebuild routes done'); const event_FirmwareUpdateFinished = new SanitizedEventName( 'firmware update finished' ); const event_ValueNotification = new SanitizedEventName('value notification'); const event_Notification = new SanitizedEventName('notification'); const event_ValueUpdated = new SanitizedEventName('value updated'); const event_ValueAdded = new SanitizedEventName('value added'); const event_Wake = new SanitizedEventName('wake up'); const event_Sleep = new SanitizedEventName('sleep'); const event_Dead = new SanitizedEventName('dead'); const event_Alive = new SanitizedEventName('alive'); const event_InterviewStarted = new SanitizedEventName('interview started'); const event_InterviewFailed = new SanitizedEventName('interview failed'); const event_InterviewCompleted = new SanitizedEventName( 'interview completed' ); const event_Ready = new SanitizedEventName('ready'); const event_HealNetworkProgress = new SanitizedEventName( 'rebuild routes progress' ); const FWK = '127c49b6f2928a6579e82ecab64a83fc94a6436f03d5cb670b8ac44412687b75f0667843'; SetupGlobals(RED); function Init(config) { RED.nodes.createNode(this, config); const RedNode = this; const NetworkIdentifier = parseInt(config.networkIdentifier || 1); let UI = new UIServer(RED, NetworkIdentifier); let Driver; let Logger; let FileTransport; let Pin2Transport; let _GrantResolve = undefined; let _DSKResolve = undefined; let _ClientSideAuth = undefined; let RecoveryTimer = undefined; let CanRecover = false; const RecoverDriver = (Seconds) => { if (RecoveryTimer !== undefined) { clearTimeout(RecoveryTimer); RecoveryTimer = undefined; } EmitRecoveryEvent(`Recovery Scheduled (${Seconds}s)`); RecoveryTimer = setTimeout(() => { EmitRecoveryEvent('Attempting Recovery'); AttemptRecovery(); }, Seconds * 1000); }; function EmitRecoveryEvent(Event) { SetFlowNodeStatus({ fill: 'red', shape: 'dot', text: 'Watchdog: ' + Event }); UI.Status('Watchdog: ' + Event); Send(undefined, 'WATCHDOG', { status: Event }); } function AttemptRecovery() { Log( 'info', 'NDERED', undefined, '[SHUTDOWN] [WATCHDOG-RECOVERY]', 'Cleaning up...' ); Driver.destroy().then(() => { InitDriver(); StartDriver(); }); } let DriverOptions = {}; // Log function const Log = function (level, label, direction, tag1, msg, tag2) { if (Logger !== undefined) { const logEntry = { direction: ' ', message: msg, level: level, label: label, timestamp: new Date().toJSON(), multiline: Array.isArray(msg) }; if (direction !== undefined) { logEntry.direction = direction === 'IN' ? '« ' : '» '; } if (tag1 !== undefined) { logEntry.primaryTags = tag1; } if (tag2 !== undefined) { logEntry.secondaryTags = tag2; } Logger.log(logEntry); } }; function SetFlowNodeStatus(Status) { Status.text = `[Net: ${NetworkIdentifier}] ${Status.text}`; RedNode.status(Status); } // eslint-disable-next-line no-unused-vars let RestoreReadyTimer; function RestoreReadyStatus() { if (RestoreReadyTimer !== undefined) { clearTimeout(RestoreReadyTimer); RestoreReadyTimer = undefined; } RestoreReadyTimer = setTimeout(() => { const NotReady = []; let AllReady = true; Driver.controller.nodes.forEach((N) => { if ( !N.ready || ZWaveJS.InterviewStage[N.interviewStage] !== 'Complete' ) { NotReady.push(N.id); AllReady = false; } }); if (AllReady) { SetFlowNodeStatus({ fill: 'green', shape: 'dot', text: event_AllNodesReady.statusName }); UI.Status(event_AllNodesReady.statusName); } else { SetFlowNodeStatus({ fill: 'yellow', shape: 'dot', text: 'Nodes : ' + NotReady.toString() + ' Not ready.' }); UI.Status('Nodes : ' + NotReady.toString() + ' Not ready.'); } }, 5000); } // Create Logger (if needed) if (config.logLevel !== 'none' || config.logLevelPin !== 'none') { Logger = Winston.createLogger(); } if (config.logLevel !== 'none') { const FileTransportOptions = { filename: Path.join(RED.settings.userDir, 'zwave-js.log'), format: createDefaultTransportFormat(false, false), level: config.logLevel }; if (config.logFile !== undefined && config.logFile.length > 0) { FileTransportOptions.filename = config.logFile; } FileTransport = new Winston.transports.File(FileTransportOptions); Logger.add(FileTransport); } function P2Log(Info) { RedNode.send([undefined, { payload: Info }]); } if (config.logLevelPin !== 'none') { const Options = { level: config.logLevelPin, callback: P2Log }; Pin2Transport = new Pin2LogTransport(Options); Logger.add(Pin2Transport); } SetFlowNodeStatus({ fill: 'red', shape: 'dot', text: 'Starting Z-Wave driver...' }); UI.Status('Starting Z-Wave driver...'); NodeEventEmitter.on( `zwjs:${NetworkIdentifier}:node:command`, processMessageEvent ); async function processMessageEvent(MSG) { await Input(MSG, undefined, undefined, true); } DriverOptions = {}; // Logging DriverOptions.logConfig = {}; if (Logger !== undefined) { DriverOptions.logConfig.enabled = true; if ( config.logNodeFilter !== undefined && config.logNodeFilter.length > 0 ) { const Nodes = config.logNodeFilter.split(','); const NodesArray = []; Nodes.forEach((N) => { NodesArray.push(parseInt(N)); }); DriverOptions.logConfig.nodeFilter = NodesArray; } DriverOptions.logConfig.transports = []; if (FileTransport !== undefined) { DriverOptions.logConfig.transports.push(FileTransport); } if (Pin2Transport !== undefined) { DriverOptions.logConfig.transports.push(Pin2Transport); } } else { DriverOptions.logConfig.enabled = false; } // Code Interview if (config.intvwUserCodes !== undefined && config.intvwUserCodes) { Log( 'debug', 'NDERED', undefined, '[options] [interview.queryAllUserCodes]', 'Enabled' ); DriverOptions.interview = { queryAllUserCodes: true }; } else { Log( 'debug', 'NDERED', undefined, '[options] [interview.queryAllUserCodes]', 'Disabled' ); DriverOptions.interview = { queryAllUserCodes: false }; } // Optimsitic Value Updates if ( config.disableOptimisticValueUpdate !== undefined && config.disableOptimisticValueUpdate ) { Log( 'debug', 'NDERED', undefined, '[options] [disableOptimisticValueUpdate]', 'Enabled' ); DriverOptions.disableOptimisticValueUpdate = true; } else { Log( 'debug', 'NDERED', undefined, '[options] [disableOptimisticValueUpdate]', 'Disabled' ); DriverOptions.disableOptimisticValueUpdate = false; } DriverOptions.features = {}; // Soft Reset if (config.softResetUSB !== undefined && config.softResetUSB) { Log( 'debug', 'NDERED', undefined, '[options] [features.softReset]', 'Enabled' ); DriverOptions.features.softReset = true; if ( config.serialAPIStarted !== undefined && config.serialAPIStarted.length > 0 ) { Log( 'debug', 'NDERED', undefined, '[options] [timeouts.serialAPIStarted]', config.serialAPIStarted ); DriverOptions.timeouts = {}; DriverOptions.timeouts.serialAPIStarted = parseInt( config.serialAPIStarted ); } } else { Log( 'debug', 'NDERED', undefined, '[options] [features.softReset]', 'Disabled' ); DriverOptions.features.softReset = false; } DriverOptions.storage = {}; // Cache Dir Log( 'debug', 'NDERED', undefined, '[options] [storage.cacheDir]', Path.join(RED.settings.userDir, 'zwave-js-cache') ); DriverOptions.storage.cacheDir = Path.join( RED.settings.userDir, 'zwave-js-cache' ); // Custom Config Path if ( config.customConfigPath !== undefined && config.customConfigPath.length > 0 ) { Log( 'debug', 'NDERED', undefined, '[options] [storage.deviceConfigPriorityDir]', config.customConfigPath ); DriverOptions.storage.deviceConfigPriorityDir = config.customConfigPath; } if ( config.baseConfigPath !== undefined && config.baseConfigPath.length > 0 ) { Log( 'debug', 'NDERED', undefined, '[options] [storage.deviceConfigExternalDir]', config.baseConfigPath ); DriverOptions.storage.deviceConfigExternalDir = config.baseConfigPath; } // Disk throttle if ( config.valueCacheDiskThrottle !== undefined && config.valueCacheDiskThrottle.length > 0 ) { Log( 'debug', 'NDERED', undefined, '[options] [storage.throttle]', config.valueCacheDiskThrottle ); DriverOptions.storage.throttle = config.valueCacheDiskThrottle; } // Timeout if (!DriverOptions.hasOwnProperty('timeouts')) { DriverOptions.timeouts = {}; } if (config.ackTimeout !== undefined && config.ackTimeout.length > 0) { Log( 'debug', 'NDERED', undefined, '[options] [timeouts.ack]', config.ackTimeout ); DriverOptions.timeouts.ack = parseInt(config.ackTimeout); } if ( config.controllerTimeout !== undefined && config.controllerTimeout.length > 0 ) { Log( 'debug', 'NDERED', undefined, '[options] [timeouts.response]', config.controllerTimeout ); DriverOptions.timeouts.response = parseInt(config.controllerTimeout); } if ( config.sendDataCallback !== undefined && config.sendDataCallback.length > 0 ) { Log( 'debug', 'NDERED', undefined, '[options] [timeouts.sendDataCallback]', config.sendDataCallback ); DriverOptions.timeouts.sendDataCallback = parseInt( config.sendDataCallback ); } if ( config.sendResponseTimeout !== undefined && config.sendResponseTimeout.length > 0 ) { Log( 'debug', 'NDERED', undefined, '[options] [timeouts.report]', config.sendResponseTimeout ); DriverOptions.timeouts.report = parseInt(config.sendResponseTimeout); } DriverOptions.securityKeys = {}; const GetKey = (Property, ZWAVEJSName) => { if (config[Property] !== undefined && config[Property].length > 0) { const Buf = Buffer.from(config[Property], 'hex'); Log( 'debug', 'NDERED', undefined, '[options] [securityKeys.' + ZWAVEJSName + ']', 'Encryption key provided', '[' + Buf.length + ' bytes]' ); if (Buf.length === 16) { DriverOptions.securityKeys[ZWAVEJSName] = Buffer.from(Buf); } } }; GetKey('encryptionKey', 'S0_Legacy'); GetKey('encryptionKeyS2U', 'S2_Unauthenticated'); GetKey('encryptionKeyS2A', 'S2_Authenticated'); GetKey('encryptionKeyS2AC', 'S2_AccessControl'); // S2 Callbacks DriverOptions.inclusionUserCallbacks = { grantSecurityClasses: GrantSecurityClasses, validateDSKAndEnterPIN: ValidateDSK, abort: Abort }; // Scales DriverOptions.preferences = { scales: { temperature: 0x00, humidity: 0x00 } }; if (config.scalesTemp !== undefined) { DriverOptions.preferences.scales.temperature = parseInt( config.scalesTemp ); Log( 'debug', 'NDERED', undefined, '[options] [preferences.scales.temperature]', config.scalesTemp ); } if (config.scalesHumidity !== undefined) { DriverOptions.preferences.scales.humidity = parseInt( config.scalesHumidity ); Log( 'debug', 'NDERED', undefined, '[options] [preferences.scales.humidity]', config.scalesHumidity ); } // License Keys DriverOptions.apiKeys = {}; if (config.FWlicenseKey !== undefined && config.FWlicenseKey.length > 0) { if (config.FWlicenseKey.toUpperCase() !== 'NON-COMMERCIAL') { DriverOptions.apiKeys.firmwareUpdateService = config.FWlicenseKey; Log( 'debug', 'NDERED', undefined, '[FWUS]', 'Commercial license applied' ); } else { DriverOptions.apiKeys.firmwareUpdateService = FWK; Log( 'debug', 'NDERED', undefined, '[FWUS]', 'Open source license applied' ); } } else { Log( 'debug', 'NDERED', undefined, '[FWUS]', 'No key provided - Service may fail!' ); } function ShareNodeList() { const NodeList = {}; NodeList['No location'] = []; Driver.controller.nodes.forEach((ZWN) => { if (ZWN.isControllerNode) { return; } const Node = { id: ZWN.id, name: ZWN.name !== undefined ? ZWN.name : 'No name', location: ZWN.location !== undefined ? ZWN.location : 'No location' }; if (!NodeList.hasOwnProperty(Node.location)) { NodeList[Node.location] = []; } NodeList[Node.location].push(Node); }); UI.UpateNodeList(NodeList); } function NodeCheck(ID, SkipReady) { if (Driver.controller.nodes.get(ID) === undefined) { const ErrorMSG = 'Node ' + ID + ' does not exist.'; throw new Error(ErrorMSG); } if (!SkipReady) { if (!Driver.controller.nodes.get(ID).ready) { const ErrorMSG = 'Node ' + ID + ' is not yet ready to receive commands.'; throw new Error(ErrorMSG); } } } function ThrowVirtualNodeLimit() { throw new Error( 'Multicast only supports ValueAPI:setValue and CCAPI set type commands.' ); } RedNode.on('close', (removed, done) => { const Type = removed ? 'DELETE' : 'RESTART'; Log( 'info', 'NDERED', undefined, '[SHUTDOWN] [' + Type + ']', 'Cleaning up...' ); if (RecoveryTimer !== undefined) { clearTimeout(RecoveryTimer); RecoveryTimer = undefined; } CanRecover = false; UI.Unregister(false); UI = undefined; Driver.destroy().then(() => { NodeEventEmitter.removeListener( `zwjs:${NetworkIdentifier}:node:command`, processMessageEvent ); if (Logger !== undefined) { Logger.clear(); Logger = undefined; } if (Pin2Transport !== undefined) { Pin2Transport = undefined; } if (FileTransport !== undefined) { FileTransport = undefined; } Driver = undefined; if (done) { done(); } }); }); RedNode.on('input', Input); const Convert = (msg) => { if (msg.payload.cmd) { const CMD = msg.payload.cmd; const CMDProp = msg.payload.cmdProperties || {}; switch (CMD.api) { case 'DRIVER': msg.payload = { mode: 'DriverAPI', method: CMD.method, params: CMDProp.args }; break; case 'ASSOCIATIONS': msg.payload = { mode: 'AssociationsAPI', method: CMD.method, params: CMDProp.args }; break; case 'CONTROLLER': msg.payload = { mode: 'ControllerAPI', method: CMD.method, params: CMDProp.args }; break; case 'VALUE': msg.payload = { mode: 'ValueAPI', method: CMD.method, node: CMDProp.nodeId }; msg.payload.params = []; msg.payload.params.push(CMDProp.valueId); if (CMD.method === 'setValue') { msg.payload.params.push(CMDProp.value); if (CMDProp.setValueOptions) { msg.payload.params.push(CMDProp.setValueOptions); } } break; case 'CC': msg.payload = { mode: 'CCAPI', cc: CMDProp.commandClass, method: CMDProp.method, node: CMDProp.nodeId, endpoint: CMDProp.endpoint, params: CMDProp.args, responseThroughEvent: msg.payload.responseThroughEvent, forceUpdate: msg.payload.forceUpdate }; break; } } return msg; }; async function Input(msg, send, done, internal) { // For my own sanity, i'll convert the new format back to old format if its being used, as this will be much easier during the transition phase msg = Convert(msg); let Type = 'CONTROLLER'; if (internal !== undefined && internal) { Type = 'EVENT'; } Log('debug', 'NDERED', 'IN', '[' + Type + ']', 'Payload received.'); try { const Mode = msg.payload.mode; switch (Mode) { case 'IEAPI': await IEAPI(msg, send); break; case 'CCAPI': await CCAPI(msg, send); break; case 'ValueAPI': await ValueAPI(msg, send); break; case 'DriverAPI': await DriverAPI(msg, send); break; case 'ControllerAPI': await ControllerAPI(msg, send); break; case 'AssociationsAPI': await AssociationsAPI(msg, send); break; } if (done) { done(); } } catch (er) { Log('error', 'NDERED', undefined, '[ERROR] [INPUT]', er.message); if (done) { done(er); } else { RedNode.error(er); } } } function CheckKey(strategy) { if (strategy === 2) { return; } const KeyRequirementsCFG = { 0: [ 'S0_Legacy', 'S2_Unauthenticated', 'S2_Authenticated', 'S2_AccessControl' ], 1: [ 'S0_Legacy', 'S2_Unauthenticated', 'S2_Authenticated', 'S2_AccessControl' ], 3: ['S0_Legacy'], 4: ['S2_Unauthenticated', 'S2_Authenticated', 'S2_AccessControl'] }; const KeyRequirementsLable = { 0: ['S0 ', 'S2 Unauth ', 'S2 Auth ', 'S2 Access Ctrl'], 1: ['S0 ', 'S2 Unauth ', 'S2 Auth ', 'S2 Access Ctrl'], 3: ['S0'], 4: ['S2 Unauth ', 'S2 Auth ', 'S2 Access Ctrl'] }; const Set = KeyRequirementsCFG[strategy]; Set.forEach((KR) => { if (DriverOptions.securityKeys[KR] === undefined) { const Label = KeyRequirementsLable[strategy]; throw new Error( 'The chosen inclusion strategy require the following keys to be present: ' + Label ); } }); } async function IEAPI(msg, send) { const Method = msg.payload.method; const Params = msg.payload.params || []; switch (Method) { case 'checkKeyReq': try { CheckKey(Params[0]); Send(undefined, 'KEY_CHECK_RESULT', { ok: true }, send); } catch (err) { Send( undefined, 'KEY_CHECK_RESULT', { ok: false, message: err.message }, send ); } break; case 'unprovisionAllSmartStart': const Entries = Driver.controller.getProvisioningEntries(); for (let i = 0; i < Entries.length; i++) { const Entry = Entries[i]; Driver.controller.unprovisionSmartStartNode(Entry.dsk); } Send( undefined, 'ACTION_DONE', { api: arguments.callee.name, method: Method }, send ); break; case 'unprovisionSmartStartNode': Driver.controller.unprovisionSmartStartNode(Params[0]); Send( undefined, 'ACTION_DONE', { api: arguments.callee.name, method: Method }, send ); break; case 'commitScans': Params.forEach((S) => { Driver.controller.provisionSmartStartNode(S); }); Send( undefined, 'ACTION_DONE', { api: arguments.callee.name, method: Method }, send ); break; case 'beginInclusion': await Driver.controller.beginInclusion(Params[0]); Send( undefined, 'ACTION_DONE', { api: arguments.callee.name, method: Method }, send ); break; case 'beginExclusion': const ExOptions = { strategy: ZWaveJS.ExclusionStrategy.ExcludeOnly }; if (Params[0]) { ExOptions.strategy = ZWaveJS.ExclusionStrategy.Unprovision; } await Driver.controller.beginExclusion(ExOptions); Send( undefined, 'ACTION_DONE', { api: arguments.callee.name, method: Method }, send ); break; case 'grantClasses': Grant(Params[0]); Send( undefined, 'ACTION_DONE', { api: arguments.callee.name, method: Method }, send ); break; case 'verifyDSK': VerifyDSK(Params[0]); Send( undefined, 'ACTION_DONE', { api: arguments.callee.name, method: Method }, send ); break; case 'replaceFailedNode': await Driver.controller.replaceFailedNode(Params[0], Params[1]); Send( undefined, 'ACTION_DONE', { api: arguments.callee.name, method: Method }, send ); break; case 'stopIE': const IS = await Driver.controller.stopInclusion(); const ES = await Driver.controller.stopExclusion(); Send( undefined, 'ACTION_DONE', { api: arguments.callee.name, method: Method }, send ); if (IS || ES) { RestoreReadyStatus(); } if (_GrantResolve !== undefined) { _GrantResolve(false); _GrantResolve = undefined; } if (_DSKResolve !== undefined) { _DSKResolve(false); _DSKResolve = undefined; } break; } return; } function GrantSecurityClasses(_Request) { _ClientSideAuth = _Request.clientSideAuth; UI.SendEvent('node-inclusion-step', 'grant security', { classes: _Request.securityClasses }); return new Promise((res) => { _GrantResolve = res; }); } function Grant(Classes) { _GrantResolve({ securityClasses: Classes, clientSideAuth: _ClientSideAuth }); _GrantResolve = undefined; } function ValidateDSK(DSK) { UI.SendEvent('node-inclusion-step', 'verify dsk', { dsk: DSK }); return new Promise((res) => { _DSKResolve = res; }); } function VerifyDSK(Pin) { _DSKResolve(Pin); _DSKResolve = undefined; } function Abort() { if (_GrantResolve !== undefined) { _GrantResolve = undefined; } if (_DSKResolve !== undefined) { _DSKResolve = undefined; } UI.SendEvent('node-inclusion-step', 'aborted'); } async function ControllerAPI(msg, send) { const Method = msg.payload.method; const Params = msg.payload.params || []; const ReturnNode = { id: '' }; Log( 'debug', 'NDERED', 'IN', undefined, printParams('ControllerAPI', undefined, Method, Params) ); let SupportsNN = false; let Result; switch (Method) { case 'getAvailableFirmwareUpdates': NodeCheck(Params[0]); ReturnNode.id = Params[0]; const FWU = await Driver.controller.getAvailableFirmwareUpdates( Params[0] ); Send(ReturnNode, 'FIRMWARE_UPDATE_CHECK_RESULT', FWU, send); break; case 'firmwareUpdateOTA': NodeCheck(Params[0]); ReturnNode.id = Params[0]; await Driver.controller.firmwareUpdateOTA(Params[0], Params[1]); Send(ReturnNode, 'FIRMWARE_UPDATE_STARTED', Params[1], send); break; case 'restoreNVM': Result = await Driver.controller.restoreNVM( Params[0], Params[1], Params[2] ); Send(undefined, 'NVM_RESTORE_DONE', Result, send); break; case 'backupNVMRaw': const Data = await Driver.controller.backupNVMRaw(Params[0]); Send(undefined, 'NVM_BACKUP', Data, send); break; case 'abortFirmwareUpdate': NodeCheck(Params[0]); ReturnNode.id = Params[0]; await Driver.controller.nodes.get(Params[0]).abortFirmwareUpdate(); Send(ReturnNode, 'FIRMWARE_UPDATE_ABORTED', undefined, send); break; case 'updateFirmware': NodeCheck(Params[0]); ReturnNode.id = Params[0]; const Format = ZWaveJS.guessFirmwareFileFormat(Params[2], Params[3]); const Firmware = ZWaveJS.extractFirmware(Params[3], Format); const Package = { data: Firmware.data, firmwareTarget: Params[1] }; await Driver.controller.nodes .get(Params[0]) .updateFirmware([Package]); Send(ReturnNode, 'FIRMWARE_UPDATE_STARTED', Params[1], send); break; case 'getRFRegion': const RFR = await Driver.controller.getRFRegion(); Send(undefined, 'CURRENT_RF_REGION', RFR, send); break; case 'setRFRegion': Result = await Driver.controller.setRFRegion(Params[0]); Send( undefined, 'RF_REGION_SET_RESULT', { targetRegion: Params[0], success: Result }, send ); break; case 'setPowerlevel': Result = await Driver.controller.setPowerlevel(Params[0], Params[1]); Send( undefined, 'CONTROLLER_POWER_LEVEL_SET_RESULT', { targetLevels: { powerlevel: Params[0], measured0dBm: Params[1] }, success: Result }, send ); break; case 'getPowerlevel': Result = await Driver.controller.getPowerlevel(); Send(undefined, 'CONTROLLER_POWER_LEVEL', Result, send); break; case 'toggleRF': Result = await Driver.controller.toggleRF(Params[0]); Send( undefined, 'RF_STATUS_SET_RESULT', { targetStatus: Params[0], success: Result }, send ); break; case 'getNodes': const Nodes = []; Driver.controller.nodes.forEach((N) => { Nodes.push({ nodeId: N.id, name: N.name, location: N.location, status: ZWaveJS.NodeStatus[N.status], ready: N.ready, interviewStage: ZWaveJS.InterviewStage[N.interviewStage], zwavePlusVersion: N.zwavePlusVersion, zwavePlusNodeType: N.zwavePlusNodeType, zwavePlusRoleType: N.zwavePlusRoleType, isListening: N.isListening, isFrequentListening: N.isFrequentListening, canSleep: N.canSleep, isRouting: N.isRouting, supportedDataRates: N.supportedDataRates, maxDataRate: N.maxDataRate, supportsSecurity: N.supportsSecurity, isSecure: N.isSecure, highestSecurityClass: N.getHighestSecurityClass(), protocolVersion: ZWaveJS.ProtocolVersion[N.protocolVersion], manufacturerId: N.manufacturerId, productId: N.productId, productType: N.productType, firmwareVersion: N.firmwareVersion, deviceConfig: N.deviceConfig, isControllerNode: N.isControllerNode, supportsBeaming: N.supportsBeaming, keepAwake: N.keepAwake, lastSeen: new Date(N.lastSeen).getTime(), powerSource: { type: N.supportsCC(CommandClasses.Battery) ? 'battery' : 'mains', level: N.getValue({ commandClass: 128, endpoint: 0, property: 'level' }), isLow: N.getValue({ commandClass: 128, endpoint: 0, property: 'isLow' }) }, statistics: N.isControllerNode ? Driver.controller.statistics : N.statistics }); }); Nodes.sort((A, B) => A.nodeId - B.nodeId); Send(undefined, 'NODE_LIST', Nodes, send); break; case 'keepNodeAwake': NodeCheck(Params[0]); ReturnNode.id = Params[0]; Driver.controller.nodes.get(Params[0]).keepAwake = Params[1]; Send(ReturnNode, 'NODE_KEEP_AWAKE', Params[1], send); break; case 'getNodeNeighbors': NodeCheck(Params[0]); const NIDs = await Driver.controller.getNodeNeighbors(Params[0]); ReturnNode.id = Params[0]; Send(ReturnNode, 'NODE_NEIGHBORS', NIDs, send); break; case 'setNodeName': NodeCheck(Params[0]); Driver.controller.nodes.get(Params[0]).name = Params[1]; SupportsNN = Driver.controller.nodes .get(Params[0]) .supportsCC(CommandClasses['Node Naming and Location']); if (SupportsNN) { await Driver.controller.nodes .get(Params[0]) .commandClasses['Node Naming and Location'].setName(Params[1]); } ReturnNode.id = Params[0]; Send(ReturnNode, 'NODE_NAME_SET', Params[1], send); ShareNodeList(); break; case 'setNodeLocation': NodeCheck(Params[0]); Driver.controller.nodes.get(Params[0]).location = Params[1]; SupportsNN = Driver.controller.nodes .get(Params[0]) .supportsCC(CommandClasses['Node Naming and Location']); if (SupportsNN) { await Driver.controller.nodes .get(Params[0]) .commandClasses[ 'Node Naming and Location' ].setLocation(Params[1]); } ReturnNode.id = Params[0]; Send(ReturnNode, 'NODE_LOCATION_SET', Params[1], send); ShareNodeList(); break; case 'refreshInfo': NodeCheck(Params[0], true); const Stage = ZWaveJS.InterviewStage[ Driver.controller.nodes.get(Params[0]).interviewStage ]; if (Stage !== 'Complete') { const ErrorMSG = 'Node ' + Params[0] + ' is already being interviewed. Current interview stage : ' + Stage + ''; throw new Error(ErrorMSG); } else { await Driver.controller.nodes.get(Params[0]).refreshInfo(); Send( undefined, 'ACTION_DONE', { api: arguments.callee.name, method: Method }, send ); } break; case 'hardReset': await Driver.hardReset(); Send(undefined, 'CONTROLLER_RESET_COMPLETE', undefined, send); break; case 'rebuildNodeRoutes': NodeCheck(Params[0]); ReturnNode.id = Params[0]; Send(ReturnNode, 'NODE_HEAL_STARTED', undefined, send); SetFlowNodeStatus({ fill: 'yellow', shape: 'dot', text: 'Rebuild Node Routes Started: ' + Params[0] }); UI.Status('Rebuild Node Routes Started: ' + Params[0]); const HealResponse = await Driver.controller.rebuildNodeRoutes( Params[0] ); if (HealResponse) { SetFlowNodeStatus({ fill: 'green', shape: 'dot', text: 'Rebuild Node Routes Successful: ' + Params[0] }); UI.Status('Rebuild Node Routes Successful ' + Params[0]); } else { SetFlowNodeStatus({ fill: 'red', shape: 'dot', text: 'Rebuild Node Routes Unsuccessful: ' + Params[0] }); UI.Status('Rebuild Node Routes Unsuccessful: ' + Params[0]); } Send( ReturnNode, 'NODE_HEAL_FINISHED', { success: HealResponse }, send ); RestoreReadyStatus(); break; case 'beginRebuildingRoutes': await Driver.controller.beginRebuildingRoutes(); Send(undefined, 'NETWORK_HEAL_STARTED', undefined, send); SetFlowNodeStatus({ fill: 'yellow', shape: 'dot', text: 'Route Rebuilding Started.' }); UI.Status('Route Rebuilding Started.'); break; case 'stopRebuildingRoutes': await Driver.controller.stopRebuildingRoutes(); Send(undefined, 'NETWORK_HEAL_STOPPED', undefined, send); SetFlowNodeStatus({ fill: 'blue', shape: 'dot', text: 'Route Rebuilding Stopped.' }); UI.Status('Route Rebuilding Stopped.'); RestoreReadyStatus(); break; case 'removeFailedNode': await Driver.controller.removeFailedNode(Params[0]); Send( undefined, 'ACTION_DONE', { api: arguments.callee.name, method: Method }, send ); break; case 'proprietaryFunction': const ZWaveMessage = new ZWaveJS.Message(Driver, { type: ZWaveJS.MessageType.Request, functionType: Params[0], payload: Params[1] }); const MessageSettings = { priority: ZWaveJS.MessagePriority.Controller, supportCheck: false }; await Driver.sendMessage(ZWaveMessage, MessageSettings); Send( undefined, 'ACTION_DONE', { api: arguments.callee.name, method: Method }, send ); break; } return; } async function ValueAPI(msg, send) { const Method = msg.payload.method; const Params = msg.payload.params || []; const Node = msg.payload.node; const Multicast = Array.isArray(Node); let ZWaveNode; if (Multicast) { ZWaveNode = Driver.controller.getMulticastGroup(Node); } else { NodeCheck(Node); ZWaveNode = Driver.controller.nodes.get(Node); } Log( 'debug', 'NDERED', 'IN', '[Node: ' + ZWaveNode.id + ']', printParams('ValueAPI', undefined, Method, Params) ); const ReturnNode = { id: ZWaveNode.id }; switch (Method) { case 'getValueTimestamp': if (Multicast) ThrowVirtualNodeLimit(); const TS = ZWaveNode.getValueTimestamp(Params[0]); Send(ReturnNode, 'VALUE_TIMESTAMP', TS, send); break; case 'getDefinedValueIDs': if (Multicast) ThrowVirtualNodeLimit(); const VIDs = ZWaveNode.getDefinedValueIDs(); Send(ReturnNode, 'VALUE_ID_LIST', VIDs, send); break; case 'getValueMetadata': if (Multicast) ThrowVirtualNodeLimit(); const M = ZWaveNode.getValueMetadata(Params[0]); const ReturnObjectM = { ...Params[0], metadata: M }; Send(ReturnNode, 'GET_VALUE_METADATA_RESPONSE', ReturnObjectM, send); break; case 'getValue': if (Multicast) ThrowVirtualNodeLimit(); const V = ZWaveNode.getValue(Params[0]); const ReturnObject = { ...Params[0], currentValue: V }; ReturnObject.normalizedObject = buildNormalized( ReturnObject, ReturnNode.id ); if (msg.isolatedNodeId !== undefined) { ReturnNode.targetFlowNode = msg.isolatedNodeId; delete msg['isolatedNodeId']; } Send(ReturnNode, 'GET_VALUE_RESPONSE', ReturnObject, send); break; case 'setValue': await ZWaveNode.setValue(...Params); Send( undefined, 'ACTION_DONE', { api: arguments.callee.name, method: Method }, send ); break; case 'pollValue': if (Multicast) ThrowVirtualNodeLimit(); await ZWaveNode.pollValue(Params[0]); Send( undefined, 'ACTION_DONE', { api: arguments.callee.name, method: Method }, send ); break; } return; } async function CCAPI(msg, send) { const CC = msg.payload.cc; const Method = msg.payload.method; const Params = msg.payload.params || []; const Node = msg.payload.node; const Endpoint = msg.payload.endpoint || 0; const EnumSelection = msg.payload.enums; const ForceUpdate = msg.payload.forceUpdate; const Multicast = Array.isArray(Node); let IsEventResponse = true; let ZWaveNode; if (Multicast) { ZWaveNode = Driver.controller.getMulticastGroup(Node); } else { NodeCheck(Node); ZWaveNode = Driver.controller.nodes.get(Node); } Log( 'debug', 'NDERED', 'IN', '[Node: ' + ZWaveNode.id + ']', printParams('CCAPI', CC, Method, Params) ); if (msg.payload.responseThroughEvent !== undefined) { IsEventResponse = msg.payload.responseThroughEvent; } const ReturnNode = { id: ZWaveNode.id }; if (EnumSelection !== undefined) { const ParamIndexs = Object.keys(EnumSelection); ParamIndexs.forEach((PI) => { const EnumName = EnumSelection[PI]; const Enum = ZWaveJS[EnumName]; Params[PI] = Enum[Params[PI]]; }); } const Result = await ZWaveNode.getEndpoint(Endpoint).invokeCCAPI( CommandClasses[CC], Method, ...Params ); if (!IsEventResponse && ForceUpdate === undefined) { Send(ReturnNode, 'VALUE_UPDATED', Result, send); } else { Send( undefined, 'ACTION_DONE', { api: arguments.callee.name, method: Method }, send ); } if (ForceUpdate !== undefined) { if (Multicast) ThrowVirtualNodeLimit(); const ValueID = { commandClass: CommandClasses[CC], endpoint: Endpoint }; Object.keys(ForceUpdate).forEach((VIDK) => { ValueID[VIDK] = ForceUpdate[VIDK]; }); Log( 'debug', 'NDERED', undefined, '[POLL]', printForceUpdate(Node, ValueID) ); await ZWaveNode.pollValue(ValueID); } return; } async function DriverAPI(msg, send) { const Method = msg.payload.method; const Params = msg.payload.params || []; Log( 'debug', 'NDERED', 'IN', undefined, printParams('DriverAPI', undefined, Method, Params) ); switch (Method) { case 'getLastEvents': const PL = []; Driver.controller.nodes.forEach((N) => { if (N.isControllerNode) { return; } const I = { node: N.id, nodeName: getNodeInfoForPayload(N.id, 'name'), nodeLocation: getNodeInfoForPayload(N.id, 'location'), timestamp: N.ZWNR_lastSeen || 0, event: N.ZWNR_lastEvent, object: N.ZWNR_lastObject }; PL.push(I); }); Send(undefined, 'LAST_EVENTS_RESULT', PL, send); break; case 'checkLifelineHealth': const NID = Params[0]; const Rounds = Params[1] || undefined; const CallBack = Params[2] || undefined; NodeCheck(NID); const HCResult = await Driver.controller.nodes .get(NID) .checkLifelineHealth(Rounds, CallBack); Send( undefined, 'HEALTH_CHECK_RESULT', { node: NID, health: HCResult }, send ); break; case 'installConfigUpdate': let Success = false; const Version = await Driver.checkForConfigUpdates(); if (Version !== undefined) { Success = await Driver.installConfigUpdate(); } Send( undefined, 'DB_UPDATE_RESULT', { installed: Success, version: Version }, send ); break; case 'getNodeStatistics': const NS = {}; if (Params.length < 1) { Driver.controller.nodes.forEach((N) => { NS[N.id] = N.statistics; }); } else { Params.forEach((NID) => { const _N = Driver.controller.nodes.get(NID); NS[_N.id] = _N.statistics; }); } Send(undefined, 'NODE_STATISTICS', NS, send); break; case 'getControllerStatistics': Send( undefined, 'CONTROLER_STATISTICS', Driver.controller.statistics, send ); break; case 'getValueDB': const Result = []; if (Params.length < 1) { Driver.controller.nodes.forEach((N) => { Params.push(N.id); }); } Params.forEach((NID) => { const G = { nodeId: NID, nodeName: getNodeInfoForPayload(NID, 'name'), nodeLocation: getNodeInfoForPayload(NID, 'location'), values: [] }; const VIDs = Driver.controller.nodes.get(NID).getDefinedValueIDs(); VIDs.forEach((VID) => { const M = Driver.controller.nodes.get(NID).getValueMetadata(VID); const V = Driver.controller.nodes.get(NID).getValue(VID); const VI = { ...VID, currentValue: V }; VI.normalizedObject = buildNormalized(VI, NID); VI.metadata = M; G.values.push(VI); }); Result.push(G); }); Send(undefined, 'VALUE_DB', Result, send); break; } return; } async function AssociationsAPI(msg, send) { const Method = msg.payload.method; const Params = msg.payload.params || []; Log( 'debug', 'NDERED', 'IN', undefined, printParams('AssociationsAPI', undefined, Method, Params) ); const ReturnNode = { id: '' }; let ResultData; let PL; switch (Method) { case 'getAssociationGroups': NodeCheck(Params[0].nodeId); ResultData = Driver.controller.getAssociationGroups(Params[0]); PL = []; ResultData.forEach((FV, FK) => { const A = { GroupID: FK, AssociationGroupInfo: FV }; PL.push(A); }); ReturnNode.id = Params[0].nodeId; Send( ReturnNode, 'ASSOCIATION_GROUPS', { SourceAddress: Params[0], Groups: PL }, send ); break; case 'getAllAssociationGroups': NodeCheck(Params[0]); ResultData = Driver.controller.getAllAssociationGroups(Params[0]); PL = []; ResultData.forEach((FV, FK) => { const A = { Endpoint: FK, Groups: [] }; FV.forEach((SV, SK) => { const B = { GroupID: SK, AssociationGroupInfo: SV }; A.Groups.push(B); }); PL.push(A); }); ReturnNode.id = Params[0]; Send(ReturnNode, 'ALL_ASSOCIATION_GROUPS', PL, send); break; case 'getAssociations': NodeCheck(Params[0].nodeId); ResultData = Driver.controller.getAssociations(Params[0]); PL = []; ResultData.forEach((FV, FK) => { const A = { GroupID: FK, AssociationAddress: [] }; FV.forEach((AA) => { A.AssociationAddress.push(AA); }); PL.push(A); }); ReturnNode.id = Params[0].nodeId; Send( ReturnNode, 'ASSOCIATIONS', { SourceAddress: Params[0], Associations: PL }, send ); break; case 'getAllAssociations': NodeCheck(Params[0]); ResultData = Driver.controller.getAllAssociations(Params[0]); PL = []; ResultData.forEach((FV, FK) => { const A = { AssociationAddress: FK, Associations: [] }; FV.forEach((SV, SK) => { const B = { GroupID: SK, AssociationAddress: SV }; A.Associations.push(B); }); PL.push(A); }); ReturnNode.id = Params[0]; Send(ReturnNode, 'ALL_ASSOCIATIONS', PL, send); break; case 'addAssociations': NodeCheck(Params[0].nodeId); Params[2].forEach((A) => { if ( Driver.controller.checkAssociation(Params[0], Params[1], A) !== ZWaveJS.AssociationCheckResult.OK ) { const ErrorMSG = `Association: Source -> ${JSON.stringify( Params[0] )}, Group -> ${Params[1]}, Destination -> ${JSON.stringify( A )} is not allowed.`; throw new Error(ErrorMSG); } }); await Driver.controller.addAssociations( Params[0], Params[1], Params[2] ); ReturnNode.id = Params[0].nodeId; Send(ReturnNode, 'ASSOCIATIONS_ADDED', undefined, send); break; case 'removeAssociations': NodeCheck(Params[0].nodeId); await Driver.controller.removeAssociations( Params[0], Params[1], Params[2] ); ReturnNode.id = Params[0].nodeId; Send(ReturnNode, 'ASSOCIATIONS_REMOVED', undefined, send); break; case 'removeNodeFromAllAssociations': NodeCheck(Params[0]); await Driver.controller.removeNodeFromAllAssociations(Params[0]); ReturnNode.id = Params[0]; Send(ReturnNode, 'ALL_ASSOCIATIONS_REMOVED', undefined, send); break; } return; } function printParams(Mode, CC, Method, Params) { const Lines = []; if (CC !== undefined) { Lines.push( '[API: ' + Mode + '] [CC: ' + CC + '] [Method: ' + Method + ']' ); } else { Lines.push('[API: ' + Mode + '] [Method: ' + Method + ']'); } if (Params.length > 0) { Lines.push('└─[params]'); let i = 0; Params.forEach((P) => { if (typeof P === 'object') { Lines.push(' ' + (i + ': ') + JSON.stringify(P)); } else { Lines.push(' ' + (i + ': ') + P); } i++; }); } return Lines; } function printForceUpdate(NID, Value) { const Lines = []; Lines.push('[Node: ' + NID + ']'); Lines.push('└─[ValueID]'); const OBKeys = Object.keys(Value); OBKeys.forEach((K) => { Lines.push(' ' + (K + ': ') + Value[K]); }); return Lines; } function getNodeInfoForPayload(NodeID, Property) { try { const Prop = Driver.controller.nodes.get(parseInt(NodeID))[Property]; return Prop; } catch (err) { return undefined; } } function buildNormalized(Payload, Node) { try { const VID = { commandClass: Payload.commandClass, endpoint: Payload.endpoint, property: Payload.property, propertyKey: Payload.propertyKey }; const CCName = getCCName(Payload.commandClass); const Meta = Driver.controller.nodes.get(Node).getValueMetadata(VID); if (Meta === undefined) { return undefined; } const NO = {}; NO.commandClass = `${VID.commandClass} - ${CCName}`; let Key = 'newValue'; if (Payload.hasOwnProperty('currentValue')) { Key = 'currentValue'; } else if (Payload.hasOwnProperty('value')) { Key = 'value'; } if ( Meta.states !== undefined && Meta.states[Payload[Key]] !== undefined ) { NO.type = typeof Meta.states[Payload[Key]]; NO[Key] = Meta.states[Payload[Key]]; if (Key === 'newValue') { NO.prevValue = Meta.states[Payload.prevValue] || Payload.prevValue; } } else { NO.type = typeof Payload[Key]; NO[Key] = Payload[Key]; if (Key === 'newValue') { NO.prevValue = Payload.prevValue; } } if (Meta.label !== undefined) { NO.label = Meta.label; } else { let Name; switch (VID.commandClass) { // Thermostat Setpoint case 'Thermostat Setpoint': case 67: Name = ZWaveJS.getEnumMemberName( ZWaveJS.ThermostatSetpointType, VID.propertyKey ); } NO.label = Name; } if (Meta.unit !== undefined) NO.unit = Meta.unit; if (Meta.description !== undefined) NO.description = Meta.description; return NO; } catch (Err) { return undefined; } } function Send(Node, Subject, Value, send) { // ACTION_DONE is only to sync the state of the UI, we therefore should not pass this on to user reqeusts. // Check if the callback name starts with SERV_ to identify these requests if (Subject === 'ACTION_DONE') { if (send === undefined || !send.name.startsWith('SERV_')) { return; } } // Stop UI Data reaching the User via DNs let SendDNs = true; if (send !== undefined && send.name.startsWith('SERV_')) { SendDNs = false; } const PL = {}; PL.networkId = NetworkIdentifier; let IsolatedNodeId; if (Node !== undefined) { PL.node = Node.nodeId || Node.id; IsolatedNodeId = Node.targetFlowNode || undefined; } if (Node !== undefined) { const N = getNodeInfoForPayload(Node.nodeId || Node.id, 'name'); if (N !== undefined) { PL.nodeName = N; } const L = getNodeInfoForPayload(Node.nodeId || Node.id, 'location'); if (L !== undefined) { PL.nodeLocation = L; } } PL.event = Subject; PL.timestamp = new Date().getTime(); if (Value !== undefined) { PL.object = Value; } let _Subject = ''; if (Node !== undefined) { _Subject = '[Node: ' + (Node.nodeId || Node.id) + '] [' + Subject + ']'; } else { _Subject = '[' + Subject + ']'; } Log('debug', 'NDERED', 'OUT', _Subject, '[DIRECT] Forwarding payload...'); if (send) { send({ payload: PL }); } else { RedNode.send({ payload: PL }); } const AllowedSubjectsForDNs = [ 'VALUE_NOTIFICATION', 'NOTIFICATION', 'VALUE_UPDATED', 'SLEEP', 'WAKE_UP', 'DEAD', 'ALIVE', 'VALUE_ID_LIST', 'GET_VALUE_RESPONSE', 'GET_VALUE_METADATA_RESPONSE', 'VALUE_TIMESTAMP' ]; const TimestampSubjects = [ 'VALUE_NOTIFICATION', 'NOTIFICATION', 'VALUE_UPDATED', 'WAKE_UP', 'ALIVE' ]; if (TimestampSubjects.includes(Subject)) { Driver.controller.nodes.get(Node.nodeId || Node.id).ZWNR_lastSeen = PL.timestamp; Driver.controller.nodes.get(Node.nodeId || Node.id).ZWNR_lastEvent = PL.event; Driver.controller.nodes.get(Node.nodeId || Node.id).ZWNR_lastObject = PL.object; } if (AllowedSubjectsForDNs.includes(Subject) && SendDNs) { if (IsolatedNodeId !== undefined) { Log( 'debug', 'NDERED', 'OUT', _Subject, '[ISOLATED] [' + IsolatedNodeId + '] Forwarding payload...' ); NodeEventEmitter.emit( `zwjs:${NetworkIdentifier}:node:event:isloated:${IsolatedNodeId}`, { payload: PL } ); } else { Log( 'debug', 'NDERED', 'OUT', _Subject, '[EVENT] Forwarding payload...' ); NodeEventEmitter.emit(`zwjs:${NetworkIdentifier}:node:event:all`, { payload: PL }); NodeEventEmitter.emit( `zwjs:${NetworkIdentifier}:node:event:${Node.nodeId || Node.id}`, { payload: PL } ); } } } InitDriver(); StartDriver(); function InitDriver() { try { Log('info', 'NDERED', undefined, undefined, 'Initializing driver...'); Driver = new ZWaveJS.Driver(config.serialPort, DriverOptions); if ( config.sendUsageStatistics !== undefined && config.sendUsageStatistics ) { Log('info', 'NDERED', undefined, '[TELEMETRY]', 'Enabling...'); Dr