UNPKG

node-red-contrib-zwave-js

Version:

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

2,098 lines (1,928 loc) 101 kB
/* eslint-env jquery */ /* eslint-env browser */ /*eslint no-undef: "warn"*/ /*eslint no-unused-vars: "warn"*/ /* UI Inclusion Functions */ let StartInclusionExclusion; let StartReplace; let GrantSelected; let ValidateDSK; /* Firmware */ let CheckForUpdate; /* UI RF Functions */ let SetPowerLevel; let SetRegion; let backupNVMRaw; let restoreNVM; let GroupedNodes = true; /* Just stuff */ let DriverReady = false; const WindowSize = { w: 700, h: 600 }; let NetworkIdentifier = undefined; /* Commands used throughout */ const DCs = { backupNVMRaw: { API: 'ControllerAPI', name: 'backupNVMRaw', noWait: true }, getRFRegion: { API: 'ControllerAPI', name: 'getRFRegion', noWait: false }, setRFRegion: { API: 'ControllerAPI', name: 'setRFRegion', noWait: false }, getPowerlevel: { API: 'ControllerAPI', name: 'getPowerlevel', noWait: false }, setPowerlevel: { API: 'ControllerAPI', name: 'setPowerlevel', noWait: false }, checkLifelineHealth: { API: 'DriverAPI', name: 'checkLifelineHealth', noWait: true }, abortFirmwareUpdate: { API: 'ControllerAPI', name: 'abortFirmwareUpdate', noWait: false }, addAssociations: { API: 'AssociationsAPI', name: 'addAssociations', noWait: false }, removeAssociations: { API: 'AssociationsAPI', name: 'removeAssociations', noWait: false }, getAssociations: { API: 'AssociationsAPI', name: 'getAssociations', noWait: false }, getAllAssociationGroups: { API: 'AssociationsAPI', name: 'getAllAssociationGroups', noWait: false }, getNodes: { API: 'ControllerAPI', name: 'getNodes', noWait: false }, verifyDSK: { API: 'IEAPI', name: 'verifyDSK', noWait: false }, grantClasses: { API: 'IEAPI', name: 'grantClasses', noWait: false }, getNodeStatistics: { API: 'DriverAPI', name: 'getNodeStatistics', noWait: false }, getControllerStatistics: { API: 'DriverAPI', name: 'getControllerStatistics', noWait: false }, checkKeyReq: { API: 'IEAPI', name: 'checkKeyReq', noWait: false }, replaceFailedNode: { API: 'IEAPI', name: 'replaceFailedNode', noWait: false }, beginExclusion: { API: 'IEAPI', name: 'beginExclusion', noWait: false }, beginInclusion: { API: 'IEAPI', name: 'beginInclusion', noWait: false }, stopIE: { API: 'IEAPI', name: 'stopIE', noWait: false }, commitScans: { API: 'IEAPI', name: 'commitScans', noWait: false }, unprovisionSmartStartNode: { API: 'IEAPI', name: 'unprovisionSmartStartNode', noWait: false }, unprovisionAllSmartStart: { API: 'IEAPI', name: 'unprovisionAllSmartStart', noWait: false }, rebuildNodeRoutes: { API: 'ControllerAPI', name: 'rebuildNodeRoutes', noWait: true }, beginRebuildingRoutes: { API: 'ControllerAPI', name: 'beginRebuildingRoutes', noWait: false }, stopRebuildingRoutes: { API: 'ControllerAPI', name: 'stopRebuildingRoutes', noWait: false }, hardReset: { API: 'ControllerAPI', name: 'hardReset', noWait: false }, refreshInfo: { API: 'ControllerAPI', name: 'refreshInfo', noWait: true }, removeFailedNode: { API: 'ControllerAPI', name: 'removeFailedNode', noWait: false }, setNodeName: { API: 'ControllerAPI', name: 'setNodeName', noWait: false }, setNodeLocation: { API: 'ControllerAPI', name: 'setNodeLocation', noWait: false }, getDefinedValueIDs: { API: 'ValueAPI', name: 'getDefinedValueIDs', noWait: false }, getValue: { API: 'ValueAPI', name: 'getValue', noWait: false }, setValue: { API: 'ValueAPI', name: 'setValue', noWait: true }, getValueMetadata: { API: 'ValueAPI', name: 'getValueMetadata', noWait: false }, getValueDB: { API: 'DriverAPI', name: 'getValueDB', noWait: false }, getAvailableFirmwareUpdates: { API: 'ControllerAPI', name: 'getAvailableFirmwareUpdates', noWait: false }, firmwareUpdateOTA: { API: 'ControllerAPI', name: 'firmwareUpdateOTA', noWait: 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, SmartStart: 10, SmartStartList: 11, SmartStartListEdit: 12, SmartStartDone: 13, RemoveDoneUnconfirmed: 14 }; const JSONFormatter = {}; JSONFormatter.json = { replacer: function (match, pIndent, pKey, pVal, pEnd) { var key = '<span class=json-key>'; var val = '<span class=json-value>'; var str = '<span class=json-string>'; var r = pIndent || ''; if (pKey) r = r + key + pKey + '</span>'; if (pVal) r = r + (pVal[0] === '"' ? str : val) + pVal + '</span>'; return r + (pEnd || ''); }, prettyPrint: function (obj) { var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/gm; return JSON.stringify(obj, null, 3) .replace(/&/g, '&amp;') .replace(/\\"/g, '&quot;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(jsonLine, JSONFormatter.json.replacer); } }; const ZwaveJsUI = (function () { let HCForm; // Health Check Form let HCRounds; // Health Check Rounds let FirmwareForm; // FrimwareForm let RFForm; // FrimwareForm let FWRunning = false; // Firmware Updare Running const Groups = {}; // Association Groups let Removing = false; // Removing Failed Node let Timer; // Timers let IETime; // IE Time let SecurityTime; // Security Time const BatteryUIElements = {}; // Battery Icon Elements let BA = undefined; // Node Button Array let HoveredNode = undefined; // Hovered Node let selectedNode; // Selected Node let LastTargetForBA; // BA TArget let WakeResolver; // Resolve for wake wait let WakeResolverTarget; // Target Wake Node let NodesListed = false; // nodes listed function modalAlert(message, title) { const Buts = { Ok: function () {} }; modalPrompt(message, title, Buts); } function modalPrompt(message, title, buttons, addCancel, IsHTML) { const Options = { draggable: false, modal: true, resizable: false, width: 'auto', title: title, minHeight: 75, buttons: {} }; Object.keys(buttons).forEach((BT) => { Options.buttons[BT] = function () { $(this).dialog('destroy'); buttons[BT](); }; }); if (addCancel) { Options.buttons['Cancel'] = function () { $(this).dialog('destroy'); }; } const D = $('<div>').css({ padding: 10, maxWidth: 500, wordWrap: 'break-word' }); if (IsHTML) { D.html(message); } else { D.text(message); } D.dialog(Options); return D; } function ShowCommandViewer() { $('<div>') .css({ maxHeight: '80%' }) .html('') .attr('id', 'CommandLog') .dialog({ draggable: true, modal: false, resizable: true, width: WindowSize.w, height: WindowSize.h, title: 'UI Monitor', buttons: { Close: function () { $(this).dialog('destroy'); }, 'Clear Log': function () { $('#CommandLog').empty(); } } }); } function processHealthCheckProgress(topic, data) { const P = Math.round((100 * data.payload) / HCRounds); HCForm.html( `<div style="width:430px; margin:auto;margin-top:40px;font-size:18px">Running Health Check. This may take a few minutes, please wait...</div><div class="progressbar"><div style="width:${P}%"></div></div>` ); } function processHealthResults(topic, data) { const RatingsArray = data.payload.HealthCheck.results.map((R) => R.rating); const Min = Math.min(...RatingsArray); const Max = Math.max(...RatingsArray); const Average = Math.round( RatingsArray.reduce((a, b) => a + b, 0) / RatingsArray.length ); const MC = Min < 4 ? 'red' : Min < 6 ? 'orange' : 'green'; const AC = Average < 4 ? 'red' : Average < 6 ? 'orange' : 'green'; const MXC = Max < 4 ? 'red' : Max < 6 ? 'orange' : 'green'; const Data = { rounds: data.payload.HealthCheck.results, Worst: Min, Best: Max, Average: Average, MC: MC, AC: AC, MXC: MXC }; Data.TX = data.payload.Statistics[HoveredNode.nodeId.toString()].commandsTX; Data.RX = data.payload.Statistics[HoveredNode.nodeId.toString()].commandsRX; Data.TXD = data.payload.Statistics[HoveredNode.nodeId.toString()].commandsDroppedTX; Data.RXD = data.payload.Statistics[HoveredNode.nodeId.toString()].commandsDroppedRX; Data.TO = data.payload.Statistics[HoveredNode.nodeId.toString()].timeoutResponse; HCForm.html(''); const Template = $('#TPL_HealthCheck').html(); const templateScript = Handlebars.compile(Template); const HTML = templateScript(Data); HCForm.append(HTML); RED.comms.unsubscribe( `/zwave-js/${NetworkIdentifier}/healthcheck`, processHealthResults ); RED.comms.unsubscribe( `/zwave-js/${NetworkIdentifier}/healthcheckprogress`, processHealthCheckProgress ); } function RenderHealthCheck(Rounds) { HCRounds = Rounds; ControllerCMD( DCs.checkLifelineHealth.API, DCs.checkLifelineHealth.name, undefined, [HoveredNode.nodeId, Rounds], DCs.checkLifelineHealth.noWait ) .catch((err) => { modalAlert( err.responseText || err.message, 'Could not start Health Check.' ); throw new Error(err.responseText || err.message); }) .then(() => { RED.comms.subscribe( `/zwave-js/${NetworkIdentifier}/healthcheck`, processHealthResults ); RED.comms.subscribe( `/zwave-js/${NetworkIdentifier}/healthcheckprogress`, processHealthCheckProgress ); const Options = { draggable: false, modal: true, resizable: false, width: WindowSize.w, height: WindowSize.h, title: 'Node Health Check : Node ' + HoveredNode.nodeId, minHeight: 75, buttons: { Cancel: function () { RED.comms.unsubscribe( `/zwave-js/${NetworkIdentifier}/healthcheck`, processHealthResults ); RED.comms.unsubscribe( `/zwave-js/${NetworkIdentifier}/healthcheckprogress`, processHealthCheckProgress ); $(this).dialog('destroy'); } } }; HCForm = $('<div>') .css({ padding: 10 }) .html( `<div style="width:430px; margin:auto;margin-top:40px;font-size:18px">Running Health Check. This may take a few minutes, please wait...</div><div class="progressbar" style="width:70%;margin: auto; margin-top:50px"><div></div></div>` ); HCForm.dialog(Options); }); } function HealthCheck() { IsDriverReady(); const Buttons = { 'Yes (1 Round)': () => { RenderHealthCheck(1); }, 'Yes (3 Rounds)': () => { RenderHealthCheck(3); }, 'Yes (5 Rounds)': () => { RenderHealthCheck(5); } }; modalPrompt( "A Node Health Check involves running diagnostics on a node and it's routing table, Care should be taken not to run this whilst large amounts of traffic is flowing through the network. Continue?", 'Run Diagnostics?', Buttons, true ); } function AbortUpdate() { if (FWRunning) { ControllerCMD( DCs.abortFirmwareUpdate.API, DCs.abortFirmwareUpdate.name, undefined, [selectedNode], DCs.abortFirmwareUpdate.noWait ) .then(() => { FirmwareForm.dialog('destroy'); }) .catch((err) => { FirmwareForm.dialog('destroy'); console.error(err); }); } else { FirmwareForm.dialog('destroy'); } FWRunning = false; } async function PerformUpdateFromService(Node, File) { const nodeRow = $('#zwave-js-node-list').find(`[data-nodeid='${Node}']`); if (nodeRow.data().info.status.toUpperCase() === 'ASLEEP') { const A = await WaitForNodeWake(Node); if (!A) { return; } } ControllerCMD( DCs.firmwareUpdateOTA.API, DCs.firmwareUpdateOTA.name, undefined, [Node, File], DCs.firmwareUpdateOTA.noWait ) .then(() => { FWRunning = true; selectNode(Node); $(":button:contains('Begin Update')") .prop('disabled', true) .addClass('ui-state-disabled'); $('#FWProgress').css({ display: 'block' }); }) .catch((err) => { modalAlert(err.responseText || err.message, 'Firmware rejected'); throw new Error(err.responseText || err.message); }); } async function PerformUpdate() { const CurrentFWMode = $('#tabs').tabs('option', 'active'); if (CurrentFWMode === 0) { const SelectedFW = $('#NODE_FWCV').find(':selected').data('FWTarget'); PerformUpdateFromService(SelectedFW.node, SelectedFW.file); return; } const FE = $('#FILE_FW')[0].files[0]; const NID = parseInt($('#NODE_FW option:selected').val()); const Target = $('#TARGET_FW').val(); const nodeRow = $('#zwave-js-node-list').find(`[data-nodeid='${NID}']`); if (nodeRow.data().info.status.toUpperCase() === 'ASLEEP') { const A = await WaitForNodeWake(NID); if (!A) { return; } } const FD = new FormData(); FD.append('Binary', FE); FD.append('NodeID', NID); FD.append('Target', Target); const Options = { url: `zwave-js/${NetworkIdentifier}/firmwareupdate`, method: 'POST', contentType: false, processData: false, data: FD }; $.ajax(Options) .then(() => { FWRunning = true; selectNode(NID); $(":button:contains('Begin Update')") .prop('disabled', true) .addClass('ui-state-disabled'); $('#FWProgress').css({ display: 'block' }); }) .catch((err) => { modalAlert(err.responseText || err.message, 'Firmware rejected'); throw new Error(err.responseText || err.message); }); } function FirmwareUpdate() { const Options = { draggable: false, modal: true, resizable: false, width: WindowSize.w, height: WindowSize.h, title: `ZWave Device Firmware Updater (Network ${NetworkIdentifier})`, minHeight: 75, buttons: { 'Begin Update': PerformUpdate, Cancel: function () { AbortUpdate(); } } }; FirmwareForm = $('<div>').css({ padding: 10 }).html('Please wait...'); FirmwareForm.dialog(Options); $.getJSON(`zwave-js/${NetworkIdentifier}/cfg-nodelist`, (data) => { FirmwareForm.html(''); const Template = $('#TPL_Firmware').html(); const templateScript = Handlebars.compile(Template); const HTML = templateScript({ nodes: data }); FirmwareForm.append(HTML); $('#FWProgress').css({ display: 'none' }); }); } function AddAssociation() { const NI = $('<input>') .attr('type', 'number') .attr('value', 1) .attr('min', 1); const EI = $('<input>') .attr('type', 'number') .attr('value', 0) .attr('min', 0); const Buttons = { Add: function () { const EP = parseInt(EI.val()); const ND = parseInt(NI.val()); const AD = { nodeId: ND }; if (EP > 0) { AD.endpoint = EP; } const TR = $('<tr>'); $('<td>') .html( `<div class="zwave-js-ac" style="display:inline-block"><i class="fa fa-plus fa-lg"></i></div> ${ND}` ) .appendTo(TR); $('<td>') .text(EP < 1 ? '0 (Root Device)' : EP) .appendTo(TR); const TD3 = $('<td>').css({ textAlign: 'right' }).appendTo(TR); $('<input>') .attr('type', 'button') .addClass('ui-button ui-corner-all ui-widget') .attr('value', 'Delete') .attr('data-address', JSON.stringify(AD)) .attr('data-committed', false) .attr('data-action', 'add') .click(DeleteAssociation) .appendTo(TD3); $('#zwave-js-associations-table').append(TR); } }; const HTML = $('<div>').append('Node ID: '); NI.appendTo(HTML); HTML.append(' Endpoint: '); EI.appendTo(HTML); modalPrompt(HTML, 'New Association', Buttons, true, true); } function DeleteAssociation() { const Button = $(this); const Buttons = { Yes: function () { const Committed = Button.attr('data-committed') === 'true' ? true : false; if (Committed) { Button.attr('data-committed', false); Button.attr('data-action', 'remove'); Button.closest('tr') .children('td:first') .find('.zwave-js-ac') .css({ display: 'inline-block' }) .html('<i class="fa fa-trash fa-lg"></i>'); Button.off('click'); } else { Button.closest('tr').remove(); } } }; modalPrompt( 'Are you sure you wish to remove this Association', 'Remove Association', Buttons, true ); } function GMEndPointSelected() { const Endpoint = $(event.target).val(); const GroupIDs = Object.keys(Groups[Endpoint]); const GroupSelector = $('#NODE_G'); GroupSelector.empty(); $('<option>Select Group...</option>').appendTo(GroupSelector); GroupIDs.forEach((GID) => { const Group = Groups[Endpoint][GID]; $(`<option value="${GID}">${GID} - ${Group.label}</option>`).appendTo( GroupSelector ); }); } function GMGroupSelected() { const Endpoint = parseInt($('#NODE_EP').val()); const Group = parseInt($('#NODE_G').val()); const AA = { nodeId: parseInt(HoveredNode.nodeId), endpoint: Endpoint }; ControllerCMD( DCs.getAssociations.API, DCs.getAssociations.name, undefined, [AA], DCs.getAssociations.noWait ) .then(({ object }) => { const Targets = object.Associations.filter((A) => A.GroupID === Group); $('#zwave-js-associations-table').find('tr:gt(0)').remove(); // shoukd only be 1 Targets.forEach((AG) => { AG.AssociationAddress.forEach((AD) => { const TR = $('<tr>'); $('<td>') .html( `<div class="zwave-js-ac"><i class="fa fa-plus fa-lg"></i></div> ${AD.nodeId}` ) .appendTo(TR); $('<td>') .html(AD.endpoint ?? '0 (Root Device)') .appendTo(TR); const TD3 = $('<td>').css({ textAlign: 'right' }).appendTo(TR); $('<input>') .attr('type', 'button') .addClass('ui-button ui-corner-all ui-widget') .attr('value', 'Delete') .attr('data-address', JSON.stringify(AD)) .attr('data-committed', true) .click(DeleteAssociation) .appendTo(TD3); $('#zwave-js-associations-table').append(TR); }); }); }) .catch((err) => { modalAlert( err.responseText || err.message, 'Could not get associations.' ); throw new Error(err.responseText || err.message); }); } async function WaitForNodeWake(NodeID) { const Buttons = { Cancel: function () { WakeResolver(false); } }; const WD = modalPrompt( 'This device is asleep, please wake it up...', 'Waiting for device to wake up', Buttons, false ); WakeResolverTarget = NodeID; const Result = await new Promise((res) => { WakeResolver = res; }); WakeResolver = undefined; WakeResolverTarget = undefined; try { WD.dialog('destroy'); } catch (Err) { // WD could already be destroyed } return Result; } function AssociationMGMT() { ControllerCMD( DCs.getAllAssociationGroups.API, DCs.getAllAssociationGroups.name, undefined, [HoveredNode.nodeId], DCs.getAllAssociationGroups.noWait ) .then(({ object }) => { const Options = { draggable: false, modal: true, resizable: false, width: WindowSize.w, height: WindowSize.h, title: `ZWave Association Management: Node ${HoveredNode.nodeId}`, minHeight: 75, buttons: { 'Commit Changes': async function () { const nodeRow = $('#zwave-js-node-list').find( `[data-nodeid='${HoveredNode.nodeId}']` ); if (nodeRow.data().info.status.toUpperCase() === 'ASLEEP') { const A = await WaitForNodeWake(HoveredNode.nodeId); if (!A) { return; } } const Removals = $('#zwave-js-associations-table').find( "input[data-committed='false'][data-action='remove']" ); const Additions = $('#zwave-js-associations-table').find( "input[data-committed='false'][data-action='add']" ); const D = modalPrompt( 'Processing association changes...', 'Please wait.', {}, false ); const DoRemovals = () => { return new Promise((resolve, reject) => { const PL = [ { nodeId: HoveredNode.nodeId, endpoint: parseInt($('#NODE_EP').val()) }, parseInt($('#NODE_G').val()), [] ]; Removals.each(function (index) { PL[2].push(JSON.parse($(this).attr('data-address'))); }); if (Removals.length < 1) { resolve(); return; } ControllerCMD( DCs.removeAssociations.API, DCs.removeAssociations.name, undefined, PL, DCs.removeAssociations.noWait ) .then(() => { resolve(); }) .catch((err) => { reject(err); }); }); }; const DoAdditions = () => { return new Promise((resolve, reject) => { const PL = [ { nodeId: HoveredNode.nodeId, endpoint: parseInt($('#NODE_EP').val()) }, parseInt($('#NODE_G').val()), [] ]; Additions.each(function (index) { PL[2].push(JSON.parse($(this).attr('data-address'))); }); if (Additions.length < 1) { resolve(); return; } ControllerCMD( DCs.addAssociations.API, DCs.addAssociations.name, undefined, PL, DCs.addAssociations.noWait ) .then(() => { resolve(); }) .catch((err) => { reject(err); }); }); }; DoRemovals() .then(() => { DoAdditions() .then(() => { D.dialog('destroy'); GMGroupSelected(); }) .catch((err) => { D.dialog('destroy'); modalAlert( err.responseText || err.message, 'Could not process association changes.' ); throw new Error(err.responseText || err.message); }); }) .catch((err) => { D.dialog('destroy'); modalAlert( err.responseText || err.message, 'Could not process association changes.' ); throw new Error(err.responseText || err.message); }); }, Close: function () { $(this).dialog('destroy'); } } }; const Form = $('<div>') .css({ padding: 60, paddingTop: 30 }) .html('Please wait...'); Form.dialog(Options); const Template = $('#TPL_Associations').html(); const templateScript = Handlebars.compile(Template); const HTML = templateScript({ endpoints: object }); Form.html(''); Form.append(HTML); $('#AMAddBTN').click(AddAssociation); $('#NODE_EP').change(GMEndPointSelected); $('#NODE_G').change(GMGroupSelected); object.forEach((EP) => { Groups[EP.Endpoint] = {}; EP.Groups.forEach((AG) => { Groups[EP.Endpoint][AG.GroupID] = { label: AG.AssociationGroupInfo.label, maxNodes: AG.AssociationGroupInfo.maxNodes }; }); }); }) .catch((err) => { modalAlert( err.responseText || err.message, 'Could not get associtions.' ); throw new Error(err.responseText || err.message); }); } async function GenerateMapJSON(Nodes) { return new Promise(function (res, rej) { ControllerCMD( DCs.getNodeStatistics.API, DCs.getNodeStatistics.name, undefined, undefined, DCs.getNodeStatistics.noWait ) .then(({ object }) => { const _Nodes = []; Nodes.forEach((N) => { const _Node = { controller: N.isControllerNode, nodeId: N.nodeId, lastSeen: N.lastSeen, name: N.name, location: N.location, powerSource: N.powerSource, statistics: object[N.nodeId.toString()] }; _Nodes.push(_Node); }); ControllerCMD( DCs.getControllerStatistics.API, DCs.getControllerStatistics.name, undefined, undefined, DCs.getControllerStatistics.noWait ) .then(({ object }) => { _Nodes.filter((_PC) => _PC.controller)[0].statistics = object; res(_Nodes); }) .catch((err) => { rej(err.responseText || err.message); }); }) .catch((err) => { rej(err.responseText || err.message); }); }); } function NetworkMap() { ControllerCMD( DCs.getNodes.API, DCs.getNodes.name, undefined, undefined, DCs.getNodes.noWait ) .then(({ object }) => { GenerateMapJSON(object) .then((Elements) => { localStorage.setItem('ZWJSMapData', JSON.stringify(Elements)); window.open('zwave-js/mesh', '_blank'); }) .catch((err) => { modalAlert(err, 'Could not generate map.'); throw new Error(err); }); }) .catch((err) => { modalAlert(err.responseText || err.message, 'Could not generate map.'); throw new Error(err.responseText || err.message); }); } let nodeOpts; function CheckDriverReady() { const Options = { url: `zwave-js/${NetworkIdentifier}/driverready`, method: 'GET' }; return $.ajax(Options); } function IsNodeReady(Node) { if (!Node.ready) { modalAlert( 'This node is not ready. Shift + click to view options', 'Node Not Ready' ); throw new Error('Node Not Ready'); } } function IsDriverReady() { if (!DriverReady) { modalAlert( 'The Controller has not yet been initialised.', 'Controller Not Ready' ); throw new Error('Driver Not Ready'); } } function ControllerCMD(mode, method, node, params, dontwait) { IsDriverReady(); const NoTimeoutFor = ['installConfigUpdate']; const Options = { url: `zwave-js/${NetworkIdentifier}/cmd`, method: 'POST', contentType: 'application/json' }; const Payload = { mode: mode, method: method }; if (node !== undefined) { Payload.node = node; } if (params !== undefined) { Payload.params = params; } if (dontwait !== undefined) { Payload.noWait = dontwait; } if (NoTimeoutFor.includes(method)) { Options.timeout = 0; Payload.noTimeout = true; } else { // Hopefully we will never have to depend on this, if so - there is something seriously wrong with the browser, that the user should resolve. // Our internal timeouts of 15s will see to anything driver/server related Options.timeout = 30000; } const RestrictedModes = ['IEAPI']; const RestrictedMethods = [ 'setPowerlevel', 'updateFirmware', 'abortFirmwareUpdate', 'setRFRegion', 'hardReset', 'backupNVMRaw' ]; if ( !RestrictedModes.includes(mode) && !RestrictedMethods.includes(method) ) { const Copy = JSON.parse(JSON.stringify({ payload: Payload })); delete Copy.payload.noTimeout; delete Copy.payload.noWait; const HTML = `${new Date().toString()}<hr /><pre class="MonitorEntry">${JSONFormatter.json.prettyPrint( Copy )}</pre><br />`; try { $('#CommandLog').append(HTML); $('#CommandLog').scrollTop($('#CommandLog')[0].scrollHeight); // eslint-disable-next-line no-empty } catch (err) {} } Options.data = JSON.stringify(Payload); return $.ajax(Options); } function AddNodeGroup(G) { const GP = $('<div>').css({ height: '30px', lineHeight: '30px', paddingLeft: '15px', backgroundColor: 'lightgray', fontWeight: 'bold' }); GP.attr('id', 'zwave-js-node-group-' + G.replace(/ /g, '-')); GP.html(G); $('#zwave-js-node-list').append(GP); return GP; } /* GetNodes is called for every node READY event, so we better limit this as we will get a flood of these during start up */ let GNTimer = undefined; function GetNodesThrottled() { if (GNTimer !== undefined) { clearTimeout(GNTimer); GNTimer = undefined; } GNTimer = setTimeout(() => { GetNodes(); }, 250); } function GetNodes() { BA = undefined; deselectCurrentNode(); ControllerCMD( DCs.getNodes.API, DCs.getNodes.name, undefined, undefined, DCs.getNodes.noWait ) .then(({ object }) => { const controllerNode = object.filter((N) => N.isControllerNode); if (controllerNode.length > 0) { makeInfo( '#zwave-js-controller-info', controllerNode[0].deviceConfig, controllerNode[0].firmwareVersion ); } $('#zwave-js-node-list').empty(); const Nodes = object.filter((node) => node && !node.isControllerNode); if (GroupedNodes) { let Groups = {}; Nodes.forEach((N) => { if (N.location === undefined || N.location.length < 1) { if (!Groups.hasOwnProperty('No Location')) { Groups['No Location'] = []; } Groups['No Location'].push(renderNode(N)); } else { if (!Groups.hasOwnProperty(N.location)) { Groups[N.location] = []; } Groups[N.location].push(renderNode(N)); } }); Groups = sortByKey(Groups); Object.keys(Groups).forEach((G) => { AddNodeGroup(G); Groups[G].forEach((NE) => { $('#zwave-js-node-list').append(NE); }); }); } else { Nodes.forEach((N) => $('#zwave-js-node-list').append(renderNode(N))); } NodesListed = true; $('#zwave-js-node-properties').treeList('empty'); }) .catch((err) => { modalAlert(err.responseText || err.message, 'Could not fetch nodes.'); throw new Error(err.responseText || err.message); }); } function EnableCritical(Value) { if (Value) { $('.CriticalDisable').prop('disabled', false); $('.CriticalDisable').css({ opacity: '1.0' }); } else { $('.CriticalDisable').prop('disabled', true); $('.CriticalDisable').css({ opacity: '0.4' }); } } restoreNVM = () => { $('#FILE_BU').on('change', () => { const FE = $('#FILE_BU')[0].files[0]; const FD = new FormData(); FD.append('Binary', FE); const Options = { url: `zwave-js/${NetworkIdentifier}/restorenvm`, method: 'POST', contentType: false, processData: false, data: FD }; $.ajax(Options) .then(() => { EnableCritical(false); $('#NVMProgressLabel').html('Starting Restore...'); $('#NVMProgress').css({ display: 'block' }); }) .catch((err) => { modalAlert(err.responseText || err.message, 'Could not restore NVM.'); EnableCritical(true); throw new Error(err.responseText || err.message); }); $('#FILE_BU').off('change'); }); $('#FILE_BU').click(); }; backupNVMRaw = () => { EnableCritical(false); ControllerCMD( DCs.backupNVMRaw.API, DCs.backupNVMRaw.name, undefined, undefined, DCs.backupNVMRaw.noWait ) .catch((err) => { modalAlert(err.responseText || err.message, 'Could not back NVM.'); EnableCritical(true); throw new Error(err.responseText || err.message); }) .then(() => { $('#NVMProgressLabel').html('Backing up NVM...'); $('#NVMProgress').css({ display: 'block' }); }); }; SetRegion = () => { EnableCritical(false); ControllerCMD( DCs.setRFRegion.API, DCs.setRFRegion.name, undefined, [parseInt($('#RF_REGION').val())], DCs.setRFRegion.noWait ) .catch((err) => { modalAlert(err.responseText || err.message, 'Could not set RF Region.'); EnableCritical(true); throw new Error(err.responseText || err.message); }) .then(({ object }) => { EnableCritical(true); if (!object.success) { modalAlert( 'The controller did not accept the values provided.', 'Could not set RF Region.' ); } else { modalAlert('Settings were applied successfully.', 'RF Region set.'); } }); }; SetPowerLevel = () => { EnableCritical(false); ControllerCMD( DCs.setPowerlevel.API, DCs.setPowerlevel.name, undefined, [ parseFloat($('#RF_POWER').slider('value')), parseFloat($('#RF_0DBM').slider('value')) ], DCs.setPowerlevel.noWait ) .catch((err) => { modalAlert( err.responseText || err.message, 'Could not set power level.' ); EnableCritical(true); throw new Error(err.responseText || err.message); }) .then(({ object }) => { EnableCritical(true); if (!object.success) { modalAlert( 'The controller did not accept the values provided.', 'Could not set power level.' ); } else { modalAlert('Settings were applied successfully.', 'Power level set.'); } }); }; function sortByKey(obj) { const keys = Object.keys(obj); keys.sort(); const sorted = {}; for (let i = 0; i < keys.length; i++) { const key = keys[i]; sorted[key] = obj[key]; } return sorted; } function ListRequestedClass(Classes) { Classes.forEach((SC) => { $('tr#TR_' + SC).css({ opacity: 1.0 }); $('input#SC_' + SC).prop('disabled', false); }); StepsAPI.setStepIndex(StepList.Classes); } function DisplayDSK(DSK) { $('#DSK_Previw').html(DSK); StepsAPI.setStepIndex(StepList.DSK); } ValidateDSK = () => { const B = event.target; ControllerCMD( DCs.verifyDSK.API, DCs.verifyDSK.name, undefined, [$('#SC_DSK').val()], DCs.verifyDSK.noWait ) .catch((err) => { modalAlert(err.responseText || err.message, 'Could not verify DSK.'); throw new Error(err.responseText || err.message); }) .then(() => { $(B).html('Please wait...'); ClearIETimer(); ClearSecurityCountDown(); $(B).prop('disabled', true); }); }; GrantSelected = () => { const B = event.target; const Granted = []; $('.SecurityClassCB').each(function () { if ($(this).is(':checked')) { Granted.push(parseInt($(this).attr('id').replace('SC_', ''))); } }); ControllerCMD( DCs.grantClasses.API, DCs.grantClasses.name, undefined, [Granted], DCs.grantClasses.noWait ) .catch((err) => { modalAlert( err.responseText || err.message, 'Could not grant Security Classes.' ); throw new Error(err.responseText || err.message); }) .then(() => { $(B).html('Please wait...'); ClearIETimer(); ClearSecurityCountDown(); $(B).prop('disabled', true); }); }; StartReplace = (Mode) => { const B = event.target; const OT = $(B).html(); $(B).html('Please wait...'); $(B).prop('disabled', true); const Request = {}; switch (Mode) { case 'S2': Request.strategy = 4; break; case 'S0': Request.strategy = 3; break; case 'None': Request.strategy = 2; break; } ControllerCMD( DCs.checkKeyReq.API, DCs.checkKeyReq.name, undefined, [Request.strategy], DCs.checkKeyReq.noWait ) .then(({ object }) => { if (object.ok) { ControllerCMD( DCs.replaceFailedNode.API, DCs.replaceFailedNode.name, undefined, [parseInt(HoveredNode.nodeId), Request], DCs.replaceFailedNode.noWait ).catch((err) => { modalAlert( err.responseText || err.message, 'Could not replace Node.' ); $(B).html(OT); $(B).prop('disabled', false); throw new Error(err.responseText || err.message); }); } else { modalAlert(object.message, 'Could not replace Node.'); $(B).html(OT); $(B).prop('disabled', false); } }) .catch((err) => { $(B).html(OT); $(B).prop('disabled', false); modalAlert(err.responseText || err.message, 'Could not replace Node.'); throw new Error(err.responseText || err.message); }); }; StartInclusionExclusion = (Mode) => { const B = event.target; const OT = $(B).html(); $(B).html('Please wait...'); $(B).prop('disabled', true); const Request = {}; const PreferS0 = $('#PS0').is(':checked'); switch (Mode) { case 'Default': Request.strategy = 0; Request.forceSecurity = PreferS0; break; case 'EditSmartStart': StepsAPI.setStepIndex(StepList.SmartStartListEdit); $('#SSPurgeButton').css({ display: 'inline-block' }); $.ajax({ url: `zwave-js/${NetworkIdentifier}/smart-start-list`, method: 'GET', dataType: 'json', error: function (err) { modalAlert( err.responseText || err.message, 'Could not fetch Smart Start list.' ); throw new Error(err.responseText || err.message); }, success: function (List) { List.forEach((Entry) => { const Item = $('<tr class="SmartStartEntry">'); Item.append(`<td>${Entry.dsk.substring(0, 5)}</td>`); if (Entry.manufacturer === undefined) { Item.append(`<td>Unknown Manufacturer</td>`); Item.append(`<td>Unknown Product</td>`); } else { Item.append(`<td>${Entry.manufacturer}</td>`); Item.append( `<td>${Entry.label}<br /><span style="font-size:12px">${Entry.description}</span></td>` ); } const BTNTD = $('<td>'); BTNTD.css('text-align', 'right'); const BTN = $('<button>'); BTN.addClass('ui-button ui-corner-all ui-widget'); BTN.html('Remove'); BTN.click(() => { const Buttons = { Yes: function () { ControllerCMD( DCs.unprovisionSmartStartNode.API, DCs.unprovisionSmartStartNode.name, undefined, [Entry.dsk], DCs.unprovisionSmartStartNode.noWait ) .then(() => { Item.remove(); }) .catch((err) => { modalAlert( err.responseText || err.message, 'Could not remove Smart Start entry.' ); throw new Error(err.responseText || err.message); }); } }; modalPrompt( 'Are you sure you wish to remove this entry?', 'Remove Smart Start Entry', Buttons, true ); }); BTN.appendTo(BTNTD); Item.append(BTNTD); $('#SmartStartEditList').append(Item); }); } }); return; case 'SmartStart': ControllerCMD( DCs.checkKeyReq.API, DCs.checkKeyReq.name, undefined, [1], DCs.checkKeyReq.noWait ) .catch((err) => { $(B).html(OT); $(B).prop('disabled', false); modalAlert( err.responseText || err.message, 'Could not start Inclusion' ); throw new Error(err.responseText || err.message); }) .then(({ object }) => { if (object.ok) { $('#SmartStartCommit').css({ display: 'inline' }); $.ajax({ url: `zwave-js/${NetworkIdentifier}/smartstart/startserver`, method: 'GET', error: function (err) { modalAlert( err.responseText || err.message, 'Could not start Inclusion' ); throw new Error(err.responseText || err.message); }, success: function (QRData) { StepsAPI.setStepIndex(StepList.SmartStart); new QRCode($('#SmartStartQR')[0], { text: QRData, width: 150, height: 150, colorDark: '#000000', colorLight: '#ffffff', correctLevel: QRCode.CorrectLevel.L }); $('#SmartStartURL').html(QRData); } }); } else { $(B).html(OT); $(B).prop('disabled', false); modalAlert(object.message, 'Could not start Inclusion'); } }); return; case 'None': Request.strategy = 2; break; case 'S0': Request.strategy = 3; break; case 'S2': Request.strategy = 4; break; case 'Remove': ControllerCMD( DCs.beginExclusion.API, DCs.beginExclusion.name, undefined, [$('#ERP').is(':checked')], DCs.beginExclusion.noWait ).catch((err) => { $(B).html(OT); $(B).prop('disabled', false); modalAlert( err.responseText || err.message, 'Could not start Exclusion' ); throw new Error(err.responseText || err.message); }); return; } ControllerCMD( DCs.checkKeyReq.API, DCs.checkKeyReq.name, undefined, [Request.strategy], DCs.checkKeyReq.noWait ) .then(({ object }) => { if (object.ok) { ControllerCMD( DCs.beginInclusion.API, DCs.beginInclusion.name, undefined, [Request], DCs.beginInclusion.noWait ).catch((err) => { $(B).html(OT); $(B).prop('disabled', false); modalAlert( err.responseText || err.message, 'Could not start Inclusion' ); throw new Error(err.responseText || err.message); }); } else { $(B).html(OT); $(B).prop('disabled', false); modalAlert(object.message, 'Could not start Inclusion'); } }) .catch((err) => { $(B).html(OT); $(B).prop('disabled', false); modalAlert( err.responseText || err.message, 'Could not start Inclusion' ); throw new Error(err.responseText || err.message); }); }; function ShowIncludeExcludePrompt() { const ParentDialog = $('<div>').css({ padding: 10 }).html('Please wait...'); const Options = { draggable: false, modal: true, resizable: false, width: WindowSize.w, height: WindowSize.h, title: `Node Inclusion/Exclusion (Network ${NetworkIdentifier})`, minHeight: 75, buttons: [ { id: 'SSPurgeButton', text: 'Remove All', click: function () { const Buttons = { 'Yes - Remove': function () { ControllerCMD( DCs.unprovisionAllSmartStart.API, DCs.unprovisionAllSmartStart.name, undefined, undefined, DCs.unprovisionAllSmartStart.noWait ) .catch((err) => { ParentDialog.dialog('destroy'); modalAlert( err.responseText || err.message, 'Could not purge Smart Start entries' ); throw new Error(err.responseText || err.message); }) .then(() => { ParentDialog.dialog('destroy'); }); } }; modalPrompt( 'Are you sure you wish to remove all pre-provisioned device entries (the devices them self wont be removed)', 'Purge Provisioning List', Buttons, true ); } }, { id: 'IEButton', text: 'Cancel', click: function () { $.ajax({ url: `zwave-js/${NetworkIdentifier}/smartstart/stopserver`, method: 'GET' }).catch((err) => { console.error(err); }); ClearIETimer(); ClearSecurityCountDown(); ControllerCMD( DCs.stopIE.API, DCs.stopIE.name, undefined, undefined, DCs.stopIE.noWait ).catch((err) => { console.error(err); }); ParentDialog.dialog('destroy'); } }, { id: 'SmartStartCommit', text: 'Commit Scans', click: function () { const SSEntries = $('.SmartStartEntry'); const Entries = []; SSEntries.each(function (i, e) { Entries.push($(e).data('inclusionPackage')); }); ControllerCMD( DCs.commitScans.API, DCs.commitScans.name, undefined, Entries, DCs.commitScans.noWait ) .then(() => { StepsAPI.setStepIndex(StepList.SmartStartDone); $('#SmartStartCommit').css({ display: 'none' }); $('#IEButton').css({ display: 'none' }); $('#IEClose').css({ display: 'inline-block' }); $.ajax({ url: `zwave-js/${NetworkIdentifier}/smartstart/stopserver`, method: 'GET' }).catch((err) => { console.error(err); }); }) .catch((err) => { $.ajax({ url: `zwave-js/${NetworkIdentifier}/smartstart/stopserver`, method: 'GET' }).catch((err) => { console.error(err); }); modalAlert( err.responseText || err.message, 'Could not commit Smart Start entries.' ); throw new Error(err.responseText || err.message); }); } }, { id: 'IEClose', text: 'Ok', click: function () { ParentDialog.dialog('destroy'); } } ] }; ParentDialog.dialog(Options); ParentDialog.html(''); ParentDialog.append($('#TPL_Include').html()); const Steps = $('#IncludeWizard').steps({ showFooterButtons: false }); StepsAPI = Steps.data('plugin_Steps'); $('#SmartStartCommit').css({ display: 'none' }); $('#IEClose').css({ display: 'none' }); $('#SSPurgeButton').css({ display: 'none' }); } function ShowReplacePrompt() { const Options = { draggable: false, modal: true, resizable: false, width: WindowSize.w, height: WindowSize.h, title: 'Replace Node', minHeight: 75, buttons: [ { id: 'IEButton', text: 'Cancel', click: function () { ControllerCMD( DCs.stopIE.API, DCs.stopIE.name, undefined, undefined, DCs.stopIE.noWait ).catch((err) => { console.error(err); }); $(this).dialog('destroy'); } } ] }; const IncludeForm = $('<div>').css({ padding: 10 }).html('Please wait...'); IncludeForm.dialog(Options); IncludeForm.html(''); IncludeForm.append($('#TPL_Include').html()); const Steps = $('#IncludeWizard').steps({ showFooterButtons: false }); StepsAPI = Steps.data('plugin_Steps'); StepsAPI.setStepIndex(StepList.ReplaceSecurityMode); } function StartNodeHeal() { ControllerCMD( DCs.rebuildNodeRoutes.API, DCs.rebuildNodeRoutes.name, undefined, [HoveredNode.nodeId], DCs.rebuildNodeRoutes.noWait ).catch((err) => { modalAlert(err.responseText || err.message, 'Could not start Node heal.'); throw new Error(err.responseText || err.message); }); } function StartHeal() { ControllerCMD( DCs.beginRebuildingRoutes.API, DCs.beginRebuildingRoutes.name, undefined, undefined, DCs.beginRebuildingRoutes.noWait ).catch((err) => { modalAlert( err.responseText || err.message, 'Could not start Network heal.' ); throw new Error(err.responseText || err.message); }); } function StopHeal() { ControllerCMD( DCs.stopRebuildingRoutes.API, DCs.stopRebuildingRoutes.name, undefined, undefined, DCs.stopRebuildingRoutes.noWait ).catch((err) => { console.error(err); }); } function Reset() { const Buttons = { 'Yes - Reset': function () { ControllerCMD( DCs.hardReset.API, DCs.hardReset.name, undefined, undefined, DCs.hardReset.noWait ) .then(() => { modalAlert('Your Controller has been reset.', 'Reset Complete'); GetNodes(); }) .catch((err) => { modalAlert( err.responseText || err.message, 'Could not reset the Controller.' ); throw new Error(err.responseText || err.message); }); } }; modalPrompt( 'Are you sure you wish to reset your Controller? This action is irreversible, and will clear the Controllers data and configuration.', 'Reset Controller', Buttons, true ); } function InterviewNode() { ControllerCMD( DCs.refreshInfo.API, DCs.refreshInfo.name, undefined, [HoveredNode.nodeId], DCs.refreshInfo.noWait ).catch((err) => { modalAlert( err.responseText || err.message, 'Could not interview the Node.' ); throw new Error(err.responseText || err.message); }); } function OpenDB() { const info = HoveredNode.deviceConfig; const id = [ '0x' + info.manufacturerId.toString(16).padStart(4, '0'), '0x' + info.devices[0].productType.toString(16).padStart(4, '0'), '0x' + info.devices[0].productId.toString(16).padStart(4, '0'), info.firmwareVersion.min ].join(':'); window.open(`https://devices.zwave-js.io/?jumpTo=${id}`, '_blank'); } function RFSettings() { const Options = { draggable: false, modal: true, resizable: false, width: WindowSize.w, height: WindowSize.h, title: `Advanced Transceiver Settings (Network ${NetworkIdentifier})`, minHeight: 75, buttons: { Close: function () { $(this).dialog('destroy'); } } }; RFForm = $('<div>').css({ padding: 10 }).html('Please wait...'); RFForm.dialog(Options); RFForm.html(''); const Template = $('#TPL_RF').html(); const templateScript = Handlebars.compile(Template); const HTML = templateScript({}); RFForm.append(HTML); $('#NVMProgress').css({ display: 'none' }); const PowerSlider = $('#RF_POWER_SLIDER'); $('#RF_POWER').slider({ min: -12.8, max: 12.7, step: 0.1, range: 'min', slide: function (event, ui) { PowerSlider.text(ui.value); } }); const MeasuredSlider = $('#RF_0DBM_SLIDER'); $('#RF_0DBM').slider({ min: -12.8, max: 12.7, step: 0.1, range: 'min', slide: function (event, ui) { MeasuredSlider.text(ui.value); } }); const GetPower = () => { ControllerCMD( DCs.getPowerlevel.API, DCs.getPowerlevel.name, undefined, undefined, DCs.getPowerlevel.noWait ) .then(({ object }) => { PowerSlider.text(object.powerlevel); $('#RF_POWER').slider('value', object.powerlevel); MeasuredSlider.text(object.measured0dBm); $('#RF_0DBM').slider('value', object.measured0dBm); }) .catch((err) => { $('#RF_TR_POWER').css({ opacity: '0.3', pointerEvents: 'none' }); console.error(err); }); }; ControllerCMD( DCs.getRFRegion.API, DCs.getRFRegion.name, undefined, undefined, DCs.getRFRegion.noWait ) .then(({ object }) => { $('#RF_REGION').val(object); GetPower(); }) .catch((err) => { $('#RF_TR_REGION').css({ opacity: '0.3', pointerEvents: 'none' }); console.error(err); GetPower(); }); } function RemoveFailedNode() { if (Removing) { modalAlert( 'A node is already being removed, please allow a minute or 2.', 'Could Not Remove Node' ); return; } const Buttons = { 'Yes - Remove': function () { const D = modalPrompt( 'Checking if Node has failed...', 'Please wait.', {}, false ); Removing = true; ControllerCMD( DCs.removeFailedNode.API, DCs.removeFailedNode.name, undefined, [H