diff --git a/CHANGELOG.md b/CHANGELOG.md index 97c011aa..b2582499 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # node-red-contrib-zwave-js Change Log + - 5.1.0 + + **New Features** + - Added an extremely advanced **event-filter** node, allowing node events to be filtered with ease. + - Added the ability to pipe log messages to a 2nd output pin of the Controller Node + - Added a new event type: **ALL_NODES_READY** + + **Fixes** + - Fix phantom parentheses in node location. + + **Changes** + - Improvements to the device paring wizard. + - Bump Zwave JS to 8.2.3 + - 5.0.0 **Breaking Changes** diff --git a/README.md b/README.md index 317ecdca..c03e38c4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ ![Image](./resources/ReadMe.png) -# node-red-contrib-zwave-js +# node-red-contrib-wave-js -THE ultimate Z-Wave node for node-red based on Z-Wave JS. +[![License](https://img.shields.io/npm/l/node-red-contrib-zwave-js)](https://github.com/zwave-js/node-red-contrib-zwave-js/blob/main/LICENSE) +[![Version](https://img.shields.io/npm/v/node-red-contrib-zwave-js)](https://www.npmjs.com/package/node-red-contrib-zwave-js) +[![Node Version](https://img.shields.io/node/v/node-red-contrib-zwave-js)](https://www.npmjs.com/package/node-red-contrib-zwave-js) +[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/zwave-js/node-red-contrib-zwave-js.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/zwave-js/node-red-contrib-zwave-js/context:javascript) +[![Maintenance](https://img.shields.io/npms-io/maintenance-score/node-red-contrib-zwave-js)](https://www.npmjs.com/package/node-red-contrib-zwave-js) +[![Dependencies](https://img.shields.io/david/marcus-j-davies/node-red-contrib-zwave-js)](https://www.npmjs.com/package/node-red-contrib-zwave-js) + + +THE most powerful Z-Wave node for node-red based on Z-Wave JS. If you want a fully featured Z-Wave runtime in your node-red instance, look no further.
> ### ...node-red-contrib-zwave-js is _hands down the best zwave to node red option on the planet._ @@ -22,6 +30,7 @@ If you want a fully featured Z-Wave runtime in your node-red instance, look no f - Network Actions (Include, Exclude, Heal etc etc) - 2 Different API models, catering for both experienced and inexperienced users. - Use one node for your entire network, or a node per Z-Wave device. + - An extremely advanced filter node, to route zwave messages around your flow(s). - Supports multicast to send commands to mulltiple nodes at the same time. - Access to all supported CC's provided by Z-Wave JS. diff --git a/package.json b/package.json index 8ab628c4..a3cf21e1 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,16 @@ { "name": "node-red-contrib-zwave-js", - "version": "5.0.0", + "version": "5.1.0", "license": "MIT", - "description": "An extremely easy to use, zero dependency and feature rich Z-Wave node for Node Red, based on Z-Wave JS.", + "description": "An extremely powerful, easy to use, zero dependency and feature rich Z-Wave node for Node Red, based on Z-Wave JS.", "dependencies": { "@serialport/bindings": "9.2.0", "express": "4.17.1", + "lodash": "4.17.21", "serialport": "9.2.0", "winston": "3.3.3", - "zwave-js": "8.1.1" + "winston-transport": "4.4.0", + "zwave-js": "^8.2.3" }, "devDependencies": { "eslint": "7.31.0", @@ -34,7 +36,8 @@ "node-red": { "nodes": { "zwave-js": "zwave-js/zwave-js.js", - "zwave-device": "zwave-js/zwave-device.js" + "zwave-device": "zwave-js/zwave-device.js", + "event-filter": "zwave-js/event-filter.js" } }, "repository": { diff --git a/resources/DeviceNode.PNG b/resources/DeviceNode.PNG new file mode 100644 index 00000000..83fc72ac Binary files /dev/null and b/resources/DeviceNode.PNG differ diff --git a/resources/FilterNode.PNG b/resources/FilterNode.PNG index 83fc72ac..d20ff427 100644 Binary files a/resources/FilterNode.PNG and b/resources/FilterNode.PNG differ diff --git a/zwave-js/Pin2LogTransport.js b/zwave-js/Pin2LogTransport.js new file mode 100644 index 00000000..0b9addb1 --- /dev/null +++ b/zwave-js/Pin2LogTransport.js @@ -0,0 +1,26 @@ +const WinstonTransport = require('winston-transport'); + +let _CallBack = undefined; + +class Pin2LogTransport extends WinstonTransport { + constructor(options) { + _CallBack = options.callback; + delete options.callback; + super(options); + } +} + +Pin2LogTransport.prototype.log = function (info, next) { + if (_CallBack !== undefined) { + _CallBack(info); + } + next(); +}; + +Pin2LogTransport.close = function () { + _CallBack = undefined; +}; + +module.exports = { + Pin2LogTransport: Pin2LogTransport +}; diff --git a/zwave-js/event-filter.html b/zwave-js/event-filter.html new file mode 100644 index 00000000..15e06303 --- /dev/null +++ b/zwave-js/event-filter.html @@ -0,0 +1,287 @@ + + + + + diff --git a/zwave-js/event-filter.js b/zwave-js/event-filter.js new file mode 100644 index 00000000..39c71bc7 --- /dev/null +++ b/zwave-js/event-filter.js @@ -0,0 +1,133 @@ +'use strict'; +module.exports = function (RED) { + const LD = require('lodash'); + + function Init(config) { + RED.nodes.createNode(this, config); + const node = this; + + node.on('input', Input); + + function compare(a, b) { + if (a.index < b.index) { + return -1; + } + if (a.index > b.index) { + return 1; + } + return 0; + } + + async function Input(msg, send, done) { + const SendingArray = new Array(config.filters.length); + + if ( + msg.payload !== undefined && + msg.payload.event !== undefined && + msg.payload.node !== undefined + ) { + const Filters = config.filters; + let Matched = false; + + let ArrayIndex = -1; + + for (const Filter of Filters.sort(compare)) { + ArrayIndex++; + if (Filter.events.length > 0) { + if (Filter.events.includes(msg.payload.event)) { + if (Filter.valueIds.length > 0) { + for (const ValueID of Filter.valueIds) { + if (IsValueIDMatch(ValueID, msg, msg.payload.event)) { + msg.filter = Filter; + SendingArray[ArrayIndex] = msg; + node.status({ + fill: 'green', + shape: 'dot', + text: 'Last match: ' + Filter.name + }); + send(SendingArray); + Matched = true; + break; + } + } + if (Matched) { + break; + } + } else { + msg.filter = Filter; + SendingArray[ArrayIndex] = msg; + node.status({ + fill: 'green', + shape: 'dot', + text: 'Last match: ' + Filter.name + }); + Matched = true; + send(SendingArray); + break; + } + } + } + } + + if (!Matched) { + node.status({ + fill: 'yellow', + shape: 'dot', + text: 'No match' + }); + } + } else { + node.status({ + fill: 'red', + shape: 'dot', + text: 'Not a ZWave message' + }); + } + + if (done) { + done(); + } + } + + function IsValueIDMatch(ValueID, MSG, Event) { + let Root = MSG.payload.object; + + if (Event === 'GET_VALUE_RESPONSE') { + Root = Root.valueId; + if (!config.strict) { + delete ValueID['endpoint']; + } + const Result = LD.isMatch(Root, ValueID); + return Result; + } + + if (Event === 'VALUE_UPDATED') { + if (!config.strict) { + delete ValueID['endpoint']; + } + const Result = LD.isMatch(Root, ValueID); + return Result; + } + + if (Event === 'NOTIFICATION') { + const Result = LD.isMatch(Root, ValueID); + return Result; + } + + if (Event === 'VALUE_NOTIFICATION') { + if (!config.strict) { + delete ValueID['endpoint']; + } + const Result = LD.isMatch(Root, ValueID); + return Result; + } + } + + node.on('close', (done) => { + if (done) { + done(); + } + }); + } + RED.nodes.registerType('event-filter', Init); +}; diff --git a/zwave-js/icons/rbe.png b/zwave-js/icons/rbe.png new file mode 100644 index 00000000..bc397210 Binary files /dev/null and b/zwave-js/icons/rbe.png differ diff --git a/zwave-js/ui/client.js b/zwave-js/ui/client.js index 930b671f..3461f3d2 100644 --- a/zwave-js/ui/client.js +++ b/zwave-js/ui/client.js @@ -12,10 +12,24 @@ let GrantSelected; let ValidateDSK; let DriverReady = false; +let StepsAPI; +const StepList = { + SecurityMode: 0, + NIF: 1, + Remove: 2, + Classes: 3, + DSK: 4, + AddDone: 5, + AddDoneInsecure: 6, + RemoveDone: 7, + ReplaceSecurityMode: 8, + Aborted: 9 +}; + const ZwaveJsUI = (function () { function modalAlert(message, title) { const Buts = { - Ok: function () { } + Ok: function () {} }; modalPrompt(message, title, Buts); } @@ -351,7 +365,7 @@ const ZwaveJsUI = (function () { _Nodes.push(ND); }); - if (Neigbhors === undefined) { + if (typeof Neigbhors === 'undefined') { Nodes.forEach((N) => { if (N.isControllerNode) { return; @@ -673,54 +687,10 @@ const ZwaveJsUI = (function () { }); } - let StepsAPI; - const StepList = { - SecurityMode: 0, - NIF: 1, - Remove: 2, - Classes: 3, - DSK: 4, - AddDone: 5, - AddDoneInsecure: 6, - RemoveDone: 7, - ReplaceSecurityMode: 8 - }; - const Security2Class = { - 0: { - name: 'S2 Unauthenticated', - description: - 'Like S2 Authenticated, but without verification, that the device being added, is the correct one.' - }, - 1: { - name: 'S2 Authenticated', - description: - 'Allows the device that is being added, to be verifed that it is the corect one.' - }, - 2: { - name: 'S2 Access Control', - description: - 'S2 for door locks, garage doors, access control systems etc.' - }, - 7: { - name: 'S0 Legacy', - description: "S0 for devices that don't support S2." - } - }; - function ListRequestedClass(Classes) { - Classes.securityClasses.forEach((SC) => { - const Class = Security2Class[SC.toString()]; - $('#S2Classes').append(` - - - - - - ${Class.name}
- ${Class.description} - - - `); + Classes.forEach((SC) => { + $('tr#TR_' + SC).css({ opacity: 1.0 }); + $('input#SC_' + SC).prop('disabled', false); }); StepsAPI.setStepIndex(StepList.Classes); @@ -735,6 +705,8 @@ const ZwaveJsUI = (function () { const B = event.target; $(B).html('Please wait...'); + ClearIETimer(); + ClearSecurityCountDown(); $(B).prop('disabled', true); ControllerCMD('IEAPI', 'verifyDSK', undefined, [$('#SC_DSK').val()], true); @@ -744,6 +716,8 @@ const ZwaveJsUI = (function () { const B = event.target; $(B).html('Please wait...'); + ClearIETimer(); + ClearSecurityCountDown(); $(B).prop('disabled', true); const Granted = []; @@ -850,6 +824,8 @@ const ZwaveJsUI = (function () { id: 'IEButton', text: 'Abort', click: function () { + ClearIETimer(); + ClearSecurityCountDown(); ControllerCMD('IEAPI', 'stop', undefined, undefined, true); $(this).dialog('destroy'); } @@ -1005,7 +981,7 @@ const ZwaveJsUI = (function () { .find(`[data-nodeid='${node}'].zwave-js-node-row-location`) .html(`(${object})`); if (node == selectedNode) { - $('#zwave-js-selected-node-location').text(object); + $('#zwave-js-selected-node-location').text(`(${object})`); } GetNodes(); input.hide(); @@ -1013,7 +989,12 @@ const ZwaveJsUI = (function () { }); } else { input.show(); - input.val($('#zwave-js-selected-node-location').text()); + let CurrentLocation = $('#zwave-js-selected-node-location').text(); + CurrentLocation = CurrentLocation.substring( + 1, + CurrentLocation.length - 1 + ); + input.val(CurrentLocation); Button.html('Go'); } } @@ -1353,6 +1334,8 @@ const ZwaveJsUI = (function () { switch (data.type) { case 'node-collection-change': if (data.event === 'node added') { + ClearIETimer(); + ClearSecurityCountDown(); GetNodes(); if ( data.inclusionResult.lowSecurity !== undefined && @@ -1365,6 +1348,8 @@ const ZwaveJsUI = (function () { $('#IEButton').text('Close'); } if (data.event === 'node removed') { + ClearIETimer(); + ClearSecurityCountDown(); GetNodes(); StepsAPI.setStepIndex(StepList.RemoveDone); $('#IEButton').text('Close'); @@ -1374,15 +1359,26 @@ const ZwaveJsUI = (function () { case 'node-inclusion-step': if (data.event === 'grant security') { ListRequestedClass(data.classes); + ClearIETimer(); + StartSecurityCountDown(); } if (data.event === 'verify dsk') { DisplayDSK(data.dsk); + ClearIETimer(); + StartSecurityCountDown(); } if (data.event === 'inclusion started') { StepsAPI.setStepIndex(StepList.NIF); + StartIECountDown(); } if (data.event === 'exclusion started') { StepsAPI.setStepIndex(StepList.Remove); + StartIECountDown(); + } + if (data.event === 'aborted') { + StepsAPI.setStepIndex(StepList.Aborted); + ClearIETimer(); + ClearSecurityCountDown(); } break; @@ -1401,6 +1397,45 @@ const ZwaveJsUI = (function () { } } + let Timer; + let IETime; + let SecurityTime; + function ClearIETimer() { + if (Timer !== undefined) { + clearInterval(Timer); + Timer = undefined; + } + } + function StartIECountDown() { + ClearIETimer(); + IETime = 30; + Timer = setInterval(() => { + IETime--; + $('.countdown').html(IETime + ' seconds remaining...'); + if (IETime <= 0) { + ClearIETimer(); + $('#IEButton').click(); + } + }, 1000); + } + function ClearSecurityCountDown() { + if (Timer !== undefined) { + clearInterval(Timer); + Timer = undefined; + } + } + function StartSecurityCountDown() { + ClearSecurityCountDown(); + SecurityTime = 240; + Timer = setInterval(() => { + SecurityTime--; + $('.countdown').html(SecurityTime + ' seconds remaining...'); + if (SecurityTime <= 0) { + ClearSecurityCountDown(); + } + }, 1000); + } + function renderNode(node) { let SM = ''; if (node.highestSecurityClass !== undefined) { @@ -1690,12 +1725,12 @@ const ZwaveJsUI = (function () { valueId.propertyKeyName ?? valueId.propertyName ?? valueId.property + - (valueId.propertyKey !== undefined - ? `[0x${valueId.propertyKey - .toString(16) - .toUpperCase() - .padStart(2, '0')}]` - : ''); + (valueId.propertyKey !== undefined + ? `[0x${valueId.propertyKey + .toString(16) + .toUpperCase() + .padStart(2, '0')}]` + : ''); $('') .addClass('zwave-js-node-property-name') .text(label) @@ -1716,6 +1751,16 @@ const ZwaveJsUI = (function () { title: 'Information', minHeight: 75, buttons: { + 'Add To Filter Set': function () { + if (AddValueIDToFilter(data.valueId)) { + $(this).dialog('destroy'); + } else { + modalAlert( + 'Please activate the target filter set.', + 'No Active Filter Set' + ); + } + }, Close: function () { $(this).dialog('destroy'); } diff --git a/zwave-js/zwave-device.html b/zwave-js/zwave-device.html index 649841cb..57386c09 100644 --- a/zwave-js/zwave-device.html +++ b/zwave-js/zwave-device.html @@ -1,342 +1,320 @@  \ No newline at end of file +

A Z-Wave device node.

+ +

+ Input:
+ A payload object containing a command to send.
+ params will be dependant on the type of command you are sending. +

+	{
+	   mode: "CCAPI",
+	   cc: "Configuration",
+	   method: "set",
+	   params: [0x18,0x03,1]
+	}
+	
+

+ +

+ Output:
+ A payload containing an event that has occured within the zwave network.
+ The contents of object is dependant on the event. +

+	{
+	   event: "VALUE_UPDATED",
+	   timestamp: "23-12-2020T12:23:23+000",
+	   object: ...
+	}
+	
+

+ diff --git a/zwave-js/zwave-js.html b/zwave-js/zwave-js.html index 96d032dd..03d212a5 100644 --- a/zwave-js/zwave-js.html +++ b/zwave-js/zwave-js.html @@ -2,586 +2,792 @@ - - - - + + \ No newline at end of file +

A Z-Wave Controller for node red.

+ +

+ Input:
+ A payload object containing a command.
+ params will be dependant on the type of command you are sending. +

+	{
+	   node: 2,
+	   mode: "CCAPI",
+	   cc: "Configuration",
+	   method: "set",
+	   params: [0x18,0x03,1]
+	}
+	
+

+ +

+ Output:
+ A payload containing an event that has occured within the zwave network.
+ The contents of object is dependant on the event. +

+	{
+	   node: 2,
+	   event: "VALUE_UPDATED",
+	   timestamp: "23-12-2020T12:23:23+000",
+	   object: ...
+	}
+	
+

+ diff --git a/zwave-js/zwave-js.js b/zwave-js/zwave-js.js index ffda1c8d..42766fb1 100644 --- a/zwave-js/zwave-js.js +++ b/zwave-js/zwave-js.js @@ -1,1618 +1,1668 @@ module.exports = function (RED) { - const SP = require('serialport'); - const Path = require('path'); - const ModulePackage = require('../package.json'); - const ZWaveJS = require('zwave-js'); - const { - createDefaultTransportFormat, - CommandClasses, - ZWaveErrorCodes - } = require('@zwave-js/core'); - const ZWaveJSPackage = require('zwave-js/package.json'); - const Winston = require('winston'); - - const UI = require('./ui/server.js'); - UI.init(RED); - const NodeList = {}; - - function Init(config) { - RED.nodes.createNode(this, config); - const node = this; - - let Driver; - let Logger; - let FileTransport; - - const MaxDriverAttempts = 3; - let DriverAttempts = 0; - const RetryTime = 5000; - let DriverOptions = {}; - - const NodeStats = {}; - let ControllerStats; - - // 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); - } - }; - - // 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) { - node.status({ - fill: 'green', - shape: 'dot', - text: 'All nodes ready!' - }); - UI.status('All nodes ready!'); - } else { - node.status({ - fill: 'yellow', - shape: 'dot', - text: 'Nodes : ' + NotReady.toString() + ' not ready.' - }); - UI.status('Nodes : ' + NotReady.toString() + ' not ready.'); - } - }, 5000); - } - - // Create Logger (if enabled) - if (config.logLevel !== 'none') { - Logger = Winston.createLogger(); - - const FileTransportOptions = { - filename: Path.join(RED.settings.userDir, 'zwave-js-log.txt'), - 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); - } - - node.status({ - fill: 'red', - shape: 'dot', - text: 'Starting Z-Wave driver...' - }); - UI.status('Starting Z-Wave driver...'); - - RED.events.on('zwjs: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 = [FileTransport]; - } else { - DriverOptions.logConfig.enabled = 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; - } - - // 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 - 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.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) { - if ( - config[Property].startsWith('[') && - config[Property].endsWith(']') - ) { - const RemoveBrackets = config[Property].replace('[', '').replace( - ']', - '' - ); - const _Array = RemoveBrackets.split(','); - const _Buffer = []; - for (let i = 0; i < _Array.length; i++) { - if (!isNaN(_Array[i].trim())) { - _Buffer.push(parseInt(_Array[i].trim())); - } - } - Log( - 'debug', - 'NDERED', - undefined, - '[options] [securityKeys.' + ZWAVEJSName + ']', - 'Provided as array', - '[' + _Buffer.length + ' bytes]' - ); - if (_Buffer.length === 16) { - DriverOptions.securityKeys[ZWAVEJSName] = Buffer.from(_Buffer); - } - } else { - Log( - 'debug', - 'NDERED', - undefined, - '[options] [securityKeys.' + ZWAVEJSName + ']', - 'Provided as string', - '[' + config[Property].length + ' characters]' - ); - if (config[Property].length === 16) { - DriverOptions.securityKeys[ZWAVEJSName] = Buffer.from( - config[Property] - ); - } - } - } - }; - - GetKey('encryptionKey', 'S0_Legacy'); - GetKey('encryptionKeyS2U', 'S2_Unauthenticated'); - GetKey('encryptionKeyS2A', 'S2_Authenticated'); - GetKey('encryptionKeyS2AC', 'S2_AccessControl'); - - function ShareNodeList() { - for (const Location in NodeList) delete NodeList[Location]; - - 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); - }); - } - - 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.' - ); - } - - node.on('close', (removed, done) => { - const Type = removed ? 'DELETE' : 'RESTART'; - Log( - 'info', - 'NDERED', - undefined, - '[SHUTDOWN] [' + Type + ']', - 'Cleaning up...' - ); - UI.unregister(); - Driver.destroy(); - RED.events.off('zwjs:node:command', processMessageEvent); - if (done) { - done(); - } - }); - - node.on('input', Input); - - async function Input(msg, send, done, internal) { - 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); - 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 { - node.error(er); - } - } - } - - let _GrantResolve; - let _DSKResolve; - - function CheckKey(strategy) { - if (strategy === 2) { - return; - } - - const KeyRequirementsCFG = { - 0: [ - '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'], - 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) { - const Method = msg.payload.method; - const Params = msg.payload.params || []; - - const Callbacks = { - grantSecurityClasses: GrantSecurityClasses, - validateDSKAndEnterPIN: ValidateDSK, - abort: Abort - }; - - switch (Method) { - case 'beginInclusion': - CheckKey(Params[0].strategy); - Params[0].userCallbacks = Callbacks; - await Driver.controller.beginInclusion(Params[0]); - break; - - case 'beginExclusion': - await Driver.controller.beginExclusion(); - break; - - case 'grantClasses': - Grant(Params[0]); - break; - - case 'verifyDSK': - VerifyDSK(Params[0]); - break; - - case 'replaceNode': - CheckKey(Params[1].strategy); - Params[1].userCallbacks = Callbacks; - await Driver.controller.replaceFailedNode(Params[0], Params[1]); - break; - - case 'stop': - const IS = await Driver.controller.stopInclusion(); - const ES = await Driver.controller.stopExclusion(); - if (IS || ES) { - RestoreReadyStatus(); - } - break; - } - return; - } - - function GrantSecurityClasses(RequestedClasses) { - UI.sendEvent('node-inclusion-step', 'grant security', { - classes: RequestedClasses - }); - return new Promise((res) => { - _GrantResolve = res; - }); - } - - function Grant(Classes) { - _GrantResolve({ - securityClasses: Classes, - clientSideAuth: false - }); - } - - function ValidateDSK(DSK) { - UI.sendEvent('node-inclusion-step', 'verify dsk', { dsk: DSK }); - return new Promise((res) => { - _DSKResolve = res; - }); - } - - function VerifyDSK(Pin) { - _DSKResolve(Pin); - } - - function Abort() { - 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; - - switch (Method) { - 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 'beginFirmwareUpdate': - NodeCheck(Params[0]); - ReturnNode.id = Params[0]; - const Format = ZWaveJS.guessFirmwareFileFormat(Params[2], Params[3]); - const Firmware = ZWaveJS.extractFirmware(Params[3], Format); - await Driver.controller.nodes - .get(Params[0]) - .beginFirmwareUpdate(Firmware.data, Params[1]); - Send(ReturnNode, 'FIRMWARE_UPDATE_STARTED', Params[1], send); - break; - - case 'getRFRegion': - const RFR = await Driver.controller.getRFRegion(); - Send(undefined, 'CURRENT_RF_REGION', ZWaveJS.RFRegion[RFR], send); - break; - - case 'setRFRegion': - await Driver.controller.setRFRegion(ZWaveJS.RFRegion[Params[0]]); - Send(undefined, 'RF_REGION_SET', Params[0], send); - break; - - case 'toggleRF': - await Driver.controller.toggleRF(Params[0]); - Send(undefined, 'RF_STATUS', Params[0], 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 - }); - }); - 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(); - } - break; - - case 'hardReset': - await Driver.hardReset(); - Send(undefined, 'CONTROLLER_RESET_COMPLETE', undefined, send); - break; - - case 'healNode': - NodeCheck(Params[0]); - ReturnNode.id = Params[0]; - Send(ReturnNode, 'NODE_HEAL_STARTED', undefined, send); - node.status({ - fill: 'yellow', - shape: 'dot', - text: 'Node Heal Started: ' + Params[0] - }); - UI.status('Node Heal Started: ' + Params[0]); - const HealResponse = await Driver.controller.healNode(Params[0]); - if (HealResponse) { - node.status({ - fill: 'green', - shape: 'dot', - text: 'Node Heal Successful: ' + Params[0] - }); - UI.status('Node Heal Successful: ' + Params[0]); - } else { - node.status({ - fill: 'red', - shape: 'dot', - text: 'Node Heal Unsuccessful: ' + Params[0] - }); - UI.status('Node Heal Unsuccessful: ' + Params[0]); - } - Send( - ReturnNode, - 'NODE_HEAL_FINISHED', - { success: HealResponse }, - send - ); - RestoreReadyStatus(); - break; - - case 'beginHealingNetwork': - await Driver.controller.beginHealingNetwork(); - Send(undefined, 'NETWORK_HEAL_STARTED', undefined, send); - node.status({ - fill: 'yellow', - shape: 'dot', - text: 'Network Heal Started.' - }); - UI.status('Network Heal Started.'); - break; - - case 'stopHealingNetwork': - await Driver.controller.stopHealingNetwork(); - Send(undefined, 'NETWORK_HEAL_STOPPED', undefined, send); - node.status({ - fill: 'blue', - shape: 'dot', - text: 'Network Heal Stopped.' - }); - UI.status('Network Heal Stopped.'); - RestoreReadyStatus(); - break; - - case 'removeFailedNode': - await Driver.controller.removeFailedNode(Params[0]); - 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); - 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 '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 = { - response: M, - valueId: Params[0] - }; - Send(ReturnNode, 'GET_VALUE_METADATA_RESPONSE', ReturnObjectM, send); - break; - - case 'getValue': - if (Multicast) ThrowVirtualNodeLimit(); - const V = ZWaveNode.getValue(Params[0]); - const ReturnObject = { - response: V, - valueId: Params[0] - }; - Send(ReturnNode, 'GET_VALUE_RESPONSE', ReturnObject, send); - break; - - case 'setValue': - if (Params.length > 2) { - await ZWaveNode.setValue(Params[0], Params[1], Params[2]); - } else { - await ZWaveNode.setValue(Params[0], Params[1]); - } - break; - - case 'pollValue': - if (Multicast) ThrowVirtualNodeLimit(); - await ZWaveNode.pollValue(Params[0]); - 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); - } - - 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 'getNodeStatistics': - if (Params.length < 1) { - Send(undefined, 'NODE_STATISTICS', NodeStats, send); - } else { - const Stats = {}; - Params.forEach((NID) => { - if (NodeStats.hasOwnProperty(NID)) { - Stats[NID] = NodeStats[NID]; - } - }); - Send(undefined, 'NODE_STATISTICS', Stats, send); - } - break; - - case 'getControllerStatistics': - if (ControllerStats === undefined) { - Send( - undefined, - 'CONTROLER_STATISTICS', - 'Statistics Are Pending', - send - ); - } else { - Send(undefined, 'CONTROLER_STATISTICS', ControllerStats, 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 V = Driver.controller.nodes.get(NID).getValue(VID); - const VI = { - currentValue: V, - valueId: VID - }; - 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.isAssociationAllowed(Params[0], Params[1], A) - ) { - 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 + ']'); - - if (Value !== undefined) { - 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 Send(Node, Subject, Value, send) { - const PL = {}; - - if (Node !== undefined) { - PL.node = Node.id; - } - - if (Node !== undefined) { - const N = getNodeInfoForPayload(Node.id, 'name'); - if (N !== undefined) { - PL.nodeName = N; - } - const L = getNodeInfoForPayload(Node.id, 'location'); - if (L !== undefined) { - PL.nodeLocation = L; - } - } - (PL.event = Subject), (PL.timestamp = new Date().toJSON()); - if (Value !== undefined) { - PL.object = Value; - } - - let _Subject = ''; - if (Node !== undefined) { - _Subject = '[Node: ' + Node.id + '] [' + Subject + ']'; - } else { - _Subject = '[' + Subject + ']'; - } - - Log('debug', 'NDERED', 'OUT', _Subject, 'Forwarding payload...'); - - if (send) { - send({ payload: PL }); - } else { - node.send({ payload: PL }); - } - - const AllowedSubjectsForDNs = [ - 'VALUE_NOTIFICATION', - 'NOTIFICATION', - 'VALUE_UPDATED', - 'SLEEP', - 'WAKE_UP', - 'VALUE_ID_LIST', - 'GET_VALUE_RESPONSE', - 'GET_VALUE_METADATA_RESPONSE' - ]; - - if (AllowedSubjectsForDNs.includes(Subject)) { - RED.events.emit('zwjs:node:event:all', { payload: PL }); - RED.events.emit('zwjs:node:event:' + Node.id, { payload: PL }); - } - } - - InitDriver(); - StartDriver(); - - function InitDriver() { - DriverAttempts++; - 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...'); - Driver.enableStatistics({ - applicationName: ModulePackage.name, - applicationVersion: ModulePackage.version - }); - } else { - Log('info', 'NDERED', undefined, '[TELEMETRY]', 'Disabling...'); - Driver.disableStatistics(); - } - } catch (e) { - Log('error', 'NDERED', undefined, '[ERROR] [INIT]', e.message); - node.error(e); - return; - } - - WireDriverEvents(); - UI.unregister(); - UI.register(Driver, Input); - } - - function WireDriverEvents() { - Driver.on('error', (e) => { - if (e.code === ZWaveErrorCodes.Driver_Failed) { - if (DriverAttempts >= MaxDriverAttempts) { - Log('error', 'NDERED', undefined, '[ERROR] [DRIVER]', e.message); - node.error(e); - } else { - Log('error', 'NDERED', undefined, '[ERROR] [DRIVER]', e.message); - Log( - 'debug', - 'NDERED', - undefined, - undefined, - 'Will retry in ' + - RetryTime + - 'ms. Attempted: ' + - DriverAttempts + - ', Max: ' + - MaxDriverAttempts - ); - node.error( - new Error( - 'Driver Failed: Will retry in ' + - RetryTime + - 'ms. Attempted: ' + - DriverAttempts + - ', Max: ' + - MaxDriverAttempts - ) - ); - InitDriver(); - setTimeout(StartDriver, RetryTime); - } - } else { - Log('error', 'NDERED', undefined, '[ERROR] [DRIVER]', e.message); - node.error(e); - } - }); - - Driver.on('all nodes ready', () => { - node.status({ fill: 'green', shape: 'dot', text: 'All nodes ready!' }); - UI.status('All nodes ready!'); - }); - - Driver.once('driver ready', () => { - DriverAttempts = 0; - - node.status({ - fill: 'yellow', - shape: 'dot', - text: 'Initializing network...' - }); - UI.status('Initializing network...'); - - // Add, Remove - Driver.controller.on('node added', (N) => { - ShareNodeList(); - WireNodeEvents(N); - Send(N, 'NODE_ADDED'); - Send(N, 'INTERVIEW_STARTED'); - node.status({ - fill: 'yellow', - shape: 'dot', - text: 'Node: ' + N.id + ' interview started.' - }); - UI.status('Node: ' + N.id + ' interview started.'); - }); - - Driver.controller.on('node removed', (N) => { - ShareNodeList(); - Send(N, 'NODE_REMOVED'); - }); - - // Stats - Driver.controller.on('statistics updated', (S) => { - ControllerStats = S; - }); - - // Include - Driver.controller.on('inclusion started', (Secure) => { - Send(undefined, 'INCLUSION_STARTED', { isSecureInclude: Secure }); - node.status({ - fill: 'yellow', - shape: 'dot', - text: 'Inclusion Started. Secure: ' + Secure - }); - UI.status('Inclusion Started. Secure: ' + Secure); - }); - - Driver.controller.on('inclusion failed', () => { - Send(undefined, 'INCLUSION_FAILED'); - node.status({ fill: 'red', shape: 'dot', text: 'Inclusion failed.' }); - UI.status('Inclusion failed.'); - RestoreReadyStatus(); - }); - - Driver.controller.on('inclusion stopped', () => { - Send(undefined, 'INCLUSION_STOPPED'); - node.status({ - fill: 'green', - shape: 'dot', - text: 'Inclusion Stopped.' - }); - UI.status('Inclusion Stopped.'); - }); - - // Exclusion - Driver.controller.on('exclusion started', () => { - Send(undefined, 'EXCLUSION_STARTED'); - node.status({ - fill: 'yellow', - shape: 'dot', - text: 'Exclusion Started.' - }); - UI.status('Exclusion Started.'); - }); - - Driver.controller.on('exclusion failed', () => { - Send(undefined, 'EXCLUSION_FAILED'); - node.status({ fill: 'red', shape: 'dot', text: 'Exclusion failed.' }); - UI.status('Exclusion failed.'); - RestoreReadyStatus(); - }); - - Driver.controller.on('exclusion stopped', () => { - Send(undefined, 'EXCLUSION_STOPPED'); - node.status({ - fill: 'green', - shape: 'dot', - text: 'Exclusion stopped.' - }); - UI.status('Exclusion stopped.'); - RestoreReadyStatus(); - }); - - // Network Heal - Driver.controller.on('heal network done', () => { - Send(undefined, 'NETWORK_HEAL_DONE', { - Successful: Heal_Done, - Failed: Heal_Failed, - Skipped: Heal_Skipped - }); - node.status({ - fill: 'green', - shape: 'dot', - text: 'Network heal done.' - }); - UI.status('Network heal done.'); - RestoreReadyStatus(); - }); - - const Heal_Pending = []; - const Heal_Done = []; - const Heal_Failed = []; - const Heal_Skipped = []; - - Driver.controller.on('heal network progress', (P) => { - Heal_Pending.length = 0; - Heal_Done.length = 0; - Heal_Failed.length = 0; - Heal_Skipped.length = 0; - - P.forEach((V, K) => { - switch (V) { - case 'pending': - Heal_Pending.push(K); - break; - case 'done': - Heal_Done.push(K); - break; - case 'failed': - Heal_Failed.push(K); - break; - case 'skipped': - Heal_Skipped.push(K); - break; - } - }); - - const Processed = - Heal_Done.length + Heal_Failed.length + Heal_Skipped.length; - const Remain = Heal_Pending.length; - - const Completed = (100 * Processed) / (Processed + Remain); - - node.status({ - fill: 'yellow', - shape: 'dot', - text: - 'Healing network ' + - Math.round(Completed) + - '%, Skipped:[' + - Heal_Skipped + - '], Failed:[' + - Heal_Failed + - ']' - }); - - UI.status( - 'Healing network ' + - Math.round(Completed) + - '%, Skipped:[' + - Heal_Skipped + - '], Failed:[' + - Heal_Failed + - ']' - ); - }); - - ShareNodeList(); - - Driver.controller.nodes.forEach((ZWN) => { - WireNodeEvents(ZWN); - }); - }); - } - - function WireNodeEvents(Node) { - Node.on('ready', (N) => { - if (N.isControllerNode()) { - return; - } - - Node.on('statistics updated', (N, S) => { - NodeStats[Node.id] = S; - }); - - Node.on('firmware update finished', (N, S) => { - Send(N, 'FIRMWARE_UPDATE_COMPLETE', S); - }); - - Node.on('value notification', (N, VL) => { - Send(N, 'VALUE_NOTIFICATION', VL); - }); - - Node.on('notification', (N, CC, ARGS) => { - const OBJ = { - ccId: CC, - args: ARGS - }; - Send(N, 'NOTIFICATION', OBJ); - }); - - Node.on('value added', (N, VL) => { - Send(N, 'VALUE_UPDATED', VL); - }); - - Node.on('value updated', (N, VL) => { - Send(N, 'VALUE_UPDATED', VL); - }); - - Node.on('wake up', (N) => { - Send(N, 'WAKE_UP'); - }); - - Node.on('sleep', (N) => { - Send(N, 'SLEEP'); - }); - }); - - Node.on('interview started', (N) => { - Send(N, 'INTERVIEW_STARTED'); - node.status({ - fill: 'yellow', - shape: 'dot', - text: 'Node: ' + N.id + ' interview started.' - }); - UI.status('Node: ' + N.id + ' interview started.'); - }); - - Node.on('interview failed', (N, Er) => { - Send(N, 'INTERVIEW_FAILED', Er); - node.status({ - fill: 'red', - shape: 'dot', - text: 'Node: ' + N.id + ' interview failed.' - }); - UI.status('Node: ' + N.id + ' interview failed.'); - RestoreReadyStatus(); - }); - - Node.on('interview completed', (N) => { - Send(N, 'INTERVIEW_COMPLETE'); - node.status({ - fill: 'green', - shape: 'dot', - text: 'Node: ' + N.id + ' interview completed.' - }); - UI.status('Node: ' + N.id + ' interview completed.'); - RestoreReadyStatus(); - }); - } - - function StartDriver() { - Log('info', 'NDERED', undefined, undefined, 'Starting driver...'); - Driver.start() - .catch((e) => { - if (e.code === ZWaveErrorCodes.Driver_Failed) { - if (DriverAttempts >= MaxDriverAttempts) { - Log('error', 'NDERED', undefined, '[ERROR] [DRIVER]', e.message); - node.error(e); - } else { - Log('error', 'NDERED', undefined, '[ERROR] [DRIVER]', e.message); - Log( - 'debug', - 'NDERED', - undefined, - undefined, - 'Will retry in ' + - RetryTime + - 'ms. Attempted: ' + - DriverAttempts + - ', Max: ' + - MaxDriverAttempts - ); - node.error( - new Error( - 'Driver failed: Will retry in ' + - RetryTime + - 'ms. Attempted: ' + - DriverAttempts + - ', Max: ' + - MaxDriverAttempts - ) - ); - InitDriver(); - setTimeout(StartDriver, RetryTime); - } - } else { - Log('error', 'NDERED', undefined, '[ERROR] [DRIVER]', e.message); - node.error(e); - } - }) - .then(() => { - // now what - just sit and wait. - }); - } - } - - RED.nodes.registerType('zwave-js', Init); - - RED.httpAdmin.get('/zwjsgetnodelist', function (req, res) { - res.json(NodeList); - }); - - RED.httpAdmin.get('/zwjsgetversion', function (req, res) { - res.json({ - zwjsversion: ZWaveJSPackage.version, - moduleversion: ModulePackage.version - }); - }); - - RED.httpAdmin.get( - '/zwjsgetports', - RED.auth.needsPermission('serial.read'), - function (req, res) { - SP.list() - .then((ports) => { - const a = ports.map((p) => p.path); - res.json(a); - }) - .catch((err) => { - RED.log.error('Error listing serial ports', err); - res.json([]); - }); - } - ); + const SP = require('serialport'); + const Path = require('path'); + const ModulePackage = require('../package.json'); + const ZWaveJS = require('zwave-js'); + const { + createDefaultTransportFormat, + CommandClasses, + ZWaveErrorCodes + } = require('@zwave-js/core'); + const ZWaveJSPackage = require('zwave-js/package.json'); + const Winston = require('winston'); + const { Pin2LogTransport } = require('./Pin2LogTransport'); + + const UI = require('./ui/server.js'); + UI.init(RED); + const NodeList = {}; + + function Init(config) { + RED.nodes.createNode(this, config); + const node = this; + + let Driver; + let Logger; + let FileTransport; + let Pin2Transport; + + let _GrantResolve = undefined; + let _DSKResolve = undefined; + let _ClientSideAuth = undefined; + + const MaxDriverAttempts = 3; + let DriverAttempts = 0; + const RetryTime = 5000; + let DriverOptions = {}; + + const NodeStats = {}; + let ControllerStats; + + // 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); + } + }; + + // 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) { + node.status({ + fill: 'green', + shape: 'dot', + text: 'All nodes ready!' + }); + UI.status('All nodes ready!'); + } else { + node.status({ + 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.txt'), + 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) { + node.send([undefined, { payload: Info }]); + } + + if (config.logLevelPin !== 'none') { + const Options = { + level: config.logLevelPin, + callback: P2Log + }; + Pin2Transport = new Pin2LogTransport(Options); + Logger.add(Pin2Transport); + } + + node.status({ + fill: 'red', + shape: 'dot', + text: 'Starting Z-Wave driver...' + }); + UI.status('Starting Z-Wave driver...'); + + RED.events.on('zwjs: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; + } + + 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; + } + + // 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 + 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.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) { + if ( + config[Property].startsWith('[') && + config[Property].endsWith(']') + ) { + const RemoveBrackets = config[Property].replace('[', '').replace( + ']', + '' + ); + const _Array = RemoveBrackets.split(','); + const _Buffer = []; + for (let i = 0; i < _Array.length; i++) { + if (!isNaN(_Array[i].trim())) { + _Buffer.push(parseInt(_Array[i].trim())); + } + } + Log( + 'debug', + 'NDERED', + undefined, + '[options] [securityKeys.' + ZWAVEJSName + ']', + 'Provided as array', + '[' + _Buffer.length + ' bytes]' + ); + if (_Buffer.length === 16) { + DriverOptions.securityKeys[ZWAVEJSName] = Buffer.from(_Buffer); + } + } else { + Log( + 'debug', + 'NDERED', + undefined, + '[options] [securityKeys.' + ZWAVEJSName + ']', + 'Provided as string', + '[' + config[Property].length + ' characters]' + ); + if (config[Property].length === 16) { + DriverOptions.securityKeys[ZWAVEJSName] = Buffer.from( + config[Property] + ); + } + } + } + }; + + GetKey('encryptionKey', 'S0_Legacy'); + GetKey('encryptionKeyS2U', 'S2_Unauthenticated'); + GetKey('encryptionKeyS2A', 'S2_Authenticated'); + GetKey('encryptionKeyS2AC', 'S2_AccessControl'); + + function ShareNodeList() { + for (const Location in NodeList) delete NodeList[Location]; + + 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); + }); + } + + 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.' + ); + } + + node.on('close', (removed, done) => { + const Type = removed ? 'DELETE' : 'RESTART'; + Log( + 'info', + 'NDERED', + undefined, + '[SHUTDOWN] [' + Type + ']', + 'Cleaning up...' + ); + UI.unregister(); + Driver.destroy(); + RED.events.off('zwjs:node:command', processMessageEvent); + if (Logger !== undefined) { + Logger.clear(); + Logger = undefined; + } + if (Pin2Transport !== undefined) { + Pin2Transport = undefined; + } + if (FileTransport !== undefined) { + FileTransport = undefined; + } + if (done) { + done(); + } + }); + + node.on('input', Input); + + async function Input(msg, send, done, internal) { + 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); + 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 { + node.error(er); + } + } + } + + function CheckKey(strategy) { + if (strategy === 2) { + return; + } + + const KeyRequirementsCFG = { + 0: [ + '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'], + 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) { + const Method = msg.payload.method; + const Params = msg.payload.params || []; + + const Callbacks = { + grantSecurityClasses: GrantSecurityClasses, + validateDSKAndEnterPIN: ValidateDSK, + abort: Abort + }; + + switch (Method) { + case 'beginInclusion': + CheckKey(Params[0].strategy); + Params[0].userCallbacks = Callbacks; + await Driver.controller.beginInclusion(Params[0]); + break; + + case 'beginExclusion': + await Driver.controller.beginExclusion(); + break; + + case 'grantClasses': + Grant(Params[0]); + break; + + case 'verifyDSK': + VerifyDSK(Params[0]); + break; + + case 'replaceNode': + CheckKey(Params[1].strategy); + Params[1].userCallbacks = Callbacks; + await Driver.controller.replaceFailedNode(Params[0], Params[1]); + break; + + case 'stop': + const IS = await Driver.controller.stopInclusion(); + const ES = await Driver.controller.stopExclusion(); + 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; + + switch (Method) { + 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 'beginFirmwareUpdate': + NodeCheck(Params[0]); + ReturnNode.id = Params[0]; + const Format = ZWaveJS.guessFirmwareFileFormat(Params[2], Params[3]); + const Firmware = ZWaveJS.extractFirmware(Params[3], Format); + await Driver.controller.nodes + .get(Params[0]) + .beginFirmwareUpdate(Firmware.data, Params[1]); + Send(ReturnNode, 'FIRMWARE_UPDATE_STARTED', Params[1], send); + break; + + case 'getRFRegion': + const RFR = await Driver.controller.getRFRegion(); + Send(undefined, 'CURRENT_RF_REGION', ZWaveJS.RFRegion[RFR], send); + break; + + case 'setRFRegion': + await Driver.controller.setRFRegion(ZWaveJS.RFRegion[Params[0]]); + Send(undefined, 'RF_REGION_SET', Params[0], send); + break; + + case 'toggleRF': + await Driver.controller.toggleRF(Params[0]); + Send(undefined, 'RF_STATUS', Params[0], 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 + }); + }); + 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(); + } + break; + + case 'hardReset': + await Driver.hardReset(); + Send(undefined, 'CONTROLLER_RESET_COMPLETE', undefined, send); + break; + + case 'healNode': + NodeCheck(Params[0]); + ReturnNode.id = Params[0]; + Send(ReturnNode, 'NODE_HEAL_STARTED', undefined, send); + node.status({ + fill: 'yellow', + shape: 'dot', + text: 'Node Heal Started: ' + Params[0] + }); + UI.status('Node Heal Started: ' + Params[0]); + const HealResponse = await Driver.controller.healNode(Params[0]); + if (HealResponse) { + node.status({ + fill: 'green', + shape: 'dot', + text: 'Node Heal Successful: ' + Params[0] + }); + UI.status('Node Heal Successful: ' + Params[0]); + } else { + node.status({ + fill: 'red', + shape: 'dot', + text: 'Node Heal Unsuccessful: ' + Params[0] + }); + UI.status('Node Heal Unsuccessful: ' + Params[0]); + } + Send( + ReturnNode, + 'NODE_HEAL_FINISHED', + { success: HealResponse }, + send + ); + RestoreReadyStatus(); + break; + + case 'beginHealingNetwork': + await Driver.controller.beginHealingNetwork(); + Send(undefined, 'NETWORK_HEAL_STARTED', undefined, send); + node.status({ + fill: 'yellow', + shape: 'dot', + text: 'Network Heal Started.' + }); + UI.status('Network Heal Started.'); + break; + + case 'stopHealingNetwork': + await Driver.controller.stopHealingNetwork(); + Send(undefined, 'NETWORK_HEAL_STOPPED', undefined, send); + node.status({ + fill: 'blue', + shape: 'dot', + text: 'Network Heal Stopped.' + }); + UI.status('Network Heal Stopped.'); + RestoreReadyStatus(); + break; + + case 'removeFailedNode': + await Driver.controller.removeFailedNode(Params[0]); + 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); + 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 '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 = { + response: M, + valueId: Params[0] + }; + Send(ReturnNode, 'GET_VALUE_METADATA_RESPONSE', ReturnObjectM, send); + break; + + case 'getValue': + if (Multicast) ThrowVirtualNodeLimit(); + const V = ZWaveNode.getValue(Params[0]); + const ReturnObject = { + response: V, + valueId: Params[0] + }; + Send(ReturnNode, 'GET_VALUE_RESPONSE', ReturnObject, send); + break; + + case 'setValue': + if (Params.length > 2) { + await ZWaveNode.setValue(Params[0], Params[1], Params[2]); + } else { + await ZWaveNode.setValue(Params[0], Params[1]); + } + break; + + case 'pollValue': + if (Multicast) ThrowVirtualNodeLimit(); + await ZWaveNode.pollValue(Params[0]); + 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); + } + + 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 'getNodeStatistics': + if (Params.length < 1) { + Send(undefined, 'NODE_STATISTICS', NodeStats, send); + } else { + const Stats = {}; + Params.forEach((NID) => { + if (NodeStats.hasOwnProperty(NID)) { + Stats[NID] = NodeStats[NID]; + } + }); + Send(undefined, 'NODE_STATISTICS', Stats, send); + } + break; + + case 'getControllerStatistics': + if (ControllerStats === undefined) { + Send( + undefined, + 'CONTROLER_STATISTICS', + 'Statistics Are Pending', + send + ); + } else { + Send(undefined, 'CONTROLER_STATISTICS', ControllerStats, 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 V = Driver.controller.nodes.get(NID).getValue(VID); + const VI = { + currentValue: V, + valueId: VID + }; + 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.isAssociationAllowed(Params[0], Params[1], A) + ) { + 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 + ']'); + + if (typeof Value !== 'undefined') { + 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 Send(Node, Subject, Value, send) { + const PL = {}; + + if (Node !== undefined) { + PL.node = Node.id; + } + + if (Node !== undefined) { + const N = getNodeInfoForPayload(Node.id, 'name'); + if (N !== undefined) { + PL.nodeName = N; + } + const L = getNodeInfoForPayload(Node.id, 'location'); + if (L !== undefined) { + PL.nodeLocation = L; + } + } + (PL.event = Subject), (PL.timestamp = new Date().toJSON()); + if (Value !== undefined) { + PL.object = Value; + } + + let _Subject = ''; + if (Node !== undefined) { + _Subject = '[Node: ' + Node.id + '] [' + Subject + ']'; + } else { + _Subject = '[' + Subject + ']'; + } + + Log('debug', 'NDERED', 'OUT', _Subject, 'Forwarding payload...'); + + if (send) { + send({ payload: PL }); + } else { + node.send({ payload: PL }); + } + + const AllowedSubjectsForDNs = [ + 'VALUE_NOTIFICATION', + 'NOTIFICATION', + 'VALUE_UPDATED', + 'SLEEP', + 'WAKE_UP', + 'VALUE_ID_LIST', + 'GET_VALUE_RESPONSE', + 'GET_VALUE_METADATA_RESPONSE' + ]; + + if (AllowedSubjectsForDNs.includes(Subject)) { + RED.events.emit('zwjs:node:event:all', { payload: PL }); + RED.events.emit('zwjs:node:event:' + Node.id, { payload: PL }); + } + } + + InitDriver(); + StartDriver(); + + function InitDriver() { + DriverAttempts++; + 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...'); + Driver.enableStatistics({ + applicationName: ModulePackage.name, + applicationVersion: ModulePackage.version + }); + } else { + Log('info', 'NDERED', undefined, '[TELEMETRY]', 'Disabling...'); + Driver.disableStatistics(); + } + } catch (e) { + Log('error', 'NDERED', undefined, '[ERROR] [INIT]', e.message); + node.error(e); + return; + } + + WireDriverEvents(); + UI.unregister(); + UI.register(Driver, Input); + } + + function WireDriverEvents() { + Driver.on('error', (e) => { + if (e.code === ZWaveErrorCodes.Driver_Failed) { + if (DriverAttempts >= MaxDriverAttempts) { + Log('error', 'NDERED', undefined, '[ERROR] [DRIVER]', e.message); + node.error(e); + } else { + Log('error', 'NDERED', undefined, '[ERROR] [DRIVER]', e.message); + Log( + 'debug', + 'NDERED', + undefined, + undefined, + 'Will retry in ' + + RetryTime + + 'ms. Attempted: ' + + DriverAttempts + + ', Max: ' + + MaxDriverAttempts + ); + node.error( + new Error( + 'Driver Failed: Will retry in ' + + RetryTime + + 'ms. Attempted: ' + + DriverAttempts + + ', Max: ' + + MaxDriverAttempts + ) + ); + InitDriver(); + setTimeout(StartDriver, RetryTime); + } + } else { + Log('error', 'NDERED', undefined, '[ERROR] [DRIVER]', e.message); + node.error(e); + } + }); + + Driver.on('all nodes ready', () => { + node.status({ fill: 'green', shape: 'dot', text: 'All nodes ready!' }); + UI.status('All nodes ready!'); + Send(undefined, 'ALL_NODES_READY'); + }); + + Driver.once('driver ready', () => { + DriverAttempts = 0; + + node.status({ + fill: 'yellow', + shape: 'dot', + text: 'Initializing network...' + }); + UI.status('Initializing network...'); + + // Add, Remove + Driver.controller.on('node added', (N) => { + ShareNodeList(); + WireNodeEvents(N); + Send(N, 'NODE_ADDED'); + Send(N, 'INTERVIEW_STARTED'); + node.status({ + fill: 'yellow', + shape: 'dot', + text: 'Node: ' + N.id + ' interview started.' + }); + UI.status('Node: ' + N.id + ' interview started.'); + }); + + Driver.controller.on('node removed', (N) => { + ShareNodeList(); + Send(N, 'NODE_REMOVED'); + }); + + // Stats + Driver.controller.on('statistics updated', (S) => { + ControllerStats = S; + }); + + // Include + Driver.controller.on('inclusion started', (Secure) => { + Send(undefined, 'INCLUSION_STARTED', { isSecureInclude: Secure }); + node.status({ + fill: 'yellow', + shape: 'dot', + text: 'Inclusion Started. Secure: ' + Secure + }); + UI.status('Inclusion Started. Secure: ' + Secure); + }); + + Driver.controller.on('inclusion failed', () => { + Send(undefined, 'INCLUSION_FAILED'); + node.status({ fill: 'red', shape: 'dot', text: 'Inclusion failed.' }); + UI.status('Inclusion failed.'); + RestoreReadyStatus(); + }); + + Driver.controller.on('inclusion stopped', () => { + Send(undefined, 'INCLUSION_STOPPED'); + node.status({ + fill: 'green', + shape: 'dot', + text: 'Inclusion Stopped.' + }); + UI.status('Inclusion Stopped.'); + }); + + // Exclusion + Driver.controller.on('exclusion started', () => { + Send(undefined, 'EXCLUSION_STARTED'); + node.status({ + fill: 'yellow', + shape: 'dot', + text: 'Exclusion Started.' + }); + UI.status('Exclusion Started.'); + }); + + Driver.controller.on('exclusion failed', () => { + Send(undefined, 'EXCLUSION_FAILED'); + node.status({ fill: 'red', shape: 'dot', text: 'Exclusion failed.' }); + UI.status('Exclusion failed.'); + RestoreReadyStatus(); + }); + + Driver.controller.on('exclusion stopped', () => { + Send(undefined, 'EXCLUSION_STOPPED'); + node.status({ + fill: 'green', + shape: 'dot', + text: 'Exclusion stopped.' + }); + UI.status('Exclusion stopped.'); + RestoreReadyStatus(); + }); + + // Network Heal + Driver.controller.on('heal network done', () => { + Send(undefined, 'NETWORK_HEAL_DONE', { + Successful: Heal_Done, + Failed: Heal_Failed, + Skipped: Heal_Skipped + }); + node.status({ + fill: 'green', + shape: 'dot', + text: 'Network heal done.' + }); + UI.status('Network heal done.'); + RestoreReadyStatus(); + }); + + const Heal_Pending = []; + const Heal_Done = []; + const Heal_Failed = []; + const Heal_Skipped = []; + + Driver.controller.on('heal network progress', (P) => { + Heal_Pending.length = 0; + Heal_Done.length = 0; + Heal_Failed.length = 0; + Heal_Skipped.length = 0; + + P.forEach((V, K) => { + switch (V) { + case 'pending': + Heal_Pending.push(K); + break; + case 'done': + Heal_Done.push(K); + break; + case 'failed': + Heal_Failed.push(K); + break; + case 'skipped': + Heal_Skipped.push(K); + break; + } + }); + + const Processed = + Heal_Done.length + Heal_Failed.length + Heal_Skipped.length; + const Remain = Heal_Pending.length; + + const Completed = (100 * Processed) / (Processed + Remain); + + node.status({ + fill: 'yellow', + shape: 'dot', + text: + 'Healing network ' + + Math.round(Completed) + + '%, Skipped:[' + + Heal_Skipped + + '], Failed:[' + + Heal_Failed + + ']' + }); + + UI.status( + 'Healing network ' + + Math.round(Completed) + + '%, Skipped:[' + + Heal_Skipped + + '], Failed:[' + + Heal_Failed + + ']' + ); + }); + + ShareNodeList(); + + Driver.controller.nodes.forEach((ZWN) => { + WireNodeEvents(ZWN); + }); + }); + } + + function WireNodeEvents(Node) { + Node.on('ready', (N) => { + if (N.isControllerNode()) { + return; + } + + Node.on('statistics updated', (N, S) => { + NodeStats[Node.id] = S; + }); + + Node.on('firmware update finished', (N, S) => { + Send(N, 'FIRMWARE_UPDATE_COMPLETE', S); + }); + + Node.on('value notification', (N, VL) => { + Send(N, 'VALUE_NOTIFICATION', VL); + }); + + Node.on('notification', (N, CC, ARGS) => { + const OBJ = { + ccId: CC, + args: ARGS + }; + Send(N, 'NOTIFICATION', OBJ); + }); + + Node.on('value added', (N, VL) => { + Send(N, 'VALUE_UPDATED', VL); + }); + + Node.on('value updated', (N, VL) => { + Send(N, 'VALUE_UPDATED', VL); + }); + + Node.on('wake up', (N) => { + Send(N, 'WAKE_UP'); + }); + + Node.on('sleep', (N) => { + Send(N, 'SLEEP'); + }); + }); + + Node.on('interview started', (N) => { + Send(N, 'INTERVIEW_STARTED'); + node.status({ + fill: 'yellow', + shape: 'dot', + text: 'Node: ' + N.id + ' interview started.' + }); + UI.status('Node: ' + N.id + ' interview started.'); + }); + + Node.on('interview failed', (N, Er) => { + Send(N, 'INTERVIEW_FAILED', Er); + node.status({ + fill: 'red', + shape: 'dot', + text: 'Node: ' + N.id + ' interview failed.' + }); + UI.status('Node: ' + N.id + ' interview failed.'); + RestoreReadyStatus(); + }); + + Node.on('interview completed', (N) => { + Send(N, 'INTERVIEW_COMPLETE'); + node.status({ + fill: 'green', + shape: 'dot', + text: 'Node: ' + N.id + ' interview completed.' + }); + UI.status('Node: ' + N.id + ' interview completed.'); + RestoreReadyStatus(); + }); + } + + function StartDriver() { + Log('info', 'NDERED', undefined, undefined, 'Starting driver...'); + Driver.start() + .catch((e) => { + if (e.code === ZWaveErrorCodes.Driver_Failed) { + if (DriverAttempts >= MaxDriverAttempts) { + Log('error', 'NDERED', undefined, '[ERROR] [DRIVER]', e.message); + node.error(e); + } else { + Log('error', 'NDERED', undefined, '[ERROR] [DRIVER]', e.message); + Log( + 'debug', + 'NDERED', + undefined, + undefined, + 'Will retry in ' + + RetryTime + + 'ms. Attempted: ' + + DriverAttempts + + ', Max: ' + + MaxDriverAttempts + ); + node.error( + new Error( + 'Driver failed: Will retry in ' + + RetryTime + + 'ms. Attempted: ' + + DriverAttempts + + ', Max: ' + + MaxDriverAttempts + ) + ); + InitDriver(); + setTimeout(StartDriver, RetryTime); + } + } else { + Log('error', 'NDERED', undefined, '[ERROR] [DRIVER]', e.message); + node.error(e); + } + }) + .then(() => { + // now what - just sit and wait. + }); + } + } + + RED.nodes.registerType('zwave-js', Init); + + RED.httpAdmin.get('/zwjsgetnodelist', function (req, res) { + res.json(NodeList); + }); + + RED.httpAdmin.get('/zwjsgetversion', function (req, res) { + res.json({ + zwjsversion: ZWaveJSPackage.version, + moduleversion: ModulePackage.version + }); + }); + + RED.httpAdmin.get( + '/zwjsgetports', + RED.auth.needsPermission('serial.read'), + function (req, res) { + SP.list() + .then((ports) => { + const a = ports.map((p) => p.path); + res.json(a); + }) + .catch((err) => { + RED.log.error('Error listing serial ports', err); + res.json([]); + }); + } + ); };