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.

1,332 lines (1,216 loc) 42.1 kB
const path = require('path'); const { Driver } = require('zwave-js'); const driverVersion = require('zwave-js/package.json').version; const { tryParseDSKFromQRCodeString, parseQRCodeString } = require('@zwave-js/core'); const { ConfigManager } = require('@zwave-js/config'); const ControllerAPI_Process = require('./lib/ControllerAPI').process; const ValueAPI_Process = require('./lib/ValueAPI').process; const CCAPI_Process = require('./lib/CCAPI').process; const NodeAPI_Process = require('./lib/NodeAPI').process; const Driver_Process = require('./lib/DriverAPI').process; const { getNodes } = require('./lib/Fetchers'); const APP_NAME = require('../package.json').name; const APP_VERSION = require('../package.json').version; const FWK = '127c49b6f2928a6579e82ecab64a83fc94a6436f03d5cb670b8ac44412687b75f0667843'; const Networks = {}; class SanitizedEventName { constructor(event) { this.driverName = event; this.redEventName = event.replace(/ /g, '_').toUpperCase(); this.nodeStatusName = event.charAt(0).toUpperCase() + event.substr(1).toLowerCase() + '.'; this.statusNameWithNode = (Node) => { return `Node: ${Node.id} ${this.nodeStatusName}`; }; } } // Create event objects (creates easy to use event hooks) const event_DriverReady = new SanitizedEventName('driver ready'); const event_DriverBootLoader = new SanitizedEventName('bootloader 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_RebuildRoutesDone = new SanitizedEventName('rebuild routes done'); const event_FirmwareUpdateFinished = new SanitizedEventName('firmware update finished'); const event_FirmwareUpdateProgress = new SanitizedEventName('firmware update progress'); const event_NodeFirmwareUpdateFinished = new SanitizedEventName('firmware update finished'); const event_NodeFirmwareUpdateProgress = new SanitizedEventName('firmware update progress'); 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_RebuildRoutesProgress = new SanitizedEventName('rebuild routes progress'); const event_NetworkJoined = new SanitizedEventName('network joined'); const event_NetworkLeft = new SanitizedEventName('network left'); module.exports = function (RED) { const init = function (config) { const self = this; RED.nodes.createNode(self, config); self.config = config; const _ConfigManager = new ConfigManager(); _ConfigManager.loadDeviceIndex(); // S2 Promise Resolvers let grantPromise; let dskPromise; // Controller and Device Nodes let controllerNodes = {}; let deviceNodes = {}; // Public methods (used by config clients) self.registerDeviceNode = (DeviceNodeID, Nodes, Callback) => { deviceNodes[DeviceNodeID] = { Nodes, Callback }; }; self.deregisterDeviceNode = (DeviceNodeID) => { delete deviceNodes[DeviceNodeID]; }; self.registerControllerNode = (ControllerNodeID, Callback) => { controllerNodes[ControllerNodeID] = Callback; }; self.deregisterControllerNode = (ControllerNodeID) => { delete controllerNodes[ControllerNodeID]; }; self.controllerCommand = function (Method, Args) { if (self.driverInstance) { return ControllerAPI_Process(self.driverInstance, Method, Args); } return Promise.reject('Driver Instance'); }; self.valueCommand = function (Method, NodeID, VID, Value, Options) { if (self.driverInstance) { return ValueAPI_Process(self.driverInstance, Method, NodeID, VID, Value, Options); } return Promise.reject('Driver Instance'); }; self.ccCommand = function (Method, CommandClass, CommandClassMethod, NodeID, Endpoint, Args) { if (self.driverInstance) { return CCAPI_Process(self.driverInstance, Method, CommandClass, CommandClassMethod, NodeID, Endpoint, Args); } return Promise.reject('Driver Instance'); }; self.nodeCommand = function (Method, NodeID, Value, Args) { if (self.driverInstance) { return NodeAPI_Process(self.driverInstance, Method, NodeID, Value, Args); } return Promise.reject('Driver Instance'); }; self.driverCommand = function (Method, Args) { if (self.driverInstance) { return Driver_Process(self.driverInstance, Method, Args); } return Promise.reject('Driver Instance'); }; // Last status of network let lastStatus; // Send latest Status to UI and update current const updateLatestStatus = function (Status) { lastStatus = Status; RED.comms.publish(`zwave-js/ui/${self.id}/status`, { status: lastStatus }, false); }; // Create Global API const exposeGlobalAPI = () => { if (self.config.enableGlobalAPI && self.driverInstance) { let Name = self.config.globalAPIName || self.id; Name = Name.replace(/ /g, '_'); const API = { Nodes: this.driverInstance.controller.nodes, toJSON: function () { return { Nodes: 'Sorry, The Nodes collection cannot be represented here. It can only be used by Function nodes.' }; } }; Object.defineProperty(API, 'Nodes', { writable: false }); Object.defineProperty(API, 'toJSON', { writable: false }); self.context().global.set(`${Name}`, API); } }; const removeGlobalAPI = () => { let Name = self.config.globalAPIName || self.id; Name = Name.replace(/ /g, '_'); self.context().global.set(`${Name}`, undefined); }; // Create HTTP API const createHTTPAPI = (resetHTTP) => { if (resetHTTP) { removeHTTPAPI(); } Networks[self.id] = self.config.name; RED.comms.publish('zwave-js/ui/global/addnetwork', { name: self.config.name, id: self.id }, false); RED.httpAdmin.get(`/zwave-js/ui/${self.id}/status`, RED.auth.needsPermission('flows.write'), (_, response) => { response.json({ callSuccess: true, response: lastStatus }); }); RED.httpAdmin.get( `/zwave-js/ui/${self.id}/rebuildroutesprogress`, RED.auth.needsPermission('flows.write'), (_, response) => { if (self.driverInstance.controller.isRebuildingRoutes) { response.json({ callSuccess: true, response: Object.fromEntries(self.driverInstance.controller.rebuildRoutesProgress) }); } else { response.json({ callSuccess: true, response: false }); } } ); RED.httpAdmin.get(`/zwave-js/ui/${self.id}/version`, RED.auth.needsPermission('flows.write'), (_, response) => { response.json({ callSuccess: true, response: { driverVersion: driverVersion, configVersion: self.driverInstance.configManager.configVersion, moudleVersion: APP_VERSION } }); }); RED.httpAdmin.post( `/zwave-js/ui/${self.id}/s2/grant`, RED.auth.needsPermission('flows.write'), (request, response) => { grantPromise(request.body[0]); response.json({ callSuccess: true }); } ); RED.httpAdmin.post( `/zwave-js/ui/${self.id}/s2/dsk`, RED.auth.needsPermission('flows.write'), (request, response) => { dskPromise(request.body[0]); response.json({ callSuccess: true }); } ); RED.httpAdmin.get( `/zwave-js/ui/${self.id}/s2/provisioningentries`, RED.auth.needsPermission('flows.write'), (_, response) => { const Entries = self.driverInstance?.controller.getProvisioningEntries(); response.json({ callSuccess: true, response: Entries }); } ); RED.httpAdmin.post( `/zwave-js/ui/${self.id}/s2/parseqr`, RED.auth.needsPermission('flows.write'), async (request, response) => { const DSK = tryParseDSKFromQRCodeString(request.body[0]); if (DSK !== undefined) { response.json({ callSuccess: true, response: { isDSK: true } }); return; } else { let SSQR; try { SSQR = await parseQRCodeString(request.body[0]); _ConfigManager.lookupDevice(SSQR.manufacturerId, SSQR.productType, SSQR.productId).then((dc) => { response.json({ callSuccess: true, response: { isDSK: false, qrProvisioningInformation: SSQR, deviceConfig: dc } }); }); } catch (Err) { response.json({ callSuccess: false, response: Err.message }); } } } ); RED.httpAdmin.get( `/zwave-js/ui/${self.id}/:api/:method`, RED.auth.needsPermission('flows.write'), (request, response) => { const TargetAPI = request.params.api; const Method = request.params.method; let args = undefined; switch (TargetAPI) { case 'CONTROLLER': if (Method === 'beginJoiningNetwork') { args = [{ strategy: 0 }]; } if (Method === 'backupNVMRaw') { args = [ (bytesRead, total) => { RED.comms.publish( `zwave-js/ui/${self.id}/controller/nvm/backupprogress`, { bytesRead, total, label: 'Extracting NVM...' }, false ); } ]; } self .controllerCommand(Method, args) .then((R) => { response.json({ callSuccess: true, response: R }); }) .catch((error) => { response.json({ callSuccess: false, response: error.message }); }); break; case 'DRIVER': if (Method === 'Restart') { Shutdown().then(() => { RED.comms.publish('zwave-js/ui/global/removenetwork', { id: self.id }, false); response.json({ callSuccess: true }); Startup(true); }); break; } self .driverCommand(Method) .then((R) => { response.json({ callSuccess: true, response: R }); }) .catch((error) => { response.json({ callSuccess: false, response: error.message }); }); break; } } ); RED.httpAdmin.post( `/zwave-js/ui/${self.id}/:api/:method`, RED.auth.needsPermission('flows.write'), (request, response) => { const TargetAPI = request.params.api; const Method = request.params.method; let args = undefined; switch (TargetAPI) { case 'NODE': args = request.body.args; if (Method === 'checkLifelineHealth') { const CB = (round, totalRounds, lastRating, lastResult) => { RED.comms.publish( `zwave-js/ui/${self.id}/nodes/healthcheck`, { nodeId: request.body.nodeId, check: { round, totalRounds, lastRating, lastResult } }, false ); }; args = [5, CB]; } if (Method === 'updateFirmware') { const D = args[0][0].data; const byteArray = Array.isArray(D) ? D : Object.values(D); const uint8Array = new Uint8Array(byteArray); args = [[{ data: uint8Array }]]; } self .nodeCommand(Method, request.body.nodeId, request.body.value, args) .then((R) => { response.json({ callSuccess: true, response: R }); }) .catch((error) => { response.json({ callSuccess: false, response: error.message }); }); break; case 'VALUE': self .valueCommand(Method, request.body.nodeId, request.body.valueId, request.body.value) .then((R) => { response.json({ callSuccess: true, response: R }); }) .catch((error) => { response.json({ callSuccess: false, response: error.message }); }); break; case 'CONTROLLER': args = request.body; if (Method === 'restoreNVM') { const D = args[0].nvmData; const byteArray = Array.isArray(D) ? D : Object.values(D); const uint8Array = new Uint8Array(byteArray); const Send = (Label, done, total) => { RED.comms.publish( `zwave-js/ui/${self.id}/controller/nvm/restoreprogress`, { done, total, label: Label }, false ); }; args = [ uint8Array, (bytesRead, total) => { Send('Converting Restore...', bytesRead, total); }, (bytesWritten, total) => { Send('Writing to NVM...', bytesWritten, total); } ]; } self .controllerCommand(Method, args) .then((R) => { response.json({ callSuccess: true, response: R }); }) .catch((error) => { response.json({ callSuccess: false, response: error.message }); }); break; case 'DRIVER': args = request.body; if (Method === 'firmwareUpdateOTW') { if (args[0].data) { const D = args[0].data; const byteArray = Array.isArray(D) ? D : Object.values(D); const uint8Array = new Uint8Array(byteArray); args = [uint8Array]; } } self .driverCommand(Method, args) .then((R) => { response.json({ callSuccess: true, response: R }); }) .catch((error) => { response.json({ callSuccess: false, response: error.message }); }); break; } } ); }; // Remove HTTP API const removeHTTPAPI = () => { const Check = (Route) => { if (Route.route === undefined) return true; if (!Route.route.path.startsWith(`/zwave-js/ui/${self.id}`)) return true; return false; }; delete Networks[self.id]; RED.comms.publish('zwave-js/ui/global/removenetwork', { id: self.id }, false); RED.httpAdmin._router.stack = RED.httpAdmin._router.stack.filter(Check); }; // On close self.on('close', function (_, done) { removeGlobalAPI(); removeHTTPAPI(); controllerNodes = {}; deviceNodes = {}; Shutdown().then(() => { done(); }); }); // S2 Callbacks const s2Void = () => { RED.comms.publish(`zwave-js/ui/${self.id}/s2/void`, {}, false); }; const grantSecurityClasses = (Request) => { return new Promise((resolve) => { RED.comms.publish(`zwave-js/ui/${self.id}/s2/grant`, Request, false); grantPromise = resolve; }); }; const validateDSKAndEnterPIN = (DSK) => { return new Promise((resolve) => { RED.comms.publish(`zwave-js/ui/${self.id}/s2/dsk`, { dsk: DSK }, false); dskPromise = resolve; }); }; const showDSK = (DSK) => { RED.comms.publish(`zwave-js/ui/${self.id}/controller/slave/dsk`, { slaveJoinDSK: DSK }, false); }; const DSKDone = () => { return; }; // Driver settings const ZWaveOptions = { logConfig: {}, timeouts: {}, securityKeys: {}, securityKeysLongRange: {}, bootloaderMode: 'allow', features: { softReset: self.config.enableSoftReset }, storage: { throttle: self.config.storage_throttle, cacheDir: path.join(RED.settings.userDir || '', 'zwave-js-cache') }, preferences: { scales: { temperature: parseInt(self.config.preferences_scales_temperature), humidity: parseInt(self.config.preferences_scales_humidity) } }, interview: { queryAllUserCodes: self.config.interview_queryAllUserCodes }, apiKeys: { firmwareUpdateService: self.config.apiKeys_firmwareUpdateService || FWK }, inclusionUserCallbacks: { grantSecurityClasses: grantSecurityClasses, validateDSKAndEnterPIN: validateDSKAndEnterPIN, abort: s2Void }, joinNetworkUserCallbacks: { showDSK: showDSK, done: DSKDone }, disableOptimisticValueUpdate: !self.config.disableOptimisticValueUpdate }; if (self.config.storage_deviceConfigPriorityDir) { ZWaveOptions.storage.deviceConfigPriorityDir = self.config.storage_deviceConfigPriorityDir; } if (self.config.storage_deviceConfigExternalDir) { ZWaveOptions.storage.deviceConfigExternalDir = self.config.storage_deviceConfigExternalDir; } // Cleanup Logging config const LogEnabled = self.config.logConfig_level !== 'off'; ZWaveOptions.logConfig.enabled = LogEnabled; ZWaveOptions.logConfig.logToFile = LogEnabled; ZWaveOptions.logConfig.filename = path.join(RED.settings.userDir || '', 'zwavejs'); if (LogEnabled) { ZWaveOptions.logConfig.level = self.config.logConfig_level; } let nodeLogFilter; if (self.config.LogConfig_nodeFilter) { nodeLogFilter = self.config.LogConfig_nodeFilter.split(',').map((N) => parseInt(N.trim())); ZWaveOptions.logConfig.nodeFilter = nodeLogFilter; } // Cleanup Timeouts if (self.config.timeouts_ack) ZWaveOptions.timeouts.ack = parseInt(self.config.timeouts_ack); if (self.config.timeouts_report) ZWaveOptions.timeouts.report = parseInt(self.config.timeouts_report); if (self.config.timeouts_response) ZWaveOptions.timeouts.response = parseInt(self.config.timeouts_response); if (self.config.timeouts_serialAPIStarted) ZWaveOptions.timeouts.serialAPIStarted = parseInt(self.config.timeouts_serialAPIStarted); if (self.config.timeouts_sendDataCallback) ZWaveOptions.timeouts.sendDataCallback = parseInt(self.config.timeouts_sendDataCallback); // Cleanup Keys if (self.config.securityKeys_S0_Legacy) ZWaveOptions.securityKeys.S0_Legacy = Buffer.from(self.config.securityKeys_S0_Legacy, 'hex'); if (self.config.securityKeys_S2_AccessControl) ZWaveOptions.securityKeys.S2_AccessControl = Buffer.from(self.config.securityKeys_S2_AccessControl, 'hex'); if (self.config.securityKeys_S2_Authenticated) ZWaveOptions.securityKeys.S2_Authenticated = Buffer.from(self.config.securityKeys_S2_Authenticated, 'hex'); if (self.config.securityKeys_S2_Unauthenticated) ZWaveOptions.securityKeys.S2_Unauthenticated = Buffer.from(self.config.securityKeys_S2_Unauthenticated, 'hex'); if (self.config.securityKeys_S2LR_Authenticated) ZWaveOptions.securityKeysLongRange.S2_Authenticated = Buffer.from( self.config.securityKeys_S2LR_Authenticated, 'hex' ); if (self.config.securityKeys_S2LR_AccessControl) ZWaveOptions.securityKeysLongRange.S2_AccessControl = Buffer.from( self.config.securityKeys_S2LR_AccessControl, 'hex' ); // Driver callback subscriptions const wireDriverEvents = (resetHTTP) => { createHTTPAPI(resetHTTP); // Bootloader Mode REady self.driverInstance?.once(event_DriverBootLoader.driverName, () => { /* We do this to monitor recovery */ // Firmware Update Progress (Controller) self.driverInstance?.on(event_FirmwareUpdateProgress.driverName, (progress) => { RED.comms.publish(`zwave-js/ui/${self.id}/driver/firmwareupdate/progress`, { ...progress }, false); }); updateLatestStatus(event_DriverBootLoader.nodeStatusName); const ControllerNodeIDs = Object.keys(controllerNodes); const Status = { Type: 'STATUS', Status: { fill: 'red', shape: 'dot', text: event_DriverBootLoader.nodeStatusName } }; ControllerNodeIDs.forEach((ID) => { controllerNodes[ID](Status); }); }); // Driver ready self.driverInstance?.once(event_DriverReady.driverName, () => { exposeGlobalAPI(); wireSubDriverEvents(); const ControllerNodeIDs = Object.keys(controllerNodes); const Status = { Type: 'STATUS', Status: { fill: 'green', shape: 'dot', text: event_DriverReady.nodeStatusName, clearTime: 5000 } }; updateLatestStatus(event_DriverReady.nodeStatusName); ControllerNodeIDs.forEach((ID) => { controllerNodes[ID](Status); }); self.driverInstance?.controller.nodes.forEach((Node) => { wireNodeEvents(Node); }); }); }; // Driver callback subscriptions that occure after driver ready const wireSubDriverEvents = () => { // Joined As Slave self.driverInstance?.controller.on(event_NetworkJoined.driverName, () => { self.driverInstance?.controller.nodes.forEach((Node) => { wireNodeEvents(Node); }); RED.comms.publish(`zwave-js/ui/${self.id}/controller/slave/joined`, {}, false); }); // Left As Slave self.driverInstance?.controller.on(event_NetworkLeft.driverName, () => { RED.comms.publish(`zwave-js/ui/${self.id}/controller/slave/left`, {}, false); }); // Firmware Update Progress (Controller) self.driverInstance?.removeAllListeners(event_FirmwareUpdateProgress.driverName); // we may have subscribed during recovery self.driverInstance?.on(event_FirmwareUpdateProgress.driverName, (progress) => { RED.comms.publish(`zwave-js/ui/${self.id}/driver/firmwareupdate/progress`, { ...progress }, false); }); // Firmware Update Completed (Controller) self.driverInstance?.on(event_FirmwareUpdateFinished.driverName, (result) => { RED.comms.publish(`zwave-js/ui/${self.id}/driver/firmwareupdate/finished`, { ...result }, false); }); // Al Nodes Ready self.driverInstance?.on(event_AllNodesReady.driverName, () => { const Timestamp = new Date().getTime(); const ControllerNodeIDs = Object.keys(controllerNodes); const Event = { Type: 'EVENT', Event: { event: event_AllNodesReady.redEventName, timestamp: Timestamp } }; const Status = { Type: 'STATUS', Status: { fill: 'green', shape: 'dot', text: event_AllNodesReady.nodeStatusName, clearTime: 5000 } }; updateLatestStatus(event_AllNodesReady.nodeStatusName); ControllerNodeIDs.forEach((ID) => { controllerNodes[ID](Event); controllerNodes[ID](Status); }); }); // Node Added self.driverInstance?.controller.on(event_NodeAdded.driverName, (ThisNode, Result) => { const ControllerNodeIDs = Object.keys(controllerNodes); const Timestamp = new Date().getTime(); RED.comms.publish( `zwave-js/ui/${this.id}/nodes/added`, { nodeId: ThisNode.id, highestSecurityClass: ThisNode.getHighestSecurityClass(), lowSecurity: Result.lowSecurity }, false ); const Event = { Type: 'EVENT', Event: { event: event_NodeAdded.redEventName, timestamp: Timestamp, eventBody: { nodeId: ThisNode.id, lowSecurity: Result.lowSecurity } } }; const Status = { Type: 'STATUS', Status: { fill: 'green', shape: 'dot', text: event_NodeAdded.statusNameWithNode(ThisNode), clearTime: 5000 } }; updateLatestStatus(event_NodeAdded.statusNameWithNode(ThisNode)); ControllerNodeIDs.forEach((ID) => { controllerNodes[ID](Status); controllerNodes[ID](Event); }); wireNodeEvents(ThisNode); }); // Node Removed self.driverInstance?.controller.on(event_NodeRemoved.driverName, (ThisNode, Reason) => { const ControllerNodeIDs = Object.keys(controllerNodes); const Timestamp = new Date().getTime(); RED.comms.publish(`zwave-js/ui/${this.id}/nodes/removed`, { nodeId: ThisNode.id, reason: Reason }, false); const Event = { Type: 'EVENT', Event: { event: event_NodeRemoved.redEventName, timestamp: Timestamp, eventBody: { nodeId: ThisNode.id, reason: Reason } } }; const Status = { Type: 'STATUS', Status: { fill: 'green', shape: 'dot', text: event_NodeRemoved.statusNameWithNode(ThisNode), clearTime: 5000 } }; updateLatestStatus(event_NodeRemoved.statusNameWithNode(ThisNode)); ControllerNodeIDs.forEach((ID) => { controllerNodes[ID](Status); controllerNodes[ID](Event); }); }); // inclusion started self.driverInstance?.controller.on(event_InclusionStarted.driverName, (InclusionStrategy) => { const Timestamp = new Date().getTime(); const ControllerNodeIDs = Object.keys(controllerNodes); const Body = { inclusionStrategy: InclusionStrategy }; const Event = { Type: 'EVENT', Event: { event: event_InclusionStarted.redEventName, timestamp: Timestamp, eventBody: Body } }; const Status = { Type: 'STATUS', Status: { fill: 'yellow', shape: 'dot', text: event_InclusionStarted.nodeStatusName } }; updateLatestStatus(event_InclusionStarted.nodeStatusName); ControllerNodeIDs.forEach((ID) => { controllerNodes[ID](Event); controllerNodes[ID](Status); }); }); // inclusion failed self.driverInstance?.controller.on(event_InclusionFailed.driverName, () => { const Timestamp = new Date().getTime(); const ControllerNodeIDs = Object.keys(controllerNodes); const Event = { Type: 'EVENT', Event: { event: event_InclusionFailed.redEventName, timestamp: Timestamp } }; const Status = { Type: 'STATUS', Status: { fill: 'red', shape: 'dot', text: event_InclusionFailed.nodeStatusName, clearTime: 5000 } }; updateLatestStatus(event_InclusionFailed.nodeStatusName); ControllerNodeIDs.forEach((ID) => { controllerNodes[ID](Event); controllerNodes[ID](Status); }); }); // inclusion stopped self.driverInstance?.controller.on(event_InclusionStopped.driverName, () => { const Timestamp = new Date().getTime(); const ControllerNodeIDs = Object.keys(controllerNodes); const Event = { Type: 'EVENT', Event: { event: event_InclusionStopped.redEventName, timestamp: Timestamp } }; const Status = { Type: 'STATUS', Status: { fill: 'yellow', shape: 'ring', text: event_InclusionStopped.nodeStatusName, clearTime: 5000 } }; updateLatestStatus(event_InclusionStopped.nodeStatusName); ControllerNodeIDs.forEach((ID) => { controllerNodes[ID](Event); controllerNodes[ID](Status); }); }); // exclusion started self.driverInstance?.controller.on(event_ExclusionStarted.driverName, () => { const Timestamp = new Date().getTime(); const ControllerNodeIDs = Object.keys(controllerNodes); const Event = { Type: 'EVENT', Event: { event: event_ExclusionStarted.redEventName, timestamp: Timestamp } }; const Status = { Type: 'STATUS', Status: { fill: 'yellow', shape: 'dot', text: event_ExclusionStarted.nodeStatusName } }; updateLatestStatus(event_ExclusionStarted.nodeStatusName); ControllerNodeIDs.forEach((ID) => { controllerNodes[ID](Event); controllerNodes[ID](Status); }); }); // exclusion failed self.driverInstance?.controller.on(event_ExclusionFailed.driverName, () => { const Timestamp = new Date().getTime(); const ControllerNodeIDs = Object.keys(controllerNodes); const Event = { Type: 'EVENT', Event: { event: event_ExclusionFailed.redEventName, timestamp: Timestamp } }; const Status = { Type: 'STATUS', Status: { fill: 'red', shape: 'dot', text: event_ExclusionFailed.nodeStatusName, clearTime: 5000 } }; updateLatestStatus(event_ExclusionFailed.nodeStatusName); ControllerNodeIDs.forEach((ID) => { controllerNodes[ID](Event); controllerNodes[ID](Status); }); }); // exclusion stopped self.driverInstance?.controller.on(event_ExclusionStopped.driverName, () => { const Timestamp = new Date().getTime(); const ControllerNodeIDs = Object.keys(controllerNodes); const Event = { Type: 'EVENT', Event: { event: event_ExclusionStopped.redEventName, timestamp: Timestamp } }; const Status = { Type: 'STATUS', Status: { fill: 'yellow', shape: 'ring', text: event_ExclusionStopped.nodeStatusName, clearTime: 5000 } }; updateLatestStatus(event_ExclusionStopped.nodeStatusName); ControllerNodeIDs.forEach((ID) => { controllerNodes[ID](Event); controllerNodes[ID](Status); }); }); // Heal finnished self.driverInstance?.controller.on(event_RebuildRoutesDone.driverName, (Result) => { const Timestamp = new Date().getTime(); const ControllerNodeIDs = Object.keys(controllerNodes); const Event = { Type: 'EVENT', Event: { event: event_RebuildRoutesDone.redEventName, timestamp: Timestamp, eventBody: Result } }; const Status = { Type: 'STATUS', Status: { fill: 'green', shape: 'dot', text: event_RebuildRoutesDone.nodeStatusName, clearTime: 5000 } }; updateLatestStatus(event_RebuildRoutesDone.nodeStatusName); ControllerNodeIDs.forEach((ID) => { controllerNodes[ID](Event); controllerNodes[ID](Status); }); }); // Heal Progress self.driverInstance?.controller.on(event_RebuildRoutesProgress.driverName, (Progress) => { RED.comms.publish( `zwave-js/ui/${this.id}/rebuildroutes/progress`, { Progress: Object.fromEntries(Progress) }, false ); const Timestamp = new Date().getTime(); const ControllerNodeIDs = Object.keys(controllerNodes); const Event = { Type: 'EVENT', Event: { event: event_RebuildRoutesProgress.redEventName, timestamp: Timestamp, eventBody: Progress } }; const Count = Progress.size; const Remain = [...Progress.values()].filter((V) => V === 'pending').length; const Completed = Count - Remain; const CompletedPercentage = Math.round((100 * Completed) / (Completed + Remain)); const Status = { Type: 'STATUS', Status: { fill: 'yellow', shape: 'dot', text: `${event_RebuildRoutesProgress.nodeStatusName} : ${CompletedPercentage}%` } }; updateLatestStatus(`${event_RebuildRoutesProgress.nodeStatusName} : ${CompletedPercentage}%`); ControllerNodeIDs.forEach((ID) => { controllerNodes[ID](Event); controllerNodes[ID](Status); }); }); }; // Node callback subscriptions const wireNodeEvents = (Node) => { if (Node.isControllerNode) { return; } // Firmware Update Progress (Node) Node.on(event_NodeFirmwareUpdateProgress.driverName, (ThisNode, progress) => { RED.comms.publish( `zwave-js/ui/${self.id}/nodes/firmwareupdate/progress`, { nodeId: ThisNode.id, progress }, false ); }); // Firmware Update Completed (Node) Node.on(event_NodeFirmwareUpdateFinished.driverName, (ThisNode, result) => { RED.comms.publish( `zwave-js/ui/${self.id}/nodes/firmwareupdate/finished`, { nodeId: ThisNode.id, result }, false ); }); // Ready Node.on(event_Ready.driverName, (ThisNode) => { const Timestamp = new Date().getTime(); const InterestedDeviceNodes = Object.values(deviceNodes).filter( (I) => I.Nodes.includes(Node.id) || I.Nodes[0] === 0 ); const Event = { Type: 'EVENT', Event: { event: event_Ready.redEventName, timestamp: Timestamp, nodeId: ThisNode.id, nodeName: ThisNode.name, nodeLocation: ThisNode.location } }; InterestedDeviceNodes.forEach((Target) => Target.Callback(Event)); const NodeInfo = getNodes(self.driverInstance).find((N) => N.nodeId === ThisNode.id); RED.comms.publish(`zwave-js/ui/${this.id}/nodes/ready`, { nodeInfo: NodeInfo }, false); }); // Alive Node.on(event_Alive.driverName, (ThisNode, OldStatus) => { const Timestamp = new Date().getTime(); const InterestedDeviceNodes = Object.values(deviceNodes).filter( (I) => I.Nodes.includes(Node.id) || I.Nodes[0] === 0 ); const Event = { Type: 'EVENT', Event: { event: event_Alive.redEventName, timestamp: Timestamp, nodeId: ThisNode.id, nodeName: ThisNode.name, nodeLocation: ThisNode.location, eventBody: { oldStatus: OldStatus } } }; InterestedDeviceNodes.forEach((Target) => Target.Callback(Event)); const NodeInfo = getNodes(self.driverInstance).find((N) => N.nodeId === ThisNode.id); RED.comms.publish(`zwave-js/ui/${this.id}/nodes/alive`, { nodeInfo: NodeInfo }, false); }); // Wake Node.on(event_Wake.driverName, (ThisNode, OldStatus) => { const Timestamp = new Date().getTime(); const InterestedDeviceNodes = Object.values(deviceNodes).filter( (I) => I.Nodes.includes(Node.id) || I.Nodes[0] === 0 ); const Event = { Type: 'EVENT', Event: { event: event_Wake.redEventName, timestamp: Timestamp, nodeId: ThisNode.id, nodeName: ThisNode.name, nodeLocation: ThisNode.location, eventBody: { oldStatus: OldStatus } } }; InterestedDeviceNodes.forEach((Target) => Target.Callback(Event)); const NodeInfo = getNodes(self.driverInstance).find((N) => N.nodeId === ThisNode.id); RED.comms.publish(`zwave-js/ui/${this.id}/nodes/wakeup`, { nodeInfo: NodeInfo }, false); }); // Sleep Node.on(event_Sleep.driverName, (ThisNode, OldStatus) => { const Timestamp = new Date().getTime(); const InterestedDeviceNodes = Object.values(deviceNodes).filter( (I) => I.Nodes.includes(Node.id) || I.Nodes[0] === 0 ); const Event = { Type: 'EVENT', Event: { event: event_Sleep.redEventName, timestamp: Timestamp, nodeId: ThisNode.id, nodeName: ThisNode.name, nodeLocation: ThisNode.location, eventBody: { oldStatus: OldStatus } } }; InterestedDeviceNodes.forEach((Target) => Target.Callback(Event)); const NodeInfo = getNodes(self.driverInstance).find((N) => N.nodeId === ThisNode.id); RED.comms.publish(`zwave-js/ui/${this.id}/nodes/sleep`, { nodeInfo: NodeInfo }, false); }); // Dead Node.on(event_Dead.driverName, (ThisNode, OldStatus) => { const Timestamp = new Date().getTime(); const InterestedDeviceNodes = Object.values(deviceNodes).filter( (I) => I.Nodes.includes(Node.id) || I.Nodes[0] === 0 ); const Event = { Type: 'EVENT', Event: { event: event_Dead.redEventName, timestamp: Timestamp, nodeId: ThisNode.id, nodeName: ThisNode.name, nodeLocation: ThisNode.location, eventBody: { oldStatus: OldStatus } } }; InterestedDeviceNodes.forEach((Target) => Target.Callback(Event)); const NodeInfo = getNodes(self.driverInstance).find((N) => N.nodeId === ThisNode.id); RED.comms.publish(`zwave-js/ui/${this.id}/nodes/dead`, { nodeInfo: NodeInfo }, false); }); // Interview Started Node.on(event_InterviewStarted.driverName, (ThisNode) => { const Timestamp = new Date().getTime(); const ControllerNodeIDs = Object.keys(controllerNodes); const Event = { Type: 'EVENT', Event: { event: event_InterviewStarted.redEventName, timestamp: Timestamp, eventBody: { nodeId: ThisNode.id } } }; const Status = { Type: 'STATUS', Status: { fill: 'yellow', shape: 'dot', text: event_InterviewStarted.statusNameWithNode(ThisNode) } }; updateLatestStatus(event_InterviewStarted.statusNameWithNode(ThisNode)); ControllerNodeIDs.forEach((ID) => { controllerNodes[ID](Event); controllerNodes[ID](Status); }); const NodeInfo = getNodes(self.driverInstance).find((N) => N.nodeId === ThisNode.id); RED.comms.publish(`zwave-js/ui/${this.id}/nodes/interviewstarted`, { nodeInfo: NodeInfo }, false); }); // Interview Completed Node.on(event_InterviewCompleted.driverName, (ThisNode) => { const Timestamp = new Date().getTime(); const ControllerNodeIDs = Object.keys(controllerNodes); const Event = { Type: 'EVENT', Event: { event: event_InterviewCompleted.redEventName, timestamp: Timestamp, eventBody: { nodeId: ThisNode.id } } }; const Status = { Type: 'STATUS', Status: { fill: 'green', shape: 'dot', text: event_InterviewCompleted.statusNameWithNode(ThisNode), clearTime: 5000 } }; updateLatestStatus(event_InterviewCompleted.statusNameWithNode(ThisNode)); ControllerNodeIDs.forEach((ID) => { controllerNodes[ID](Event); controllerNodes[ID](Status); }); const NodeInfo = getNodes(self.driverInstance).find((N) => N.nodeId === ThisNode.id); RED.comms.publish(`zwave-js/ui/${this.id}/nodes/interviewed`, { nodeInfo: NodeInfo }, false); }); // Interview Failed Node.on(event_InterviewFailed.driverName, (ThisNode, Args) => { const Timestamp = new Date().getTime(); const ControllerNodeIDs = Object.keys(controllerNodes); const Event = { Type: 'EVENT', Event: { event: event_InterviewFailed.redEventName, timestamp: Timestamp, eventBody: { nodeId: ThisNode.id, args: Args } } }; const Status = { Type: 'STATUS', Status: { fill: 'red', shape: 'dot', text: event_InterviewFailed.statusNameWithNode(ThisNode), clearTime: 5000 } }; updateLatestStatus(event_InterviewFailed.statusNameWithNode(ThisNode)); ControllerNodeIDs.forEach((ID) => { controllerNodes[ID](Event); controllerNodes[ID](Status); }); const NodeInfo = getNodes(self.driverInstance).find((N) => N.nodeId === ThisNode.id); RED.comms.publish(`zwave-js/ui/${this.id}/nodes/interviewfailed`, { nodeInfo: NodeInfo }, false); }); // Value Notification Node.on(event_ValueNotification.driverName, (ThisNode, Args) => { const Timestamp = new Date().getTime(); const InterestedDeviceNodes = Object.values(deviceNodes).filter( (I) => I.Nodes.includes(Node.id) || I.Nodes[0] === 0 ); const { value, ...valueId } = Args; const Event = { Type: 'EVENT', Event: { event: event_ValueNotification.redEventName, timestamp: Timestamp, nodeId: ThisNode.id, nodeName: ThisNode.name, nodeLocation: ThisNode.location, eventBody: { valueId, value } } }; InterestedDeviceNodes.forEach((Target) => Target.Callback(Event)); }); // Value updated Node.on(event_ValueUpdated.driverName, (ThisNode, Args) => { const Timestamp = new Date().getTime(); const InterestedDeviceNodes = Object.values(deviceNodes).filter( (I) => I.Nodes.includes(Node.id) || I.Nodes[0] === 0 ); const { newValue, prevValue, ...valueId } = Args; const Event = { Type: 'EVENT', Event: { event: event_ValueUpdated.redEventName, timestamp: Timestamp, nodeId: ThisNode.id, nodeName: ThisNode.name, nodeLocation: ThisNode.location, eventBody: { valueId, newValue, prevValue } } }; InterestedDeviceNodes.forEach((Target) => Target.Callback(Event)); RED.comms.publish( `zwave-js/ui/${this.id}/nodes/valueupdate`, { nodeId: ThisNode.id, eventBody: Event.Event.eventBody }, false ); }); // Value Added Node.on(event_ValueAdded.driverName, (ThisNode, Args) => { const Timestamp = new Date().getTime(); const InterestedDeviceNodes = Object.values(deviceNodes).filter( (I) => I.Nodes.includes(Node.id) || I.Nodes[0] === 0 ); const { newValue, ...valueId } = Args; const Event = { Type: 'EVENT', Event: { event: event_ValueAdded.redEventName, timestamp: Timestamp, nodeId: ThisNode.id, nodeName: ThisNode.name, nodeLocation: ThisNode.location, eventBody: { valueId, newValue } } }; InterestedDeviceNodes.forEach((Target) => Target.Callback(Event)); RED.comms.publish( `zwave-js/ui/${this.id}/nodes/valueadded`, { nodeId: ThisNode.id, eventBody: Event.Event.eventBody }, false ); }); // Notification Node.on(event_Notification.driverName, (Endpoint, CC, Args) => { const Timestamp = new Date().getTime(); const InterestedDeviceNodes = Object.values(deviceNodes).filter( (I) => I.Nodes.includes(Node.id) || I.Nodes[0] === 0 ); const Event = { Type: 'EVENT', Event: { event: event_Notification.redEventName, timestamp: Timestamp, nodeId: Endpoint.nodeId, nodeName: Node.name, nodeLocation: Node.location, eventBody: { ccId: CC, args: Args } } }; InterestedDeviceNodes.forEach((Target) => Target.Callback(Event)); }); }; const Shutdown = async () => { if (self.driverInstance) { await self.driverInstance.destroy(); self.driverInstance = undefined; } }; const Startup = (resetHTTP) => { try { self.driverInstance = new Driver(self.config.serialPort, ZWaveOptions); } catch (err) { self.error(err); return; } if (self.config.enableStatistics) { self.driverInstance?.enableStatistics({ applicationName: APP_NAME, applicationVersion: APP_VERSION }); } else { self.driverInstance?.disableStatistics(); } wireDriverEvents(resetHTTP); self.driverInstance?.on('error', (Err) => { self.error(Err); }); self.driverInstance .start() .catch((e) => { self.error(e); }) .then(() => { // }); }; Startup(false); }; RED.httpAdmin.get('/zwave-js/ui/global/networks', RED.auth.needsPermission('flows.write'), (_, response) => { response.json({ callSuccess: true, response: Networks }); }); RED.nodes.registerType('zwavejs-runtime', init); };