UNPKG

node-red-contrib-zwave-js

Version:

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

1,755 lines (1,572 loc) 83.3 kB
/* eslint no-undef: "warn"*/ // eslint-disable-next-line no-unused-vars const ZWaveJS = (function () { /* * Just Stuff * Yeah... just stuff - pretty darn important as well!! */ const AdvancedPanels = []; const SetValueOptionExamples = { transitionDuration: '30s, 1m, 1m10s', volume: 45 }; const GroupMode = [true, true]; // Grouped, Expanded let networkId = undefined; let selectedNode = undefined; let QRS; let TPL_SidePanel = undefined; let TPL_ControllerManagement = undefined; let TPL_ControllerManagementRecover = undefined; let TPL_NodeManagement = undefined; let TPL_ValueManagement = undefined; let AssociationGroups; let clientSideAuth = false; let SelectedNodeVIDs = {}; let MiniEdtiorDialog; let ViewingValueID = undefined; let BootLoaderMode = false; let CodeEditor = undefined; let Panels = undefined; let isCurrentTray = false; /* * Driver Communciation Methods * These methods are used to send messages to the API's of the module itself * Runtime.Get, Runtime.Post - sends messages to the runtime of the Driver/Module */ const Runtime = { Get: async function (API, Method, URL) { return new Promise((resolve, reject) => { $.ajax({ type: 'GET', timeout: 0, url: URL || `zwave-js/ui/${networkId}/${API}/${Method}`, success: (data) => resolve(data), error: (jqXHR, textStatus, errorThrown) => reject(new Error(`${textStatus}: ${errorThrown}`)) /* Transport error */, dataType: 'json' }); }); }, Post: async function (API, Method, Data, URL) { return new Promise((resolve, reject) => { $.ajax({ type: 'POST', timeout: 0, data: JSON.stringify(Data), url: URL || `zwave-js/ui/${networkId}/${API}/${Method}`, success: (data) => resolve(data), error: (jqXHR, textStatus, errorThrown) => reject(new Error(`${textStatus}: ${errorThrown}`)) /* Transport error */, dataType: 'json', contentType: 'application/json' }); }); } }; /* * Public UI Methods * These methods are seen by the editor (UA) - and need to be. */ // Init UI const init = () => { $.get('resources/node-red-contrib-zwave-js/UITab/ValueEditors.html', function (html) { Handlebars.registerPartial('ValueEditors', html); }); Handlebars.registerHelper('json', function (object) { return JSON.stringify(object, undefined, 2); }); Handlebars.registerHelper('encode', function (object) { const json = JSON.stringify(object); const encoded = btoa(String.fromCharCode(...new TextEncoder().encode(json))); return encoded; }); Handlebars.registerHelper('eq', function (actual, expected, options) { if (actual === expected) { return options.fn(this); } return options.inverse(this); }); Handlebars.registerHelper('select', function (context, options) { const $el = $('<select />').html(options.fn(this)); $el.find(`[value="${context}"]`).attr({ selected: 'selected' }); return $el.html(); }); Handlebars.registerHelper('pretty', function (context) { return JSONFormatter.json.prettyPrint(context); }); Handlebars.registerHelper('editor', function (id, context) { setTimeout(() => { CodeEditor = RED.editor.createEditor({ id: id, mode: 'ace/mode/json', value: context.fn(this) }); }, 175); return new Handlebars.SafeString( `<div style="height: 250px; min-height:150px;" class="node-text-editor" id="${id}"></div>` ); }); // Templates TPL_SidePanel = Handlebars.compile($('#ZWJS_TPL_SidePanel').html()); TPL_ControllerManagement = Handlebars.compile($('#ZWJS_TPL_Tray-Controller').html()); TPL_ControllerManagementRecover = Handlebars.compile($('#ZWJS_TPL_Tray-Controller-Recover').html()); TPL_NodeManagement = Handlebars.compile($('#ZWJS_TPL_Tray-Node').html()); TPL_ValueManagement = Handlebars.compile($('#ZWJS_TPL_Tray-Node-Value').html()); // Add tab RED.sidebar.addTab({ id: 'zwave-js', label: ' ZWave JS', name: 'Z-Wave JS', content: TPL_SidePanel({}), enableOnEdit: true, iconClass: 'fa fa-wifi', onchange: () => setTimeout(resizeStack, 0) }); $('#zwjs-node-list').treeList({ data: [] }); $('#zwjs-node-list').on('treelistselect', nodeSelected); $('#zwjs-cc-list').treeList({ data: [] }); Panels = RED.panels.create({ container: $('#zwjs-panel-stack') }); Panels.ratio(0.3); const resizeStack = () => Panels.resize($('#zwjs-sidebar').height()); RED.events.on('sidebar:resize', resizeStack); $(window).on('resize', resizeStack); $(window).on('focus', resizeStack); commsListOrAddNetworks(true); RED.comms.subscribe('zwave-js/ui/global/addnetwork', (topic, network) => commsListOrAddNetworks(false, network)); RED.comms.subscribe('zwave-js/ui/global/removenetwork', (topic, network) => commsRemoveNetwork(network)); }; // Backup Names const BackupNames = (Button) => { DisableButton(Button); Runtime.Get('CONTROLLER', 'getNodes').then((Data) => { if (Data.callSuccess) { const CD = $('#zwjs-controller-info').data('info'); const FileName = `zwave_names_locations_${CD.homeId}.json`; const Map = []; Data.response.forEach((N) => { if (N.nodeName !== undefined || N.nodeLocation !== undefined) { Map.push({ nodeId: N.nodeId, name: N.nodeName, location: N.nodeLocation }); } }); const Output = JSON.stringify(Map, null, 2); const blob = new Blob([Output], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = FileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } else { alert(Data.response); } EnableButton(Button); }); }; const RestoreNames = (Button) => { DisableButton(Button); const input = document.createElement('input'); input.type = 'file'; const OnLoad = async (e) => { try { const Nodes = JSON.parse(e.target.result); for (let i = 0; i < Nodes.length; i++) { const Node = Nodes[i]; await Runtime.Post('NODE', 'setLocation', { nodeId: Node.nodeId, value: Node.location || undefined }); await Runtime.Post('NODE', 'setName', { nodeId: Node.nodeId, value: Node.name || undefined }); } EnableButton(Button); RefreshNodes('Named'); alert('Restore Completed Successfully'); } catch (err) { alert(err.message); } }; const OnChange = (e) => { const reader = new FileReader(); reader.onload = OnLoad; reader.readAsText(e.target.files[0]); }; input.onchange = OnChange; input.click(); }; // Zoom const ZoomUI = (value) => { const sidebar = $('#zwjs-sidebar'); if (value === undefined) { sidebar.css('zoom', '1.0'); Panels.resize($('#zwjs-sidebar').height()); return; } let current = parseFloat(sidebar.css('zoom')); if (isNaN(current)) current = 1; let newZoom = current + value; newZoom = Math.min(Math.max(newZoom, 0.1), 2.0); newZoom = parseFloat(newZoom.toFixed(2)); sidebar.css('zoom', newZoom); Panels.resize($('#zwjs-sidebar').height()); }; // Install Config Update const CFGUpdate = () => { Runtime.Post('DRIVER', 'checkForConfigUpdates').then((data) => { if (data.callSuccess) { if (data.response !== undefined) { const UD = confirm( `A configuration database update is available (${data.response}). Would you like to update?` ); if (UD) { Runtime.Post('DRIVER', 'installConfigUpdate').then((res) => { if (res.callSuccess && res.response) { alert('Update was installed.'); } else { alert(`Update was not installed: ${res.response}.`); } }); } } else { alert('No update available.'); } } else { alert(data.response); } }); }; // Save Spliter const UpdateSplitter = () => { const Node = RED.nodes.node($('#zwjs-splitters').val()); const NextIndex = Node.splits.length ? Math.max(...Node.splits.map((x) => x.index)) + 1 : 0; const entry = { valueId: JSON.parse(CodeEditor.getValue()), index: NextIndex, name: $('#zwjs-splitter-output-name').val() }; if (entry.valueId.commandClass === undefined) { entry.custom = true; } Node.splits.push(entry); Node.outputs++; Node.dirty = true; Node.changed = true; Node.resize = true; RED.view.redraw(true); RED.nodes.dirty(true); CloseTray(); }; // Rebuid Routes (Nodes) const RebuildNodeRoutes = () => { Runtime.Post('CONTROLLER', 'rebuildNodeRoutes', [selectedNode.nodeId]).then((data) => { if (data.callSuccess) { alert('Rebuiliding Node routes completed successfully.'); } else { alert(data.response); } }); }; // Rebuid Routes const RebuildRoutes = (button, battery) => { button && DisableButton(button); const Battery = battery || $('#zwjs-routes-battery').prop('checked'); Runtime.Post('CONTROLLER', 'beginRebuildingRoutes', [{ includeSleeping: Battery }]).then((data) => { if (data.callSuccess) { button && EnableButton(button); } else { alert(data.response); button && EnableButton(button); } }); }; // Update Value const UpdateValue = (Button, VID, Defined) => { DisableButton(Button); VID = DecodeObject(VID); let Value; if (CodeEditor) { Value = JSON.parse(CodeEditor.getValue()); } else if (Defined) { Value = parseInt($('#zwjs-cc-value-new-defined').val()); $('#zwjs-cc-value-new').val(Value); } else { const el = $('#zwjs-cc-value-new'); if (el.is('input')) { switch (el.attr('type')) { case 'number': Value = parseInt(el.val()); break; case 'checkbox': Value = el.prop('checked'); break; case 'color': Value = el.val().substring(1); break; } } if (el.is('select')) { Value = parseInt(el.val()); } } Runtime.Post('VALUE', 'setValue', { nodeId: selectedNode.nodeId, valueId: VID, value: Value }).then((data) => { if (data.callSuccess) { switch (data.response.status) { case 0: alert('The Node does not support the command'); break; case 1: alert('The Node is working on the requested change'); break; case 2: alert('The Node rejected the change'); break; case 3: alert('The target Endpoint was not found on the Node'); break; case 4: alert('The set command has not been implemented for this CC'); break; case 5: alert('The provided value was not valid'); break; default: alert('The update was successfull'); if (MiniEdtiorDialog) { MiniEdtiorDialog.dialog('destroy'); MiniEdtiorDialog = undefined; } break; } EnableButton(Button); } else { alert(data.response); EnableButton(Button); } }); }; // Collapse LIst const NodeCollapseToggle = (Mode, A) => { $('i.zwjs-button-group').removeAttr('selected'); $(A).find('i.zwjs-button-group').attr('selected', ''); switch (Mode) { case 1: GroupMode[0] = true; GroupMode[1] = true; RefreshNodes('Sorted'); break; case 2: GroupMode[0] = true; GroupMode[1] = false; RefreshNodes('Sorted'); break; case 3: GroupMode[0] = false; GroupMode[1] = true; RefreshNodes('Sorted'); break; } }; // Remove Failed const RemoveFailedNode = (NodeID, Row) => { const ID = NodeID || selectedNode?.nodeId; if (ID) { if (confirm('Are you sure you wish to remove this Node from your network?')) { Runtime.Post('CONTROLLER', 'removeFailedNode', [ID]).then((data) => { if (data.callSuccess) { if (Row) { $(Row).closest('tr').remove(); } } else { alert(data.response); } }); } } }; // Ping Node const PingNode = (NodeID) => { Runtime.Post('NODE', 'ping', { nodeId: NodeID }).then((data) => { if (data.callSuccess) { data.response ? alert('Ping was successful') : alert('Ping failed'); } else { alert(data.response); } }); }; // Set Class PowerLevel const SetClassicPowerLevel = (Button) => { DisableButton(Button); const PL = parseInt($('#zwjs-controller-setting-power-classic').val()); const Calibration = 0; Runtime.Post('CONTROLLER', 'setPowerlevel', [PL, Calibration]) .then((data) => { if (data.callSuccess) { alert('Power Level Set Succcessfully'); EnableButton(Button); } else { alert(data.response); EnableButton(Button); } }) .catch((Error) => { alert(Error.message); }); }; // Set LR PowerLevel const SetLWPowerLevel = (Button) => { DisableButton(Button); const PL = parseInt($('#zwjs-controller-setting-power-lr').val()); Runtime.Post('CONTROLLER', 'setMaxLongRangePowerlevel', [PL]) .then((data) => { if (data.callSuccess) { alert('Power Level Set Succcessfully'); EnableButton(Button); } else { alert(data.response); EnableButton(Button); } }) .catch((Error) => { alert(Error.message); }); }; // Set Region const SetRegion = (Button) => { DisableButton(Button); const R = parseInt($('#zwjs-controller-setting-region option:selected').val()); Runtime.Post('CONTROLLER', 'setRFRegion', [R]) .then((data) => { if (data.callSuccess) { alert('Region Set Succcessfully'); EnableButton(Button); } else { alert(data.response); EnableButton(Button); } }) .catch((Error) => { alert(Error.message); }); }; // Network Selecetd const NetworkSelected = function () { if (networkId) { // unsubscribe setSubscription(false); } if ($('#zwjs-network').val() === 'NONE') { ClearSelection(true); return; } ClearSelection(true); $('#zwjs-controller-info').text('--'); $('#zwjs-controller-status').text('Waiting for Network Status Report...'); networkId = $('#zwjs-network').val(); setSubscription(true); const pollStatus = () => { Runtime.Get(undefined, undefined, `zwave-js/ui/${networkId}/status`) .then((data) => { if (data.response === undefined) { setTimeout(pollStatus, 500); return; } $('#zwjs-controller-status').text(data.response); if (data.response === 'Bootloader ready.') { handleBootloader(); } else { BootLoaderMode = false; RefreshNodes('NetworkSelected'); } }) .catch((error) => { alert(error.message); }); }; pollStatus(); }; // Show Recovery const ShowRecovery = () => { const Options = { width: 900, title: 'ZWave JS Controller Management (Recovery)', buttons: [ { id: 'zwjs-tray-close', text: 'Close', click: function () { CloseTray(); } } ], open: function (tray) { isCurrentTray = true; const trayBody = tray.find('.red-ui-tray-body, .editor-tray-body'); const State = { Network: $('#zwjs-controller-info').text(), Status: $('#zwjs-controller-status').text() }; trayBody.append(TPL_ControllerManagementRecover(State)); } }; RED.tray.show(Options); setTimeout(() => { const el = $('.zwjs-tray-menu > div[default]')[0]; el.onclick.call(el); }, 250); }; // Show Network Options const ShowNetworkManagement = () => { if (!networkId) { return; } CloseTray(); if (BootLoaderMode) { ShowRecovery(); return; } Runtime.Get('CONTROLLER', 'getNodes') .then((data) => { if (data.callSuccess) { const RCD = data.response.find((N) => N.isControllerNode); if (RCD.statistics.backgroundRSSI) { RCD.backgroundRSSI = FlattenChannelAverages(RCD.statistics.backgroundRSSI); } else { RCD.backgroundRSSI = {}; } delete RCD.statistics.backgroundRSSI; $('#zwjs-controller-info').data('info', RCD); const Options = { width: 900, title: 'ZWave JS Controller Management', buttons: [ { id: 'zwjs-tray-close', text: 'Close', click: function () { CloseTray(); } } ], open: function (tray) { isCurrentTray = true; const trayBody = tray.find('.red-ui-tray-body, .editor-tray-body'); const State = { Network: $('#zwjs-controller-info').text(), Status: $('#zwjs-controller-status').text() }; trayBody.append(TPL_ControllerManagement(State)); } }; RED.tray.show(Options); setTimeout(() => { const el = $('.zwjs-tray-menu > div[default]')[0]; el.onclick.call(el); }, 250); } else { alert(data.response); } }) .catch((Error) => { alert(Error.message); }); }; // Show Node Options const ShowNodeManagement = () => { if (!selectedNode) { return; } CloseTray(); Runtime.Get('CONTROLLER', 'getNodes') .then((data) => { if (data.callSuccess) { const RND = data.response.find((N) => N.nodeId === selectedNode.nodeId); delete RND.statistics.lwr; GetNodeGroup(selectedNode.nodeLocation).children.find((N) => N.nodeData.nodeId === RND.nodeId).nodeData = RND; const Options = { width: 900, title: 'ZWave JS Node Management', buttons: [ { id: 'zwjs-tray-close', text: 'Close', click: function () { CloseTray(); } } ], open: function (tray) { isCurrentTray = true; const trayBody = tray.find('.red-ui-tray-body, .editor-tray-body'); const State = { NodeID: $('#zwjs-node-info-id').text(), Status: $('#zwjs-node-status').text(), NodeInfo: $('#zwjs-node-info').text() }; trayBody.append(TPL_NodeManagement(State)); } }; RED.tray.show(Options); setTimeout(() => { const el = $('.zwjs-tray-menu > div[default]')[0]; el.onclick.call(el); }, 250); } else { alert(data.response); } }) .catch((Error) => { alert(Error.message); }); }; // Interview Current Node const InterviewCurrentNode = () => { if (!selectedNode) { return; } if (confirm('Are you sure you wish to re-interview this Node?')) { Runtime.Post('NODE', 'refreshInfo', { nodeId: selectedNode.nodeId }) .then((data) => { if (!data.callSuccess) { alert(data.response); } }) .catch((Error) => { alert(Error.message); }); } }; // Render Advanced Panel Content const RenderAdvanced = async (TemplateID, Target, FunctionIDORObject, WriteTarget) => { if (!AdvancedPanels.find((P) => P.id === TemplateID)) { const TPL = Handlebars.compile($(`#${TemplateID}`).html()); AdvancedPanels.push({ id: TemplateID, compiled: TPL }); } let Data = {}; if (FunctionIDORObject && RenderFunctions[FunctionIDORObject]) { try { Data = await RenderFunctions[FunctionIDORObject](); } catch (Response) { alert(Response); return; } } else if (FunctionIDORObject && typeof FunctionIDORObject === 'object') { Data = FunctionIDORObject; } else if (FunctionIDORObject && typeof FunctionIDORObject === 'string') { Data = DecodeObject(FunctionIDORObject); } const Output = AdvancedPanels.find((P) => P.id === TemplateID).compiled(Data); $(WriteTarget || '#zwjs-advanced-content').empty(); $(WriteTarget || '#zwjs-advanced-content').append(Output); if (Target) { $('.zwjs-tray-menu div').removeAttr('active'); $(Target).attr('active', ''); } }; // Exclusion const StartExclusion = () => { Runtime.Get('CONTROLLER', 'beginExclusion').then((R) => { if (R.callSuccess) { RenderAdvanced('ZWJS_TPL_NIFWait', undefined, { mode: 'Exclusion' }); } else { alert(R.response); } }); }; // Inclusion const StartInclusion = () => { const IS = $('input[type="radio"][name="ZWJS_IS"]:checked').val(); if (IS !== 'SS') { const ISO = { strategy: parseInt(IS), forceSecurity: false }; Runtime.Post('CONTROLLER', 'beginInclusion', [ISO]).then((R) => { if (R.callSuccess) { RenderAdvanced('ZWJS_TPL_NIFWait', undefined, { mode: 'Inclusion' }); } else { alert(R.response); } }); } else { RenderAdvanced('ZWJS_TPL_QRRead', undefined, 'StartCamera'); } }; // Grant Secuity Classes const GrantClasses = (Button) => { const Granted = { clientSideAuth: clientSideAuth, securityClasses: [] }; $('input[type="checkbox"][name="ZWJS_SCLASS"]:checked').each((i, e) => { Granted.securityClasses.push(parseInt($(e).val())); }); Runtime.Post(undefined, undefined, [Granted], `zwave-js/ui/${networkId}/s2/grant`).then((R) => { if (R.callSuccess) { DisableButton(Button); } else { alert(R.response); } }); }; // Submit DSK const SubmitDSK = (Button) => { Runtime.Post(undefined, undefined, [$('#zwjs-dsk').val()], `zwave-js/ui/${networkId}/s2/dsk`).then((R) => { if (R.callSuccess) { DisableButton(Button); } else { alert(R.response); } }); }; // Submit Provisioning Entry const SubmitProvisioningEntry = (Button) => { DisableButton(Button); const Entry = JSON.parse(atob($('#zwjs-qrdata').attr('data-entry'))); Entry.securityClasses = []; Entry.status = 0; $('input[type="checkbox"][name="ZWJS_SCLASS"]:checked').each((i, e) => { Entry.securityClasses.push(parseInt($(e).val())); }); Runtime.Post('CONTROLLER', 'provisionSmartStartNode', [Entry]).then((R) => { if (R.callSuccess) { RenderAdvanced('ZWJS_TPL_SSDone'); } else { alert(R.response); } }); }; // Set Provisioning Entry Status const SetPEActive = (El, Entry) => { Entry = DecodeObject(Entry); delete Entry.checked; delete Entry.shortDSK; Entry.status = $(El).prop('checked') ? 0 : 1; Runtime.Post('CONTROLLER', 'provisionSmartStartNode', [Entry]).then((R) => { if (!R.callSuccess) { alert(R.response); } }); }; // Delete Provisioning Entry const DeletePE = (El, Entry) => { if (confirm('Are you sure you wish to delete this Provisioning Entry? Note: it will not exclude the device.')) { Entry = DecodeObject(Entry); delete Entry.checked; delete Entry.shortDSK; Runtime.Post('CONTROLLER', 'unprovisionSmartStartNode', [Entry.dsk]).then((R) => { if (R.callSuccess) { $(El).parent().parent().remove(); } else { alert(R.response); } }); } }; // Set Name & Location const SetNameLocation = (Button) => { DisableButton(Button); Runtime.Post('NODE', 'setName', { nodeId: selectedNode.nodeId, value: $('#zwjs-node-edit-name').val() || undefined }) .then((data) => { if (!data.callSuccess) { alert(data.response); EnableButton(Button); } else { Runtime.Post('NODE', 'setLocation', { nodeId: selectedNode.nodeId, value: $('#zwjs-node-edit-location').val() || undefined }) .then((data) => { if (!data.callSuccess) { alert(data.response); EnableButton(Button); } else { RefreshNodes('Named'); alert('Name & Location Set Successfully!'); EnableButton(Button); } }) .catch((Error) => { EnableButton(Button); alert(Error.message); }); } }) .catch((Error) => { alert(Error.message); }); }; // Asso EP Select Callback const processAssociationEPSelect = () => { const EP = $('#zwjs-asso-endpoints').val(); const GPs = AssociationGroups[EP]; $('#zwjs-asso-groups').empty(); $('#zwjs-asso-groups').append(new Option('Select Association Group')); for (const [ID, GP] of Object.entries(GPs)) { $('#zwjs-asso-groups').append(new Option(`${GP.label} (Max: ${GP.maxNodes})`, ID)); } }; // Asso GP Select Callback const processAssociationGPSelect = () => { const Group = parseInt($('#zwjs-asso-groups').val()); const Address = { nodeId: selectedNode.nodeId, endpoint: parseInt($('#zwjs-asso-endpoints').val()) }; Runtime.Post('CONTROLLER', 'getAssociations', [Address]).then((data) => { const Mapped = data.response[Group]; $('#zwjs-asso-mappings').empty(); $('#zwjs-asso-mappings').append( '<tr><td style="text-align:center">Target Node</td><td style="text-align:center">Target Endpoint</td><td style="text-align:center">Delete</td></tr>' ); Mapped.forEach((v) => { let EP; switch (v.endpoint) { case undefined: EP = '<span class="zwjs-asso-ep">NODE</span>'; break; case 0: EP = '<span class="zwjs-asso-ep">ROOT</span>'; break; default: EP = `<span class="zwjs-asso-ep">EP${v.endpoint}</span>`; break; } $('#zwjs-asso-mappings').append( `<tr><td style="text-align:center"><span class="zwjs-node-id">${v.nodeId}</span></td><td style="text-align:center">${EP}</td><td style="text-align:center"><i class="fa fa-trash" aria-hidden="true" style="font-size: 18px;color: red; cursor:pointer" onclick="ZWaveJS.MarkAssoDelete(this)"></i></td></tr>` ); }); }); }; // Add new Asso element const PreppNewAssociation = () => { $('#zwjs-asso-mappings').append( '<tr data-role="zwjs-new-association"><td style="text-align:center"><input type="number" data-role="zwjs-node" value="1" min="1"></td><td style="text-align:center"><input type="number" data-role="zwjs-endpoint" min="0" placeholder="<Empty: Node-Association>"></td><td>&nbsp;</td></tr>' ); }; // Send Associations const CommitAssociations = (Button) => { DisableButton(Button); const Addresses = []; $("[data-role='zwjs-remove-association']").each(function () { const Node = parseInt($(this).find('td').first().text()); let Endpoint = parseInt($(this).find('td').first().next().text()); if (isNaN(Endpoint)) { Endpoint = undefined; } Addresses.push({ nodeId: Node, endpoint: Endpoint }); }); if (Addresses.length > 0) { const Params = [ { nodeId: selectedNode.nodeId, endpoint: parseInt($('#zwjs-asso-endpoints').val()) }, parseInt($('#zwjs-asso-groups').val()), Addresses ]; Runtime.Post('CONTROLLER', 'removeAssociations', Params) .then((response) => { if (response.callSuccess) { CommitAssociationsAdd(Button); } else { EnableButton(Button); alert(response.response); } }) .catch((Error) => { alert(Error.message); EnableButton(Button); }); } else { CommitAssociationsAdd(Button); } }; // Clear All Associations const ResetAllAssociations = (Button) => { if ( confirm( 'Are you sure you wish to wipe all Associations? this includes the LifeLine associations, you will need to re-create them after.' ) ) { DisableButton(Button); Runtime.Post('CONTROLLER', 'getAllAssociations', [selectedNode.nodeId]) .then((response) => { if (response.callSuccess) { response.response.forEach(function (E) { Object.keys(E.associations).forEach(async function (G) { if (E.associations[G].length > 0) { const Params = []; Params.push(E.associationAddress); Params.push(parseInt(G)); Params.push(E.associations[G]); try { await Runtime.Post('CONTROLLER', 'removeAssociations', Params); } catch (Error) { alert(Error.message); EnableButton(Button); } } }); }); alert('All associations successfully removed!'); EnableButton(Button); processAssociationGPSelect(); } else { alert(response.response); EnableButton(Button); } }) .catch((Error) => { EnableButton(Button); alert(Error.message); }); } }; // Mark Asso for removal const MarkAssoDelete = (El) => { $(El).closest('tr').attr('data-role', 'zwjs-remove-association'); $(El).closest('tr').css({ filter: 'grayscale()' }); }; // Check Node Helath const CheckNodeHealth = (Button) => { DisableButton(Button); $('#zwjs-node-health-check').find('tr:gt(0)').remove(); const AddTesting = () => { $('#zwjs-node-health-check').append( '<tr><td style="text-align:center"><div class="zwjs-rating" wait>Testing...</div></td><td style="text-align:center">---</td><td style="text-align:center">---</td><td style="text-align:center">---</td><td style="text-align:center">---</td><td style="text-align:center">---</td><td style="text-align:center">---</td><td style="text-align:center">---</td></tr>' ); }; const RemoveTesting = () => { $('#zwjs-node-health-check tr:last').remove(); }; const FeedBack = (topic, data) => { RemoveTesting(); const Rating = () => { if (data.check.lastResult.rating > 5) { return `<div class="zwjs-rating" good>${data.check.lastResult.rating}/10</div>`; } if (data.check.lastResult.rating > 3) { return `<div class="zwjs-rating" warn>${data.check.lastResult.rating}/10</div>`; } return `<div class="zwjs-rating" bad>${data.check.lastResult.rating}/10</div>`; }; $('#zwjs-node-health-check').append( `<tr><td style="text-align:center">${Rating()}</td><td style="text-align:center">${data.check.lastResult.failedPingsNode}</td><td style="text-align:center">${data.check.lastResult.failedPingsController ?? 0}</td><td style="text-align:center">${data.check.lastResult.routeChanges}</td><td style="text-align:center">${data.check.lastResult.latency} ms</td><td style="text-align:center">${data.check.lastResult.numNeighbors}</td><td style="text-align:center">${data.check.lastResult.minPowerlevel} dBm</td><td style="text-align:center">${data.check.lastResult.snrMargin} dBm</td></tr>` ); AddTesting(); }; RED.comms.subscribe(`zwave-js/ui/${networkId}/nodes/healthcheck`, FeedBack); AddTesting(); Runtime.Post('NODE', 'checkLifelineHealth', { nodeId: selectedNode.nodeId }) .then((data) => { if (data.callSuccess) { setTimeout(() => { EnableButton(Button); RemoveTesting(); RED.comms.unsubscribe(`zwave-js/ui/${networkId}/nodes/healthcheck`, FeedBack); }, 250); } else { alert(data.response); } }) .catch((Error) => { EnableButton(Button); RED.comms.unsubscribe(`zwave-js/ui/${networkId}/nodes/healthcheck`, FeedBack); alert(Error.message); }); }; // Become Secondary const JoinAsSlave = (Button) => { Runtime.Get('CONTROLLER', 'beginJoiningNetwork').then((R) => { if (!R.callSuccess) { EnableButton(Button); alert(R.response); } else { const Result = R.response; switch (Result) { case 0: DisableButton(Button); break; case 1: alert('The Controller is currently too busy to perform the join.'); break; case 2: alert("The Controller's role does not permit joining as a secondary controller - try resetting it!"); break; case 3: alert('An unknown error occured.'); break; } } }); }; // Give up Secondary Role const LeaveAsSlave = (Button) => { DisableButton(Button); Runtime.Get('CONTROLLER', 'beginLeavingNetwork').then((R) => { if (!R.callSuccess) { EnableButton(Button); alert(R.response); } }); }; // List Nodes const RefreshNodes = (Reason, NodeID) => { if (!networkId) { return; } switch (Reason) { case 'Refresh': case 'NetworkJoin': case 'NetworkLeft': case 'NetworkSelected': case 'DriverReady': ClearSelection(); CloseTray(); break; case 'NodeAdded': case 'Named': case 'Sorted': break; case 'NodeRemoved': if (selectedNode && selectedNode.nodeId === NodeID) { ClearSelection(); } break; } Runtime.Get('CONTROLLER', 'getNodes') .then((data) => { if (data.callSuccess) { data = data.response; const Controller = data.find((N) => N.isControllerNode); const Nodes = data.filter( (N) => !N.isControllerNode && (N.zwavePlusRoleType > 3 || N.zwavePlusRoleType === undefined) ); const Info = `${Controller.deviceConfig.manufacturer} | ${Controller.deviceConfig.label} | v${Controller.firmwareVersion}`; $('#zwjs-controller-info').text(Info); $('#zwjs-controller-info').data('info', Controller); // Render List const TreeData = []; const getInitials = (name) => { if (!name) return ''; return name .split(/\s+/) .map((word) => word[0] || '') .join('') .toUpperCase(); }; const groupedNodes = Nodes.reduce((acc, node) => { const location = GroupMode[0] ? node.nodeLocation || 'No Location' : 'All Nodes'; if (!acc[location]) { acc[location] = []; } acc[location].push(node); return acc; }, {}); Object.keys(groupedNodes).forEach((LK) => { const GLabel = $( `<div zwjs-node-group><span class="zwjs-node-id">${getInitials(LK)}</span> <i aria-hidden="true" class="zwjs-group-status fa fa-exclamation-triangle zwjs-state-amber" style="display:none"></i> ${LK} </div>` ); const GIconSpan = $('<span group>').addClass('zwjs-node-state-group'); GLabel.append(GIconSpan); GIconSpan.append('<i aria-hidden="true">Int</i>'); GIconSpan.append('<i aria-hidden="true">Sta</i>'); GIconSpan.append('<i aria-hidden="true">Pow</i>'); GIconSpan.append('<i aria-hidden="true">Sec</i>'); const Group = { id: `zwjs-node-list-entry-location-${LK.replace(/ /g, '-')}`, element: GLabel, children: [], expanded: (GroupMode[0] && GroupMode[1]) || !GroupMode[0] }; TreeData.push(Group); groupedNodes[LK].forEach((N) => { const Label = $('<div>'); Label.append(`<span class="zwjs-node-id">${N.nodeId}</span>`); Label.append(`<span id="zwjs-node-name-${N.nodeId}">${N.nodeName || 'No Name'}</span>`); const IconSpan = $('<span>').addClass('zwjs-node-state-group'); Label.append(IconSpan); IconSpan.append(`<i id="zwjs-node-state-interview-${N.nodeId}" aria-hidden="true"></i>`); IconSpan.append(`<i id="zwjs-node-state-status-${N.nodeId}" aria-hidden="true"></i>`); IconSpan.append(`<i id="zwjs-node-state-power-${N.nodeId}" aria-hidden="true"></i>`); IconSpan.append(`<i id="zwjs-node-state-security-${N.nodeId}" aria-hidden="true"></i>`); Group.children.push({ id: `zwjs-node-list-entry-${N.nodeId}`, element: Label, nodeData: N }); }); }); $('#zwjs-node-list').treeList('data', TreeData); TreeData.forEach((G) => { G.children.forEach((N) => { if (N.nodeData) { RenderNodeIconState(N.nodeData); } }); }); } else { alert(data.response); } }) .catch((Error) => { alert(Error.message); }); RenderGroupIconState(); }; // Reset Controller const ResetController = (Button) => { if ( confirm( 'Are you sure you wish to continue? This will reset the controller back to Factory Standard, and if operating as the Primary Controller - will clear the Network of all Nodes.' ) ) { DisableButton(Button); Runtime.Get('DRIVER', 'hardReset').then((R) => { if (!R.callSuccess) { EnableButton(Button); alert(R.response); } else { EnableButton(Button); alert('The Controller has been Reset - It will now be refreshed in the UI'); CloseTray(); NetworkSelected(); } }); } }; // Restore Controller const RestoreController = (Button) => { if ( confirm( 'Note: This will alter the Controllers NVM, and will be configured according to the backup file you will restore to - Do you wish to comntinue?' ) ) { const promptFileUpload = () => { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.style.display = 'none'; document.body.appendChild(fileInput); fileInput.addEventListener('change', async () => { const file = fileInput.files[0]; if (!file) { alert('No file selected'); document.body.removeChild(fileInput); return; } const reader = new FileReader(); reader.onload = function (e) { const arrayBuffer = e.target.result; const byteArray = new Uint8Array(arrayBuffer); Runtime.Post('CONTROLLER', 'restoreNVM', [{ nvmData: byteArray }]).then((R) => { if (!R.callSuccess) { EnableButton(Button); alert(R.response); } else { EnableButton(Button); alert('The restore has been completed! - Please allow a few minutes for the controller to reboot.'); } }); }; reader.readAsArrayBuffer(file); document.body.removeChild(fileInput); }); fileInput.click(); }; DisableButton(Button); promptFileUpload(); } }; // Backup Controller const BackupController = (Button) => { DisableButton(Button); Runtime.Get('CONTROLLER', 'backupNVMRaw').then((R) => { if (!R.callSuccess) { EnableButton(Button); alert(R.response); } else { const CD = $('#zwjs-controller-info').data('info'); const FileName = `zwave_nvm_${CD.homeId}.bin`; const byteArray = Object.values(R.response); const uint8Array = new Uint8Array(byteArray); const blob = new Blob([uint8Array], { type: 'application/octet-stream' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = FileName; document.body.appendChild(a); alert(`Controller Backup is now completed, your browser will now downlaod the file: ${FileName}`); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); setTimeout(() => { $('#zwjs-prog-contain-nvm').css({ display: 'none' }); }, 100); EnableButton(Button); } }); }; // Render Advanded info (also used internally) const RenderFunctions = { CheckFUS: () => { return new Promise((resolve, reject) => { const Request = { includePrereleases: true }; Runtime.Post('CONTROLLER', 'getAllAvailableFirmwareUpdates', [Request]).then((data) => { if (data.callSuccess) { if (Object.keys(data.response).length) { resolve({ Updates: data.response, Message: getFUSLicenseStatus() }); } else { reject('No updates available.'); } } else { reject(data.response); } }); }); }, PrepFUS: () => { return new Promise((resolve) => { const Res = { Message: getFUSLicenseStatus() }; resolve(Res); }); }, GetRRCurrentProgress: () => { Runtime.Get(undefined, undefined, `zwave-js/ui/${networkId}/rebuildroutesprogress`).then((data) => { if (data.callSuccess) { if (data.response !== false) { // emulate the progress event commsRebuildRoutesProgress(undefined, { Progress: data.response }); } } }); }, ListSplitters: () => { return new Promise((resolve) => { const Splitters = []; RED.nodes.filterNodes({ type: 'zwavejs-splitter' }).forEach((F) => { Splitters.push({ name: F.name, id: F.id }); }); let Label = `${ViewingValueID.commandClassName.replace(/ /g, '_').toUpperCase()}.${ViewingValueID.propertyName.replace(/ /g, '_').toUpperCase()}`; if (ViewingValueID.propertyKeyName) { Label += `.${ViewingValueID.propertyKeyName.replace(/ /g, '_').toUpperCase()}`; } resolve({ splitters: Splitters, label: Label, shape: ViewingValueID }); }); }, RenderMap: () => { return new Promise(async (resolve, reject) => { Runtime.Get('CONTROLLER', 'getNodes').then((data) => { if (!data.callSuccess) { reject(data.Response); return; } const nodes = data.response; let nodeString = 'graph TD\r\n'; // TD = top-down let routeString = ''; const Nodes = nodes.filter((n) => !n.isControllerNode); const Controller = nodes.find((n) => n.isControllerNode); // Controller node at the top nodeString += `N0(fa:fa-wifi<br />Controller<br /><span style="font-size:10px">${Controller.deviceConfig?.manufacturer} - ${Controller.deviceConfig?.label}</span>)\r\n`; Nodes.forEach((v) => { const name = v.nodeName || 'No Name'; const icon = v.powerSource.type === 'mains' ? 'fa-plug' : 'fa-battery-full'; const device = `${v.deviceConfig?.manufacturer} - ${v.deviceConfig?.label}`; // Node definition nodeString += `N${v.nodeId}(fa:${icon}<br />${v.nodeId} - ${name}<br /><span style="font-size:10px">${device}</span>)\r\n`; // Bi-directional routes const repeaters = v.statistics?.lwr?.repeaters || []; if (repeaters.length > 0) { repeaters.forEach((r) => { routeString += `N${v.nodeId} <---> N${r}\r\n`; }); } else { routeString += `N0 <===> N${v.nodeId}\r\n`; // Direct to controller } }); const result = `${nodeString}${routeString}`; resolve({ map: result }); // Render Mermaid + enable zoom/pan setTimeout(async () => { ZWJSMermaid.initialize({ startOnLoad: false, securityLevel: 'loose', flowchart: { htmlLabels: true } }); await ZWJSMermaid.run({ querySelector: '.zwjs-mermaid' }); svgPanZoom('.zwjs-mermaid svg', { zoomEnabled: true, controlIconsEnabled: true, panEnabled: true }); }, 50); }); }); }, PrepFailed: () => { return new Promise(async (resolve, reject) => { Runtime.Get('CONTROLLER', 'getNodes').then((data) => { if (data.callSuccess) { const nodes = data.response.filter((N) => N.status === 'Dead'); resolve({ nodes }); } else { reject(data.Response); } }); }); }, ControllerInfo: () => { return new Promise(async (resolve) => { const CD = $('#zwjs-controller-info').data('info'); const versions = await Runtime.Get(undefined, undefined, `zwave-js/ui/${networkId}/version`); const Response = { configuration: $('#zwjs-network option:selected').text(), serialPort: RED.nodes.node(networkId).serialPort, ...versions.response, ...CD }; resolve(Response); }); }, ControllerStats: () => { return new Promise(async (resolve) => { const CD = $('#zwjs-controller-info').data('info'); const Result = { statistics: FormatObjectKeys(CD.statistics), backgroundRSSI: FormatObjectKeys(CD.backgroundRSSI) }; Result.backgroundRSSI.Timestamp = formatDateTime(Result.backgroundRSSI.Timestamp); resolve(Result); }); }, ControllerSettings: () => { return new Promise(async (resolve) => { let Region = await Runtime.Get('CONTROLLER', 'getRFRegion'); let RDisabled = ''; if (Region.callSuccess) { Region = `0x${Region.response.toString(16).padStart(2, '0')}`; } else { RDisabled = 'disabled="disabled"'; } let Power = await Runtime.Get('CONTROLLER', 'getPowerlevel'); if (Power.callSuccess) { Power = Power.response.powerlevel; } let LRPower = await Runtime.Get('CONTROLLER', 'getMaxLongRangePowerlevel'); if (LRPower.callSuccess) { LRPower = LRPower.response; } resolve({ Region, RDisabled, Power, LRPower }); }); }, NodeInfo: () => { return new Promise(async (resolve) => { const ND = GetNodeGroup(selectedNode.nodeLocation).children.find( (N) => N.nodeData.nodeId === selectedNode.nodeId ).nodeData; resolve(ND); }); }, NodeStats: () => { return new Promise(async (resolve) => { const ND = GetNodeGroup(selectedNode.nodeLocation).children.find( (N) => N.nodeData.nodeId === selectedNode.nodeId ).nodeData; const Result = FormatObjectKeys(ND.statistics); Result['Last Seen'] = formatDateTime(Result['Last Seen']); resolve(Result); }); }, NodeAssociationGroups: () => { return new Promise(async (resolve, reject) => { const Response = await Runtime.Post('CONTROLLER', 'getAllAssociationGroups', [selectedNode.nodeId]); if (Response.callSuccess) { AssociationGroups = Response.response; resolve(AssociationGroups); } else { reject(Response.response); } }); }, SetInclusionOptions: () => { return new Promise(async (resolve) => { setTimeout(() => { const S0K = RED.nodes.node(networkId).securityKeys_S0_Legacy; const S2ACK = RED.nodes.node(networkId).securityKeys_S2_AccessControl; const S2AK = RED.nodes.node(networkId).securityKeys_S2_Authenticated; const S2UK = RED.nodes.node(networkId).securityKeys_S2_Unauthenticated; if (S2ACK.length < 32 || S2AK.length < 32 || S2UK.length < 32) { [ 'input[type="radio"][name="ZWJS_IS"][value="0"]', 'input[type="radio"][name="ZWJS_IS"][value="4"]', 'input[type="radio"][name="ZWJS_IS"][value="SS"]' ].forEach((EL) => { $(EL).attr('disabled', 'disabled'); $(EL).parent().css({ opacity: 0.4 }); }); $('input[type="radio"][name="ZWJS_IS"][value="2"]').prop('checked', true); } if (S0K.length < 32) { ['input[type="radio"][name="ZWJS_IS"][value="3"]'].forEach((EL) => { $(EL).attr('disabled', 'disabled'); $(EL).parent().css({ opacity: 0.4 }); }); if (S2ACK.length < 32 || S2AK.length < 32 || S2UK.length < 32) { $('input[type="radio"][name="IS"][value="2"]').prop('checked', true); } } }, 10); resolve({}); }); }, StartCamera: () => { setTimeout(() => { const Options = { highlightCodeOutline: true, highlightScanRegion: true, calculateScanRegion: () => { const ve = $('#zwjs-camera-view')[0]; const sd = Math.min(ve.videoWidth, ve.videoHeight); const srz = Math.round(0.5 * sd); const region = { x: Math.round((ve.videoWidth - srz) / 2), y: Math.round((ve.videoHeight - srz) / 2), width: srz, height: srz }; return region; } }; const EL = $('#zwjs-camera-view')[0]; const Handler = (result) => { QRS.stop(); Runtime.Post(undefined, undefined, [result.data], `zwave-js/ui/${networkId}/s2/parseqr`).then((R) => { if (R.callSuccess) { if (R.response.isDSK) { alert('The QR Code you have scanned, is a DSK (Device Specific Key), it is not a Smart Start QR Code'); QRS.start(); } else { const Classes = []; R.response.qrProvisioningInformation.requestedSecurityClasses.forEach((SC) => { Classes.push({ classId: SC, className: SClassMap[SC] }); }); R.response.qrProvisioningInformation.manufacturer = R.response.deviceConfig.manufacturer; R.response.qrProvisioningInformation.label = R.response.deviceConfig.label; RenderAdvanced('ZWJS_TPL_PrePro', undefined, { QRProvisioningInformation: btoa(JSON.stringify(R.response.qrProvisioningInformation)), DSK: R.response.qrProvisioningInformation.dsk, DeviceConfig: R.response.deviceConfig, classes: Classes }); } } else { alert(R.response); QRS.start(); } }); }; QRS = new QrScanner(EL, Handler, Options); QRS.start(); }, 50); }, PrepSSList: () => { return new Promise((resolve, reject) => { Runtime.Get(undefined, undefined, `zwave-js/ui/${networkId}/s2/provisioningentries`).then((R) => { if (R.callSuccess) { R.response.forEach((E) => { E.shortDSK = E.dsk.split('-')[0]; if (E.status === 0) { E.checked = 'checked'; } }); resolve({ entries: R.response }); } else { reject(R.response); } }); }); } }; // Update Node Firmware const UpdateNFirmwareFUS = (Node, Update) => { const FWI = DecodeObject(Update); if ( confirm( `Note: This will update the Node firmware to the update chosen (version: ${FWI.normalizedVersion}), do you wish to proceed?` ) ) { RenderAdvanced('ZWJS_TPL_Tray-Node-Firmware').then(() => { Runtime.Post('DRIVER', 'firmwareUpdateOTA', [Node, FWI]).catch((Error) => { alert(Error.message); }); }); } }; const UpdateNFirmware = (Button) => { if (confirm("Note: This will update the Nodes's firmware, do you wish to proceed?")) { const promptFileUpload = () => { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.style.display = 'none'; document.body.appendChild(fileInput); fileInput.addEventListener('change', async () => { const file = fileInput.files[0]; if (!file) { alert('No file selected'); document.body.removeChild(fileInput); return; } DisableButton(Button); const reader = new FileReader(); reader.onload = function (e) { const arrayBuffer = e.target.result; const byteArray = new Uint8Array(arrayBuffer); // Handled in COMMS const msg = { nodeId: selectedNode.nodeId, args: [ [ { data: byteArray } ] ] }; Runtime.Post('NODE', 'updateFirmware', msg).catch((Error) => { alert(Error.message); }); }; reader.readAsArrayBuffer(file); document.body.removeChild(fileInput); }); fileInput.click(); }; promptFileUpload(); } }; // Update Controller Firmware const UpdateCFirmwareFUS = (Update) => { const FWI = DecodeObject(Update); if ( confirm( `Note: This will update the Controllers firmware to the update chosen (version: ${FWI.normalizedVersion}), do you wish to proceed?` ) ) { RenderAdvanced('ZWJS_TPL_Tray-Controller-Firmware').then(() => { Runtime.Post('DRIVER', 'firmwareUpdateOTW', [FWI]).catch((Error) => { alert(Error.message); }); }); } }; const UpdateCFirmware = (Button) => { if (confirm('Note: This will update the Controllers firmware, do you wish to proceed?')) { const promptFileUpload = () => { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.style.display = 'none'; document.body.appendChild(fileInput); fileInput.addEventListener('change', async () => {