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
JavaScript
/* 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> </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 () => {