zwave-js 10.3.0 → 10.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/Controller.d.ts +1 -1
- package/build/Controller.d.ts.map +1 -1
- package/build/Controller.js +2 -1
- package/build/Controller.js.map +1 -1
- package/build/Controller_safe.d.ts +1 -1
- package/build/Controller_safe.d.ts.map +1 -1
- package/build/Controller_safe.js +2 -1
- package/build/Controller_safe.js.map +1 -1
- package/build/lib/controller/Controller.d.ts +65 -2
- package/build/lib/controller/Controller.d.ts.map +1 -1
- package/build/lib/controller/Controller.js +670 -21
- package/build/lib/controller/Controller.js.map +1 -1
- package/build/lib/controller/FirmwareUpdateService.d.ts +2 -1
- package/build/lib/controller/FirmwareUpdateService.d.ts.map +1 -1
- package/build/lib/controller/FirmwareUpdateService.js +47 -82
- package/build/lib/controller/FirmwareUpdateService.js.map +1 -1
- package/build/lib/controller/Inclusion.d.ts +9 -4
- package/build/lib/controller/Inclusion.d.ts.map +1 -1
- package/build/lib/controller/Inclusion.js.map +1 -1
- package/build/lib/controller/NodeInformationFrame.d.ts.map +1 -1
- package/build/lib/controller/NodeInformationFrame.js +3 -2
- package/build/lib/controller/NodeInformationFrame.js.map +1 -1
- package/build/lib/controller/_Types.d.ts +24 -2
- package/build/lib/controller/_Types.d.ts.map +1 -1
- package/build/lib/controller/_Types.js +13 -0
- package/build/lib/controller/_Types.js.map +1 -1
- package/build/lib/driver/Bootloader.d.ts +22 -0
- package/build/lib/driver/Bootloader.d.ts.map +1 -0
- package/build/lib/driver/Bootloader.js +52 -0
- package/build/lib/driver/Bootloader.js.map +1 -0
- package/build/lib/driver/CommandQueueMachine.d.ts +4 -4
- package/build/lib/driver/CommandQueueMachine.d.ts.map +1 -1
- package/build/lib/driver/Driver.d.ts +27 -3
- package/build/lib/driver/Driver.d.ts.map +1 -1
- package/build/lib/driver/Driver.js +178 -51
- package/build/lib/driver/Driver.js.map +1 -1
- package/build/lib/driver/DriverMock.d.ts +5 -1
- package/build/lib/driver/DriverMock.d.ts.map +1 -1
- package/build/lib/driver/DriverMock.js +5 -1
- package/build/lib/driver/DriverMock.js.map +1 -1
- package/build/lib/driver/MessageGenerators.d.ts +1 -1
- package/build/lib/driver/MessageGenerators.d.ts.map +1 -1
- package/build/lib/driver/SendThreadMachine.d.ts +7 -7
- package/build/lib/driver/SendThreadMachine.d.ts.map +1 -1
- package/build/lib/driver/SerialAPICommandMachine.d.ts +8 -8
- package/build/lib/driver/SerialAPICommandMachine.d.ts.map +1 -1
- package/build/lib/driver/SerialAPICommandMachine.js +7 -1
- package/build/lib/driver/SerialAPICommandMachine.js.map +1 -1
- package/build/lib/driver/StateMachineShared.d.ts +2 -2
- package/build/lib/driver/StateMachineShared.d.ts.map +1 -1
- package/build/lib/driver/TransactionMachine.d.ts +3 -3
- package/build/lib/driver/TransactionMachine.d.ts.map +1 -1
- package/build/lib/driver/TransportServiceMachine.d.ts +4 -4
- package/build/lib/driver/TransportServiceMachine.d.ts.map +1 -1
- package/build/lib/driver/ZWaveOptions.d.ts +12 -1
- package/build/lib/driver/ZWaveOptions.d.ts.map +1 -1
- package/build/lib/node/Node.d.ts +3 -3
- package/build/lib/node/Node.d.ts.map +1 -1
- package/build/lib/node/Node.js +65 -14
- package/build/lib/node/Node.js.map +1 -1
- package/build/lib/node/NodeReadyMachine.d.ts +3 -3
- package/build/lib/node/NodeReadyMachine.d.ts.map +1 -1
- package/build/lib/node/NodeStatusMachine.d.ts +3 -3
- package/build/lib/node/NodeStatusMachine.d.ts.map +1 -1
- package/build/lib/node/_Types.d.ts +21 -21
- package/build/lib/node/_Types.d.ts.map +1 -1
- package/build/lib/serialapi/application/ApplicationUpdateRequest.d.ts +14 -10
- package/build/lib/serialapi/application/ApplicationUpdateRequest.d.ts.map +1 -1
- package/build/lib/serialapi/application/ApplicationUpdateRequest.js +27 -16
- package/build/lib/serialapi/application/ApplicationUpdateRequest.js.map +1 -1
- package/build/lib/serialapi/network-mgmt/RequestNodeInfoMessages.d.ts +3 -0
- package/build/lib/serialapi/network-mgmt/RequestNodeInfoMessages.d.ts.map +1 -1
- package/build/lib/serialapi/network-mgmt/RequestNodeInfoMessages.js +12 -0
- package/build/lib/serialapi/network-mgmt/RequestNodeInfoMessages.js.map +1 -1
- package/build/lib/serialapi/nvm/FirmwareUpdateNVMMessages.d.ts +96 -0
- package/build/lib/serialapi/nvm/FirmwareUpdateNVMMessages.d.ts.map +1 -0
- package/build/lib/serialapi/nvm/FirmwareUpdateNVMMessages.js +326 -0
- package/build/lib/serialapi/nvm/FirmwareUpdateNVMMessages.js.map +1 -0
- package/build/lib/serialapi/nvm/GetNVMIdMessages.d.ts +1 -1
- package/build/lib/serialapi/nvm/GetNVMIdMessages.d.ts.map +1 -1
- package/build/lib/serialapi/transport/SendDataShared.d.ts +3 -3
- package/build/lib/serialapi/transport/SendDataShared.d.ts.map +1 -1
- package/package.json +15 -15
|
@@ -18,6 +18,7 @@ const shared_1 = require("@zwave-js/shared");
|
|
|
18
18
|
const arrays_1 = require("alcalzone-shared/arrays");
|
|
19
19
|
const async_1 = require("alcalzone-shared/async");
|
|
20
20
|
const deferred_promise_1 = require("alcalzone-shared/deferred-promise");
|
|
21
|
+
const math_1 = require("alcalzone-shared/math");
|
|
21
22
|
const typeguards_1 = require("alcalzone-shared/typeguards");
|
|
22
23
|
const crypto_1 = __importDefault(require("crypto"));
|
|
23
24
|
const semver_1 = __importDefault(require("semver"));
|
|
@@ -27,6 +28,7 @@ const DeviceClass_1 = require("../node/DeviceClass");
|
|
|
27
28
|
const Node_1 = require("../node/Node");
|
|
28
29
|
const VirtualNode_1 = require("../node/VirtualNode");
|
|
29
30
|
const _Types_1 = require("../node/_Types");
|
|
31
|
+
const ApplicationUpdateRequest_1 = require("../serialapi/application/ApplicationUpdateRequest");
|
|
30
32
|
const GetControllerCapabilitiesMessages_1 = require("../serialapi/capability/GetControllerCapabilitiesMessages");
|
|
31
33
|
const GetControllerVersionMessages_1 = require("../serialapi/capability/GetControllerVersionMessages");
|
|
32
34
|
const GetProtocolVersionMessages_1 = require("../serialapi/capability/GetProtocolVersionMessages");
|
|
@@ -56,6 +58,7 @@ const ExtNVMReadLongBufferMessages_1 = require("../serialapi/nvm/ExtNVMReadLongB
|
|
|
56
58
|
const ExtNVMReadLongByteMessages_1 = require("../serialapi/nvm/ExtNVMReadLongByteMessages");
|
|
57
59
|
const ExtNVMWriteLongBufferMessages_1 = require("../serialapi/nvm/ExtNVMWriteLongBufferMessages");
|
|
58
60
|
const ExtNVMWriteLongByteMessages_1 = require("../serialapi/nvm/ExtNVMWriteLongByteMessages");
|
|
61
|
+
const FirmwareUpdateNVMMessages_1 = require("../serialapi/nvm/FirmwareUpdateNVMMessages");
|
|
59
62
|
const GetNVMIdMessages_1 = require("../serialapi/nvm/GetNVMIdMessages");
|
|
60
63
|
const NVMOperationsMessages_1 = require("../serialapi/nvm/NVMOperationsMessages");
|
|
61
64
|
const _Types_2 = require("../serialapi/_Types");
|
|
@@ -66,9 +69,10 @@ const Inclusion_1 = require("./Inclusion");
|
|
|
66
69
|
const NodeInformationFrame_1 = require("./NodeInformationFrame");
|
|
67
70
|
const utils_1 = require("./utils");
|
|
68
71
|
const ZWaveSDKVersions_1 = require("./ZWaveSDKVersions");
|
|
72
|
+
const _Types_3 = require("./_Types");
|
|
69
73
|
let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
70
74
|
/** @internal */
|
|
71
|
-
constructor(driver) {
|
|
75
|
+
constructor(driver, bootloaderOnly = false) {
|
|
72
76
|
super();
|
|
73
77
|
this.driver = driver;
|
|
74
78
|
this._healNetworkActive = false;
|
|
@@ -76,9 +80,13 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
76
80
|
this._smartStartEnabled = false;
|
|
77
81
|
this._includeController = false;
|
|
78
82
|
this._healNetworkProgress = new Map();
|
|
83
|
+
this._firmwareUpdateInProgress = false;
|
|
79
84
|
this._nodes = (0, shared_1.createThrowingMap)((nodeId) => {
|
|
80
85
|
throw new core_1.ZWaveError(`Node ${nodeId} was not found!`, core_1.ZWaveErrorCodes.Controller_NodeNotFound);
|
|
81
86
|
});
|
|
87
|
+
// Limit interaction with the controller in bootloader-only mode
|
|
88
|
+
if (bootloaderOnly)
|
|
89
|
+
return;
|
|
82
90
|
// register message handlers
|
|
83
91
|
driver.registerRequestHandler(serial_1.FunctionType.AddNodeToNetwork, this.handleAddNodeStatusReport.bind(this));
|
|
84
92
|
driver.registerRequestHandler(serial_1.FunctionType.RemoveNodeFromNetwork, this.handleRemoveNodeStatusReport.bind(this));
|
|
@@ -87,6 +95,9 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
87
95
|
get type() {
|
|
88
96
|
return this._type;
|
|
89
97
|
}
|
|
98
|
+
get protocolVersion() {
|
|
99
|
+
return this._protocolVersion;
|
|
100
|
+
}
|
|
90
101
|
get sdkVersion() {
|
|
91
102
|
return this._sdkVersion;
|
|
92
103
|
}
|
|
@@ -130,32 +141,28 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
130
141
|
if (this._sdkVersion === undefined) {
|
|
131
142
|
return undefined;
|
|
132
143
|
}
|
|
133
|
-
|
|
134
|
-
return semver_1.default.gt((0, shared_1.padVersion)(sdkVersion), (0, shared_1.padVersion)(version));
|
|
144
|
+
return semver_1.default.gt((0, shared_1.padVersion)(this._sdkVersion), (0, shared_1.padVersion)(version));
|
|
135
145
|
}
|
|
136
146
|
/** Checks if the SDK version is greater than or equal to the given one */
|
|
137
147
|
sdkVersionGte(version) {
|
|
138
148
|
if (this._sdkVersion === undefined) {
|
|
139
149
|
return undefined;
|
|
140
150
|
}
|
|
141
|
-
|
|
142
|
-
return semver_1.default.gte((0, shared_1.padVersion)(sdkVersion), (0, shared_1.padVersion)(version));
|
|
151
|
+
return semver_1.default.gte((0, shared_1.padVersion)(this._sdkVersion), (0, shared_1.padVersion)(version));
|
|
143
152
|
}
|
|
144
153
|
/** Checks if the SDK version is lower than the given one */
|
|
145
154
|
sdkVersionLt(version) {
|
|
146
155
|
if (this._sdkVersion === undefined) {
|
|
147
156
|
return undefined;
|
|
148
157
|
}
|
|
149
|
-
|
|
150
|
-
return semver_1.default.lt((0, shared_1.padVersion)(sdkVersion), (0, shared_1.padVersion)(version));
|
|
158
|
+
return semver_1.default.lt((0, shared_1.padVersion)(this._sdkVersion), (0, shared_1.padVersion)(version));
|
|
151
159
|
}
|
|
152
160
|
/** Checks if the SDK version is lower than or equal to the given one */
|
|
153
161
|
sdkVersionLte(version) {
|
|
154
162
|
if (this._sdkVersion === undefined) {
|
|
155
163
|
return undefined;
|
|
156
164
|
}
|
|
157
|
-
|
|
158
|
-
return semver_1.default.lte((0, shared_1.padVersion)(sdkVersion), (0, shared_1.padVersion)(version));
|
|
165
|
+
return semver_1.default.lte((0, shared_1.padVersion)(this._sdkVersion), (0, shared_1.padVersion)(version));
|
|
159
166
|
}
|
|
160
167
|
get manufacturerId() {
|
|
161
168
|
return this._manufacturerId;
|
|
@@ -219,6 +226,10 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
219
226
|
set supportsSoftReset(value) {
|
|
220
227
|
this.driver.cacheSet(NetworkCache_1.cacheKeys.controller.supportsSoftReset, value);
|
|
221
228
|
}
|
|
229
|
+
/** Which RF region the controller is currently set to, or `undefined` if it could not be determined (yet). This value is cached and can be changed through {@link setRFRegion}. */
|
|
230
|
+
get rfRegion() {
|
|
231
|
+
return this._rfRegion;
|
|
232
|
+
}
|
|
222
233
|
/** A dictionary of the nodes connected to this controller */
|
|
223
234
|
get nodes() {
|
|
224
235
|
return this._nodes;
|
|
@@ -416,18 +427,16 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
416
427
|
const version = await this.driver.sendMessage(new GetControllerVersionMessages_1.GetControllerVersionRequest(this.driver), {
|
|
417
428
|
supportCheck: false,
|
|
418
429
|
});
|
|
419
|
-
this.
|
|
430
|
+
this._protocolVersion = version.libraryVersion;
|
|
420
431
|
this._type = version.controllerType;
|
|
421
432
|
this.driver.controllerLog.print(`received version info:
|
|
422
433
|
controller type: ${(0, shared_1.getEnumMemberName)(_Types_2.ZWaveLibraryTypes, this._type)}
|
|
423
|
-
library version: ${this.
|
|
434
|
+
library version: ${this._protocolVersion}`);
|
|
424
435
|
// If supported, get more fine-grained version info
|
|
425
436
|
if (this.isFunctionSupported(serial_1.FunctionType.GetProtocolVersion)) {
|
|
426
437
|
this.driver.controllerLog.print(`querying protocol version info...`);
|
|
427
438
|
const protocol = await this.driver.sendMessage(new GetProtocolVersionMessages_1.GetProtocolVersionRequest(this.driver));
|
|
428
|
-
|
|
429
|
-
// valid for 7.x firmwares, where SDK and protocol version are the same.
|
|
430
|
-
this._sdkVersion = protocol.protocolVersion;
|
|
439
|
+
this._protocolVersion = protocol.protocolVersion;
|
|
431
440
|
let message = `received protocol version info:
|
|
432
441
|
protocol type: ${(0, shared_1.getEnumMemberName)(core_1.ProtocolType, protocol.protocolType)}
|
|
433
442
|
protocol version: ${protocol.protocolVersion}`;
|
|
@@ -441,6 +450,8 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
441
450
|
}
|
|
442
451
|
this.driver.controllerLog.print(message);
|
|
443
452
|
}
|
|
453
|
+
// The SDK version cannot be queried directly, but we can deduce it from the protocol version
|
|
454
|
+
this._sdkVersion = (0, ZWaveSDKVersions_1.protocolVersionToSDKVersion)(this._protocolVersion);
|
|
444
455
|
this.driver.controllerLog.print(`supported Z-Wave features: ${Object.keys(Features_1.ZWaveFeature)
|
|
445
456
|
.filter((k) => /^\d+$/.test(k))
|
|
446
457
|
.map((k) => parseInt(k))
|
|
@@ -484,6 +495,17 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
484
495
|
}));
|
|
485
496
|
this.driver.controllerLog.print(`Enabling TX status report ${resp.success ? "successful" : "failed"}...`);
|
|
486
497
|
}
|
|
498
|
+
// Also query the controller's current RF region if possible
|
|
499
|
+
if (this.isSerialAPISetupCommandSupported(SerialAPISetupMessages_1.SerialAPISetupCommand.GetRFRegion)) {
|
|
500
|
+
this.driver.controllerLog.print(`Querying configured RF region...`);
|
|
501
|
+
const resp = await this.getRFRegion().catch(() => undefined);
|
|
502
|
+
if (resp != undefined) {
|
|
503
|
+
this.driver.controllerLog.print(`The controller is using RF region ${(0, shared_1.getEnumMemberName)(core_1.RFRegion, resp)}`);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
this.driver.controllerLog.print(`Querying the RF region failed!`, "warn");
|
|
507
|
+
}
|
|
508
|
+
}
|
|
487
509
|
// find the SUC
|
|
488
510
|
this.driver.controllerLog.print(`finding SUC...`);
|
|
489
511
|
const suc = await this.driver.sendMessage(new GetSUCNodeIdMessages_1.GetSUCNodeIdRequest(this.driver), { supportCheck: false });
|
|
@@ -569,6 +591,8 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
569
591
|
controllerValueDB.setValue(cc_1.VersionCCValues.firmwareVersions.id, [
|
|
570
592
|
this._firmwareVersion,
|
|
571
593
|
]);
|
|
594
|
+
controllerValueDB.setMetadata(cc_1.VersionCCValues.zWaveProtocolVersion.id, cc_1.VersionCCValues.zWaveProtocolVersion.meta);
|
|
595
|
+
controllerValueDB.setValue(cc_1.VersionCCValues.zWaveProtocolVersion.id, this._protocolVersion);
|
|
572
596
|
controllerValueDB.setMetadata(cc_1.VersionCCValues.sdkVersion.id, cc_1.VersionCCValues.sdkVersion.meta);
|
|
573
597
|
controllerValueDB.setValue(cc_1.VersionCCValues.sdkVersion.id, this._sdkVersion);
|
|
574
598
|
if (this.type !== _Types_2.ZWaveLibraryTypes["Bridge Controller"] &&
|
|
@@ -964,6 +988,299 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
964
988
|
throw e;
|
|
965
989
|
}
|
|
966
990
|
}
|
|
991
|
+
/** @internal */
|
|
992
|
+
async handleApplicationUpdateRequest(msg) {
|
|
993
|
+
const nodeId = msg.getNodeId();
|
|
994
|
+
let node;
|
|
995
|
+
if (nodeId != undefined) {
|
|
996
|
+
node = this.nodes.get(nodeId);
|
|
997
|
+
}
|
|
998
|
+
if (msg instanceof ApplicationUpdateRequest_1.ApplicationUpdateRequestNodeInfoReceived) {
|
|
999
|
+
if (node) {
|
|
1000
|
+
this.driver.controllerLog.logNode(node.id, {
|
|
1001
|
+
message: "Received updated node info",
|
|
1002
|
+
direction: "inbound",
|
|
1003
|
+
});
|
|
1004
|
+
node.updateNodeInfo(msg.nodeInformation);
|
|
1005
|
+
// Tell the send thread that we received a NIF from the node
|
|
1006
|
+
this.driver["sendThread"].send({
|
|
1007
|
+
type: "NIF",
|
|
1008
|
+
nodeId: node.id,
|
|
1009
|
+
});
|
|
1010
|
+
if (node.canSleep &&
|
|
1011
|
+
node.supportsCC(core_1.CommandClasses["Wake Up"])) {
|
|
1012
|
+
// In case this is a sleeping node and there are no messages in the queue, the node may go back to sleep very soon
|
|
1013
|
+
this.driver.debounceSendNodeToSleep(node);
|
|
1014
|
+
}
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
else if (msg instanceof ApplicationUpdateRequest_1.ApplicationUpdateRequestSmartStartHomeIDReceived) {
|
|
1019
|
+
// the controller is in Smart Start learn mode and a node requests inclusion via Smart Start
|
|
1020
|
+
this.driver.controllerLog.print("Received Smart Start inclusion request");
|
|
1021
|
+
if (this.inclusionState !== Inclusion_1.InclusionState.Idle &&
|
|
1022
|
+
this.inclusionState !== Inclusion_1.InclusionState.SmartStart) {
|
|
1023
|
+
this.driver.controllerLog.print("Controller is busy and cannot handle this inclusion request right now...");
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
// Check if the node is on the provisioning list
|
|
1027
|
+
const provisioningEntry = this.provisioningList.find((entry) => (0, core_1.nwiHomeIdFromDSK)((0, core_1.dskFromString)(entry.dsk)).equals(msg.nwiHomeId));
|
|
1028
|
+
if (!provisioningEntry) {
|
|
1029
|
+
this.driver.controllerLog.print("NWI Home ID not found in provisioning list, ignoring request...");
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
else if (provisioningEntry.status === Inclusion_1.ProvisioningEntryStatus.Inactive) {
|
|
1033
|
+
this.driver.controllerLog.print("The provisioning entry for this node is inactive, ignoring request...");
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
this.driver.controllerLog.print("NWI Home ID found in provisioning list, including node...");
|
|
1037
|
+
try {
|
|
1038
|
+
const result = await this.beginInclusionSmartStart(provisioningEntry);
|
|
1039
|
+
if (!result) {
|
|
1040
|
+
this.driver.controllerLog.print("Smart Start inclusion could not be started", "error");
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
catch (e) {
|
|
1044
|
+
this.driver.controllerLog.print(`Smart Start inclusion could not be started: ${(0, shared_1.getErrorMessage)(e)}`, "error");
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
else if (msg instanceof ApplicationUpdateRequest_1.ApplicationUpdateRequestNodeRemoved) {
|
|
1048
|
+
// A node was removed by another controller
|
|
1049
|
+
const node = this.nodes.get(msg.nodeId);
|
|
1050
|
+
if (node) {
|
|
1051
|
+
this.driver.controllerLog.logNode(node.id, "was removed from the network by another controller");
|
|
1052
|
+
this.emit("node removed", node, false);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
else if (msg instanceof ApplicationUpdateRequest_1.ApplicationUpdateRequestNodeAdded) {
|
|
1056
|
+
// A node was included by another controller
|
|
1057
|
+
const nodeId = msg.nodeId;
|
|
1058
|
+
const nodeInfo = msg.nodeInformation;
|
|
1059
|
+
this.setInclusionState(Inclusion_1.InclusionState.Busy);
|
|
1060
|
+
const deviceClass = new DeviceClass_1.DeviceClass(this.driver.configManager, nodeInfo.basicDeviceClass, nodeInfo.genericDeviceClass, nodeInfo.specificDeviceClass);
|
|
1061
|
+
const newNode = new Node_1.ZWaveNode(nodeId, this.driver, deviceClass, nodeInfo.supportedCCs, undefined,
|
|
1062
|
+
// Create an empty value DB and specify that it contains no values
|
|
1063
|
+
// to avoid indexing the existing values
|
|
1064
|
+
this.createValueDBForNode(nodeId, new Set()));
|
|
1065
|
+
this._nodes.set(nodeId, newNode);
|
|
1066
|
+
this.emit("node found", {
|
|
1067
|
+
id: nodeId,
|
|
1068
|
+
deviceClass,
|
|
1069
|
+
supportedCCs: nodeInfo.supportedCCs,
|
|
1070
|
+
});
|
|
1071
|
+
this.driver.controllerLog.print(`Node ${newNode.id} was included by another controller:
|
|
1072
|
+
basic device class: ${newNode.deviceClass?.basic.label}
|
|
1073
|
+
generic device class: ${newNode.deviceClass?.generic.label}
|
|
1074
|
+
specific device class: ${newNode.deviceClass?.specific.label}
|
|
1075
|
+
supported CCs: ${nodeInfo.supportedCCs
|
|
1076
|
+
.map((cc) => `\n · ${core_1.CommandClasses[cc]} (${(0, shared_1.num2hex)(cc)})`)
|
|
1077
|
+
.join("")}`);
|
|
1078
|
+
this.driver.controllerLog.logNode(nodeId, "Waiting for initiate command to bootstrap node...");
|
|
1079
|
+
// Handle inclusion in the background
|
|
1080
|
+
process.nextTick(async () => {
|
|
1081
|
+
// If an Inclusion Controller that does not support the Inclusion Controller Command Class includes a
|
|
1082
|
+
// new node in a network, the SIS will never receive an Inclusion Controller Initiate Command. If no
|
|
1083
|
+
// Initiate Command has been received approximately 10 seconds after a new node has been added to a
|
|
1084
|
+
// network, the SIS SHOULD start interviewing the newly included node
|
|
1085
|
+
const initiate = await this.driver
|
|
1086
|
+
.waitForCommand((cc) => cc instanceof cc_1.InclusionControllerCCInitiate &&
|
|
1087
|
+
cc.isSinglecast() &&
|
|
1088
|
+
cc.includedNodeId === nodeId &&
|
|
1089
|
+
cc.step === cc_1.InclusionControllerStep.ProxyInclusion, 10000)
|
|
1090
|
+
.catch(() => undefined);
|
|
1091
|
+
// Assume the device is alive
|
|
1092
|
+
// If it is actually a sleeping device, it will be marked as such later
|
|
1093
|
+
newNode.markAsAlive();
|
|
1094
|
+
let inclCtrlr;
|
|
1095
|
+
let lowSecurity = false;
|
|
1096
|
+
if (initiate) {
|
|
1097
|
+
inclCtrlr = this.nodes.getOrThrow(initiate.nodeId);
|
|
1098
|
+
this.driver.controllerLog.logNode(nodeId, `Initiate command received from node ${inclCtrlr.id}`);
|
|
1099
|
+
// Inclusion is handled by the inclusion controller, which (hopefully) sets the SUC return route
|
|
1100
|
+
newNode.hasSUCReturnRoute = true;
|
|
1101
|
+
// SIS, A, MUST request a Node Info Frame from Joining Node, B
|
|
1102
|
+
const requestedNodeInfo = await newNode
|
|
1103
|
+
.requestNodeInfo()
|
|
1104
|
+
.catch(() => undefined);
|
|
1105
|
+
if (requestedNodeInfo)
|
|
1106
|
+
newNode.updateNodeInfo(requestedNodeInfo);
|
|
1107
|
+
// Perform S0/S2 bootstrapping
|
|
1108
|
+
lowSecurity = (await this.proxyBootstrap(newNode, inclCtrlr)).lowSecurity;
|
|
1109
|
+
}
|
|
1110
|
+
else {
|
|
1111
|
+
// No command received, bootstrap node by ourselves
|
|
1112
|
+
this.driver.controllerLog.logNode(nodeId, "no initiate command received, bootstrapping node...");
|
|
1113
|
+
// Assign SUC return route to make sure the node knows where to get its routes from
|
|
1114
|
+
newNode.hasSUCReturnRoute = await this.assignSUCReturnRoute(newNode.id);
|
|
1115
|
+
// Include using the default inclusion strategy:
|
|
1116
|
+
// * Use S2 if possible,
|
|
1117
|
+
// * only use S0 if necessary,
|
|
1118
|
+
// * use no encryption otherwise
|
|
1119
|
+
if (newNode.supportsCC(core_1.CommandClasses["Security 2"])) {
|
|
1120
|
+
await this.secureBootstrapS2(newNode);
|
|
1121
|
+
const actualSecurityClass = newNode.getHighestSecurityClass();
|
|
1122
|
+
if (actualSecurityClass == undefined ||
|
|
1123
|
+
actualSecurityClass <
|
|
1124
|
+
core_1.SecurityClass.S2_Unauthenticated) {
|
|
1125
|
+
lowSecurity = true;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
else if (newNode.supportsCC(core_1.CommandClasses.Security) &&
|
|
1129
|
+
(deviceClass.specific ?? deviceClass.generic)
|
|
1130
|
+
.requiresSecurity) {
|
|
1131
|
+
await this.secureBootstrapS0(newNode);
|
|
1132
|
+
const actualSecurityClass = newNode.getHighestSecurityClass();
|
|
1133
|
+
if (actualSecurityClass == undefined ||
|
|
1134
|
+
actualSecurityClass < core_1.SecurityClass.S0_Legacy) {
|
|
1135
|
+
lowSecurity = true;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
else {
|
|
1139
|
+
// Remember that no security classes were granted
|
|
1140
|
+
for (const secClass of core_1.securityClassOrder) {
|
|
1141
|
+
newNode.securityClasses.set(secClass, false);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
// Bootstrap the node's lifelines, so it knows where the controller is
|
|
1146
|
+
await this.bootstrapLifelineAndWakeup(newNode);
|
|
1147
|
+
// We're done adding this node, notify listeners
|
|
1148
|
+
const result = {};
|
|
1149
|
+
if (lowSecurity)
|
|
1150
|
+
result.lowSecurity = true;
|
|
1151
|
+
this.emit("node added", newNode, result);
|
|
1152
|
+
this.setInclusionState(Inclusion_1.InclusionState.Idle);
|
|
1153
|
+
if (inclCtrlr && initiate) {
|
|
1154
|
+
const inclCtrlrId = inclCtrlr.id;
|
|
1155
|
+
const step = initiate.step;
|
|
1156
|
+
newNode.once("ready", () => {
|
|
1157
|
+
this.driver.controllerLog.logNode(nodeId, `Notifying node ${inclCtrlrId} of finished inclusion`);
|
|
1158
|
+
void inclCtrlr.commandClasses["Inclusion Controller"]
|
|
1159
|
+
.completeStep(step, cc_1.InclusionControllerStatus.OK)
|
|
1160
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
1161
|
+
.catch(() => { });
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* @internal
|
|
1169
|
+
* Handles replace requests from an inclusion controller
|
|
1170
|
+
*/
|
|
1171
|
+
handleInclusionControllerCCInitiateReplace(initiate) {
|
|
1172
|
+
if (initiate.step !== cc_1.InclusionControllerStep.ProxyInclusionReplace) {
|
|
1173
|
+
throw new core_1.ZWaveError("Expected an inclusion controller replace request, but got a different step", core_1.ZWaveErrorCodes.Argument_Invalid);
|
|
1174
|
+
}
|
|
1175
|
+
this.setInclusionState(Inclusion_1.InclusionState.Busy);
|
|
1176
|
+
const inclCtrlr = this.nodes.getOrThrow(initiate.nodeId);
|
|
1177
|
+
const replacedNodeId = initiate.includedNodeId;
|
|
1178
|
+
const oldNode = this.nodes.get(replacedNodeId);
|
|
1179
|
+
if (oldNode) {
|
|
1180
|
+
this.emit("node removed", oldNode, true);
|
|
1181
|
+
this._nodes.delete(oldNode.id);
|
|
1182
|
+
}
|
|
1183
|
+
// Create a fresh node instance and forget the old one
|
|
1184
|
+
const newNode = new Node_1.ZWaveNode(replacedNodeId, this.driver, undefined, undefined, undefined,
|
|
1185
|
+
// Create an empty value DB and specify that it contains no values
|
|
1186
|
+
// to avoid indexing the existing values
|
|
1187
|
+
this.createValueDBForNode(replacedNodeId, new Set()));
|
|
1188
|
+
this._nodes.set(newNode.id, newNode);
|
|
1189
|
+
this.emit("node found", {
|
|
1190
|
+
id: newNode.id,
|
|
1191
|
+
});
|
|
1192
|
+
// Assume the device is alive
|
|
1193
|
+
// If it is actually a sleeping device, it will be marked as such later
|
|
1194
|
+
newNode.markAsAlive();
|
|
1195
|
+
// Inclusion is handled by the inclusion controller, which (hopefully) sets the SUC return route
|
|
1196
|
+
newNode.hasSUCReturnRoute = true;
|
|
1197
|
+
// Handle communication with the node in the background
|
|
1198
|
+
process.nextTick(async () => {
|
|
1199
|
+
// SIS, A, MUST request a Node Info Frame from Joining Node, B
|
|
1200
|
+
const requestedNodeInfo = await newNode
|
|
1201
|
+
.requestNodeInfo()
|
|
1202
|
+
.catch(() => undefined);
|
|
1203
|
+
if (requestedNodeInfo) {
|
|
1204
|
+
newNode.updateNodeInfo(requestedNodeInfo);
|
|
1205
|
+
// TODO: Check if this stuff works for a normal replace too
|
|
1206
|
+
const deviceClass = new DeviceClass_1.DeviceClass(this.driver.configManager, requestedNodeInfo.basicDeviceClass, requestedNodeInfo.genericDeviceClass, requestedNodeInfo.specificDeviceClass);
|
|
1207
|
+
newNode["applyDeviceClass"](deviceClass);
|
|
1208
|
+
}
|
|
1209
|
+
// Perform S0/S2 bootstrapping
|
|
1210
|
+
const lowSecurity = (await this.proxyBootstrap(newNode, inclCtrlr))
|
|
1211
|
+
.lowSecurity;
|
|
1212
|
+
// Bootstrap the node's lifelines, so it knows where the controller is
|
|
1213
|
+
await this.bootstrapLifelineAndWakeup(newNode);
|
|
1214
|
+
// We're done adding this node, notify listeners
|
|
1215
|
+
const result = {};
|
|
1216
|
+
if (lowSecurity)
|
|
1217
|
+
result.lowSecurity = true;
|
|
1218
|
+
this.emit("node added", newNode, result);
|
|
1219
|
+
this.setInclusionState(Inclusion_1.InclusionState.Idle);
|
|
1220
|
+
// And notify the inclusion controller after we're done interviewing
|
|
1221
|
+
newNode.once("ready", () => {
|
|
1222
|
+
this.driver.controllerLog.logNode(inclCtrlr.nodeId, `Notifying inclusion controller of finished inclusion`);
|
|
1223
|
+
void inclCtrlr.commandClasses["Inclusion Controller"]
|
|
1224
|
+
.completeStep(initiate.step, cc_1.InclusionControllerStatus.OK)
|
|
1225
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
1226
|
+
.catch(() => { });
|
|
1227
|
+
});
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Handles bootstrapping the security keys for a node that was included by an inclusion controller
|
|
1232
|
+
*/
|
|
1233
|
+
async proxyBootstrap(newNode, inclCtrlr) {
|
|
1234
|
+
// This part is to be done before the interview
|
|
1235
|
+
const deviceClass = newNode.deviceClass;
|
|
1236
|
+
let lowSecurity = false;
|
|
1237
|
+
// Include using the default inclusion strategy:
|
|
1238
|
+
// * Use S2 if possible,
|
|
1239
|
+
// * only use S0 if necessary,
|
|
1240
|
+
// * use no encryption otherwise
|
|
1241
|
+
if (newNode.supportsCC(core_1.CommandClasses["Security 2"])) {
|
|
1242
|
+
await this.secureBootstrapS2(newNode);
|
|
1243
|
+
const actualSecurityClass = newNode.getHighestSecurityClass();
|
|
1244
|
+
if (actualSecurityClass == undefined ||
|
|
1245
|
+
actualSecurityClass < core_1.SecurityClass.S2_Unauthenticated) {
|
|
1246
|
+
lowSecurity = true;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
else if (newNode.supportsCC(core_1.CommandClasses.Security) &&
|
|
1250
|
+
(deviceClass.specific ?? deviceClass.generic).requiresSecurity) {
|
|
1251
|
+
// S0 bootstrapping is deferred to the inclusion controller
|
|
1252
|
+
this.driver.controllerLog.logNode(newNode.id, `Waiting for node ${inclCtrlr.id} to perform S0 bootstrapping...`);
|
|
1253
|
+
await inclCtrlr.commandClasses["Inclusion Controller"].initiateStep(newNode.id, cc_1.InclusionControllerStep.S0Inclusion);
|
|
1254
|
+
// Wait 60s for the S0 bootstrapping to complete
|
|
1255
|
+
const s0result = await this.driver
|
|
1256
|
+
.waitForCommand((cc) => cc.nodeId === inclCtrlr.id &&
|
|
1257
|
+
cc instanceof cc_1.InclusionControllerCCComplete &&
|
|
1258
|
+
cc.step === cc_1.InclusionControllerStep.S0Inclusion, 60000)
|
|
1259
|
+
.catch(() => undefined);
|
|
1260
|
+
this.driver.controllerLog.logNode(newNode.id, `S0 bootstrapping ${s0result == undefined
|
|
1261
|
+
? "timed out"
|
|
1262
|
+
: s0result.status === cc_1.InclusionControllerStatus.OK
|
|
1263
|
+
? "succeeded"
|
|
1264
|
+
: "failed"}`);
|
|
1265
|
+
// When bootstrapping with S0, no other keys are granted
|
|
1266
|
+
for (const secClass of core_1.securityClassOrder) {
|
|
1267
|
+
if (secClass !== core_1.SecurityClass.S0_Legacy) {
|
|
1268
|
+
newNode.securityClasses.set(secClass, false);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
// Whether the S0 key is granted depends on the result
|
|
1272
|
+
// received from the inclusion controller
|
|
1273
|
+
newNode.securityClasses.set(core_1.SecurityClass.S0_Legacy, s0result?.status === cc_1.InclusionControllerStatus.OK);
|
|
1274
|
+
lowSecurity = !newNode.hasSecurityClass(core_1.SecurityClass.S0_Legacy);
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
// Remember that no security classes were granted
|
|
1278
|
+
for (const secClass of core_1.securityClassOrder) {
|
|
1279
|
+
newNode.securityClasses.set(secClass, false);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return { lowSecurity };
|
|
1283
|
+
}
|
|
967
1284
|
async secureBootstrapS0(node, assumeSupported = false) {
|
|
968
1285
|
// When bootstrapping with S0, no other keys are granted
|
|
969
1286
|
for (const secClass of core_1.securityClassOrder) {
|
|
@@ -1237,13 +1554,21 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
1237
1554
|
const dsk = (0, core_1.dskToString)(nodePublicKey.slice(0, 16)).slice(5);
|
|
1238
1555
|
// The time the user has to enter the PIN is limited by the timeout TAI2
|
|
1239
1556
|
const tai2RemainingMs = cc_1.inclusionTimeouts.TAI2 - (Date.now() - timerStartTAI2);
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1557
|
+
let pinResult;
|
|
1558
|
+
if ("dsk" in inclusionOptions &&
|
|
1559
|
+
typeof inclusionOptions.dsk === "string" &&
|
|
1560
|
+
(0, core_1.isValidDSK)(inclusionOptions.dsk)) {
|
|
1561
|
+
pinResult = inclusionOptions.dsk.slice(0, 5);
|
|
1562
|
+
}
|
|
1563
|
+
else {
|
|
1564
|
+
pinResult = await Promise.race([
|
|
1565
|
+
(0, async_1.wait)(tai2RemainingMs, true).then(() => false),
|
|
1566
|
+
userCallbacks
|
|
1567
|
+
.validateDSKAndEnterPIN(dsk)
|
|
1568
|
+
// ignore errors in application callbacks
|
|
1569
|
+
.catch(() => false),
|
|
1570
|
+
]);
|
|
1571
|
+
}
|
|
1247
1572
|
if (typeof pinResult !== "string" ||
|
|
1248
1573
|
!/^\d{5}$/.test(pinResult)) {
|
|
1249
1574
|
// There was a timeout, the user did not confirm the DSK or entered an invalid PIN
|
|
@@ -2442,6 +2767,7 @@ ${associatedNodes.join(", ")}`,
|
|
|
2442
2767
|
}
|
|
2443
2768
|
if (result.success)
|
|
2444
2769
|
await this.driver.trySoftReset();
|
|
2770
|
+
this._rfRegion = region;
|
|
2445
2771
|
return result.success;
|
|
2446
2772
|
}
|
|
2447
2773
|
/** Request the current RF region configured at the Z-Wave API Module */
|
|
@@ -2450,6 +2776,7 @@ ${associatedNodes.join(", ")}`,
|
|
|
2450
2776
|
if (result instanceof SerialAPISetupMessages_1.SerialAPISetup_CommandUnsupportedResponse) {
|
|
2451
2777
|
throw new core_1.ZWaveError(`Your hardware does not support getting the RF region!`, core_1.ZWaveErrorCodes.Driver_NotSupported);
|
|
2452
2778
|
}
|
|
2779
|
+
this._rfRegion = result.region;
|
|
2453
2780
|
return result.region;
|
|
2454
2781
|
}
|
|
2455
2782
|
/** Configure the Powerlevel setting of the Z-Wave API */
|
|
@@ -2586,6 +2913,67 @@ ${associatedNodes.join(", ")}`,
|
|
|
2586
2913
|
return false;
|
|
2587
2914
|
}
|
|
2588
2915
|
}
|
|
2916
|
+
/**
|
|
2917
|
+
* **Z-Wave 500 series only**
|
|
2918
|
+
*
|
|
2919
|
+
* Initialize the Firmware Update functionality and determine if the firmware can be updated.
|
|
2920
|
+
*/
|
|
2921
|
+
async firmwareUpdateNVMInit() {
|
|
2922
|
+
const ret = await this.driver.sendMessage(new FirmwareUpdateNVMMessages_1.FirmwareUpdateNVM_InitRequest(this.driver));
|
|
2923
|
+
return ret.supported;
|
|
2924
|
+
}
|
|
2925
|
+
/**
|
|
2926
|
+
* **Z-Wave 500 series only**
|
|
2927
|
+
*
|
|
2928
|
+
* Set the NEWIMAGE marker in the NVM (to the given value), which is used to signal that a new firmware image is present
|
|
2929
|
+
*/
|
|
2930
|
+
async firmwareUpdateNVMSetNewImage(value = true) {
|
|
2931
|
+
await this.driver.sendMessage(new FirmwareUpdateNVMMessages_1.FirmwareUpdateNVM_SetNewImageRequest(this.driver, {
|
|
2932
|
+
newImage: value,
|
|
2933
|
+
}));
|
|
2934
|
+
}
|
|
2935
|
+
/**
|
|
2936
|
+
* **Z-Wave 500 series only**
|
|
2937
|
+
*
|
|
2938
|
+
* Return the value of the NEWIMAGE marker in the NVM, which is used to signal that a new firmware image is present
|
|
2939
|
+
*/
|
|
2940
|
+
async firmwareUpdateNVMGetNewImage() {
|
|
2941
|
+
const ret = await this.driver.sendMessage(new FirmwareUpdateNVMMessages_1.FirmwareUpdateNVM_GetNewImageRequest(this.driver));
|
|
2942
|
+
return ret.newImage;
|
|
2943
|
+
}
|
|
2944
|
+
/**
|
|
2945
|
+
* **Z-Wave 500 series only**
|
|
2946
|
+
*
|
|
2947
|
+
* Calculates the CRC-16 for the specified block of data in the NVM
|
|
2948
|
+
*/
|
|
2949
|
+
async firmwareUpdateNVMUpdateCRC16(offset, blockLength, crcSeed) {
|
|
2950
|
+
const ret = await this.driver.sendMessage(new FirmwareUpdateNVMMessages_1.FirmwareUpdateNVM_UpdateCRC16Request(this.driver, {
|
|
2951
|
+
offset,
|
|
2952
|
+
blockLength,
|
|
2953
|
+
crcSeed,
|
|
2954
|
+
}));
|
|
2955
|
+
return ret.crc16;
|
|
2956
|
+
}
|
|
2957
|
+
/**
|
|
2958
|
+
* **Z-Wave 500 series only**
|
|
2959
|
+
*
|
|
2960
|
+
* Writes the given data into the firmware update region of the NVM.
|
|
2961
|
+
*/
|
|
2962
|
+
async firmwareUpdateNVMWrite(offset, buffer) {
|
|
2963
|
+
await this.driver.sendMessage(new FirmwareUpdateNVMMessages_1.FirmwareUpdateNVM_WriteRequest(this.driver, {
|
|
2964
|
+
offset,
|
|
2965
|
+
buffer,
|
|
2966
|
+
}));
|
|
2967
|
+
}
|
|
2968
|
+
/**
|
|
2969
|
+
* **Z-Wave 500 series only**
|
|
2970
|
+
*
|
|
2971
|
+
* Checks if the firmware present in the NVM is valid
|
|
2972
|
+
*/
|
|
2973
|
+
async firmwareUpdateNVMIsValidCRC16() {
|
|
2974
|
+
const ret = await this.driver.sendMessage(new FirmwareUpdateNVMMessages_1.FirmwareUpdateNVM_IsValidCRC16Request(this.driver));
|
|
2975
|
+
return ret.isValid;
|
|
2976
|
+
}
|
|
2589
2977
|
/**
|
|
2590
2978
|
* **Z-Wave 500 series only**
|
|
2591
2979
|
*
|
|
@@ -3035,6 +3423,7 @@ ${associatedNodes.join(", ")}`,
|
|
|
3035
3423
|
productType,
|
|
3036
3424
|
productId,
|
|
3037
3425
|
firmwareVersion,
|
|
3426
|
+
rfRegion: this.rfRegion,
|
|
3038
3427
|
}, {
|
|
3039
3428
|
userAgent: this.driver.getUserAgentStringWithComponents(options?.additionalUserAgentComponents),
|
|
3040
3429
|
apiKey: options?.apiKey ??
|
|
@@ -3070,6 +3459,18 @@ ${associatedNodes.join(", ")}`,
|
|
|
3070
3459
|
* @deprecated Use {@link firmwareUpdateOTA} instead, which properly handles multi-target updates
|
|
3071
3460
|
*/
|
|
3072
3461
|
async beginOTAFirmwareUpdate(nodeId, update) {
|
|
3462
|
+
// Don't let two firmware updates happen in parallel
|
|
3463
|
+
if (this.isAnyOTAFirmwareUpdateInProgress()) {
|
|
3464
|
+
const message = `Failed to start the update: A firmware update is already in progress on this network!`;
|
|
3465
|
+
this.driver.controllerLog.print(message, "error");
|
|
3466
|
+
throw new core_1.ZWaveError(message, core_1.ZWaveErrorCodes.FirmwareUpdateCC_NetworkBusy);
|
|
3467
|
+
}
|
|
3468
|
+
// Don't allow updating firmware when the controller is currently updating its own firmware
|
|
3469
|
+
if (this.isFirmwareUpdateInProgress()) {
|
|
3470
|
+
const message = `Failed to start the update: The controller is currently being updated!`;
|
|
3471
|
+
this.driver.controllerLog.print(message, "error");
|
|
3472
|
+
throw new core_1.ZWaveError(message, core_1.ZWaveErrorCodes.FirmwareUpdateCC_NetworkBusy);
|
|
3473
|
+
}
|
|
3073
3474
|
const node = this.nodes.getOrThrow(nodeId);
|
|
3074
3475
|
let firmware;
|
|
3075
3476
|
try {
|
|
@@ -3111,6 +3512,18 @@ ${associatedNodes.join(", ")}`,
|
|
|
3111
3512
|
if (updates.length === 0) {
|
|
3112
3513
|
throw new core_1.ZWaveError(`At least one update must be provided`, core_1.ZWaveErrorCodes.Argument_Invalid);
|
|
3113
3514
|
}
|
|
3515
|
+
// Don't let two firmware updates happen in parallel
|
|
3516
|
+
if (this.isAnyOTAFirmwareUpdateInProgress()) {
|
|
3517
|
+
const message = `Failed to start the update: A firmware update is already in progress on this network!`;
|
|
3518
|
+
this.driver.controllerLog.print(message, "error");
|
|
3519
|
+
throw new core_1.ZWaveError(message, core_1.ZWaveErrorCodes.FirmwareUpdateCC_NetworkBusy);
|
|
3520
|
+
}
|
|
3521
|
+
// Don't allow updating firmware when the controller is currently updating its own firmware
|
|
3522
|
+
if (this.isFirmwareUpdateInProgress()) {
|
|
3523
|
+
const message = `Failed to start the update: The controller is currently being updated!`;
|
|
3524
|
+
this.driver.controllerLog.print(message, "error");
|
|
3525
|
+
throw new core_1.ZWaveError(message, core_1.ZWaveErrorCodes.FirmwareUpdateCC_NetworkBusy);
|
|
3526
|
+
}
|
|
3114
3527
|
const node = this.nodes.getOrThrow(nodeId);
|
|
3115
3528
|
this.driver.controllerLog.logNode(nodeId, `OTA firmware update started, downloading ${updates.length} updates...`);
|
|
3116
3529
|
const loglevel = this.driver.getLogConfig().level;
|
|
@@ -3154,6 +3567,242 @@ ${associatedNodes.join(", ")}`,
|
|
|
3154
3567
|
this.driver.controllerLog.logNode(nodeId, `All updates downloaded, installing...`);
|
|
3155
3568
|
return node.updateFirmware(firmwares);
|
|
3156
3569
|
}
|
|
3570
|
+
/**
|
|
3571
|
+
* Returns whether a firmware update is in progress for the controller.
|
|
3572
|
+
*/
|
|
3573
|
+
isFirmwareUpdateInProgress() {
|
|
3574
|
+
return this._firmwareUpdateInProgress;
|
|
3575
|
+
}
|
|
3576
|
+
/**
|
|
3577
|
+
* Updates the firmware of the controller using the given firmware file.
|
|
3578
|
+
*
|
|
3579
|
+
* The return value indicates whether the update was successful.
|
|
3580
|
+
* **WARNING:** After a successful update, the Z-Wave driver will destroy itself so it can be restarted.
|
|
3581
|
+
*
|
|
3582
|
+
* **WARNING:** A failure during this process may put your controller in recovery mode, rendering it unusable until a correct firmware image is uploaded. Use at your own risk!
|
|
3583
|
+
*/
|
|
3584
|
+
async firmwareUpdateOTW(data) {
|
|
3585
|
+
// Don't let two firmware updates happen in parallel
|
|
3586
|
+
if (this.isAnyOTAFirmwareUpdateInProgress()) {
|
|
3587
|
+
const message = `Failed to start the update: A firmware update is already in progress on this network!`;
|
|
3588
|
+
this.driver.controllerLog.print(message, "error");
|
|
3589
|
+
throw new core_1.ZWaveError(message, core_1.ZWaveErrorCodes.OTW_Update_Busy);
|
|
3590
|
+
}
|
|
3591
|
+
// Don't allow updating firmware when the controller is currently updating its own firmware
|
|
3592
|
+
if (this.isFirmwareUpdateInProgress()) {
|
|
3593
|
+
const message = `Failed to start the update: The controller is currently being updated!`;
|
|
3594
|
+
this.driver.controllerLog.print(message, "error");
|
|
3595
|
+
throw new core_1.ZWaveError(message, core_1.ZWaveErrorCodes.OTW_Update_Busy);
|
|
3596
|
+
}
|
|
3597
|
+
if (this.sdkVersionGte("7.0")) {
|
|
3598
|
+
return this.firmwareUpdateOTW700(data);
|
|
3599
|
+
}
|
|
3600
|
+
else if (this.sdkVersionGte("6.50.0") &&
|
|
3601
|
+
this.supportedFunctionTypes?.includes(serial_1.FunctionType.FirmwareUpdateNVM)) {
|
|
3602
|
+
// This is 500 series
|
|
3603
|
+
const wasUpdated = await this.firmwareUpdateOTW500(data);
|
|
3604
|
+
if (wasUpdated) {
|
|
3605
|
+
// After updating the firmware on 500 series sticks, we MUST soft-reset them
|
|
3606
|
+
await this.driver.softResetAndRestart("Activating new firmware and restarting driver...", "Controller firmware updates require a driver restart!");
|
|
3607
|
+
}
|
|
3608
|
+
return wasUpdated;
|
|
3609
|
+
}
|
|
3610
|
+
else {
|
|
3611
|
+
throw new core_1.ZWaveError(`Firmware updates are not supported on this controller`, core_1.ZWaveErrorCodes.Controller_NotSupported);
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
async firmwareUpdateOTW500(data) {
|
|
3615
|
+
this._firmwareUpdateInProgress = true;
|
|
3616
|
+
let turnedRadioOff = false;
|
|
3617
|
+
try {
|
|
3618
|
+
this.driver.controllerLog.print("Beginning firmware update");
|
|
3619
|
+
const canUpdate = await this.firmwareUpdateNVMInit();
|
|
3620
|
+
if (!canUpdate) {
|
|
3621
|
+
this.driver.controllerLog.print("OTW update failed: This controller does not support firmware updates", "error");
|
|
3622
|
+
this.emit("firmware update finished", {
|
|
3623
|
+
success: false,
|
|
3624
|
+
status: _Types_3.ControllerFirmwareUpdateStatus.Error_NotSupported,
|
|
3625
|
+
});
|
|
3626
|
+
return false;
|
|
3627
|
+
}
|
|
3628
|
+
// Avoid interruption by incoming messages
|
|
3629
|
+
await this.toggleRF(false);
|
|
3630
|
+
turnedRadioOff = true;
|
|
3631
|
+
// Upload the firmware data
|
|
3632
|
+
const BLOCK_SIZE = 64;
|
|
3633
|
+
const numFragments = Math.ceil(data.length / BLOCK_SIZE);
|
|
3634
|
+
for (let fragment = 0; fragment < numFragments; fragment++) {
|
|
3635
|
+
const fragmentData = data.slice(fragment * BLOCK_SIZE, (fragment + 1) * BLOCK_SIZE);
|
|
3636
|
+
await this.firmwareUpdateNVMWrite(fragment * BLOCK_SIZE, fragmentData);
|
|
3637
|
+
// This progress is technically too low, but we can keep 100% for after CRC checking this way
|
|
3638
|
+
const progress = {
|
|
3639
|
+
sentFragments: fragment,
|
|
3640
|
+
totalFragments: numFragments,
|
|
3641
|
+
progress: (0, math_1.roundTo)((fragment / numFragments) * 100, 2),
|
|
3642
|
+
};
|
|
3643
|
+
this.emit("firmware update progress", progress);
|
|
3644
|
+
}
|
|
3645
|
+
// Check if a valid image was written
|
|
3646
|
+
const isValidCRC = await this.firmwareUpdateNVMIsValidCRC16();
|
|
3647
|
+
if (!isValidCRC) {
|
|
3648
|
+
this.driver.controllerLog.print("OTW update failed: The firmware image is invalid", "error");
|
|
3649
|
+
this.emit("firmware update finished", {
|
|
3650
|
+
success: false,
|
|
3651
|
+
status: _Types_3.ControllerFirmwareUpdateStatus.Error_Aborted,
|
|
3652
|
+
});
|
|
3653
|
+
return false;
|
|
3654
|
+
}
|
|
3655
|
+
this.emit("firmware update progress", {
|
|
3656
|
+
sentFragments: numFragments,
|
|
3657
|
+
totalFragments: numFragments,
|
|
3658
|
+
progress: 100,
|
|
3659
|
+
});
|
|
3660
|
+
// Enable the image
|
|
3661
|
+
await this.firmwareUpdateNVMSetNewImage();
|
|
3662
|
+
this.driver.controllerLog.print("Firmware update succeeded");
|
|
3663
|
+
this.emit("firmware update finished", {
|
|
3664
|
+
success: true,
|
|
3665
|
+
status: _Types_3.ControllerFirmwareUpdateStatus.OK,
|
|
3666
|
+
});
|
|
3667
|
+
return true;
|
|
3668
|
+
}
|
|
3669
|
+
finally {
|
|
3670
|
+
this._firmwareUpdateInProgress = false;
|
|
3671
|
+
if (turnedRadioOff)
|
|
3672
|
+
await this.toggleRF(true);
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
async firmwareUpdateOTW700(data) {
|
|
3676
|
+
this._firmwareUpdateInProgress = true;
|
|
3677
|
+
let destroy = false;
|
|
3678
|
+
try {
|
|
3679
|
+
if (!this.driver.isInBootloader()) {
|
|
3680
|
+
await this.driver.enterBootloader();
|
|
3681
|
+
}
|
|
3682
|
+
// Start the update process
|
|
3683
|
+
this.driver.controllerLog.print("Beginning firmware upload");
|
|
3684
|
+
await this.driver.bootloader.beginUpload();
|
|
3685
|
+
// Wait for the bootloader to accept fragments
|
|
3686
|
+
try {
|
|
3687
|
+
await this.driver.waitForBootloaderChunk((c) => c.type === serial_1.BootloaderChunkType.Message &&
|
|
3688
|
+
c.message === "begin upload", 5000);
|
|
3689
|
+
await this.driver.waitForBootloaderChunk((c) => c.type === serial_1.BootloaderChunkType.FlowControl &&
|
|
3690
|
+
c.command === serial_1.XModemMessageHeaders.C, 1000);
|
|
3691
|
+
}
|
|
3692
|
+
catch {
|
|
3693
|
+
this.driver.controllerLog.print("OTW update failed: Expected response not received from the bootloader", "error");
|
|
3694
|
+
this.emit("firmware update finished", {
|
|
3695
|
+
success: false,
|
|
3696
|
+
status: _Types_3.ControllerFirmwareUpdateStatus.Error_Timeout,
|
|
3697
|
+
});
|
|
3698
|
+
return false;
|
|
3699
|
+
}
|
|
3700
|
+
const BLOCK_SIZE = 128;
|
|
3701
|
+
if (data.length % BLOCK_SIZE !== 0) {
|
|
3702
|
+
// Pad the data to a multiple of BLOCK_SIZE
|
|
3703
|
+
data = Buffer.concat([
|
|
3704
|
+
data,
|
|
3705
|
+
Buffer.alloc(BLOCK_SIZE - (data.length % BLOCK_SIZE), 0xff),
|
|
3706
|
+
]);
|
|
3707
|
+
}
|
|
3708
|
+
const numFragments = Math.ceil(data.length / BLOCK_SIZE);
|
|
3709
|
+
let aborted = false;
|
|
3710
|
+
transfer: for (let fragment = 1; fragment <= numFragments; fragment++) {
|
|
3711
|
+
const fragmentData = data.slice((fragment - 1) * BLOCK_SIZE, fragment * BLOCK_SIZE);
|
|
3712
|
+
retry: for (let retry = 0; retry < 3; retry++) {
|
|
3713
|
+
await this.driver.bootloader.uploadFragment(fragment, fragmentData);
|
|
3714
|
+
let result;
|
|
3715
|
+
try {
|
|
3716
|
+
result = await this.driver.waitForBootloaderChunk((c) => c.type === serial_1.BootloaderChunkType.FlowControl, 1000);
|
|
3717
|
+
}
|
|
3718
|
+
catch (e) {
|
|
3719
|
+
this.driver.controllerLog.print("OTW update failed: The bootloader did not acknowledge the start of transfer.", "error");
|
|
3720
|
+
this.emit("firmware update finished", {
|
|
3721
|
+
success: false,
|
|
3722
|
+
status: _Types_3.ControllerFirmwareUpdateStatus.Error_Timeout,
|
|
3723
|
+
});
|
|
3724
|
+
return false;
|
|
3725
|
+
}
|
|
3726
|
+
switch (result.command) {
|
|
3727
|
+
case serial_1.XModemMessageHeaders.ACK: {
|
|
3728
|
+
// The fragment was accepted
|
|
3729
|
+
const progress = {
|
|
3730
|
+
sentFragments: fragment,
|
|
3731
|
+
totalFragments: numFragments,
|
|
3732
|
+
progress: (0, math_1.roundTo)((fragment / numFragments) * 100, 2),
|
|
3733
|
+
};
|
|
3734
|
+
this.emit("firmware update progress", progress);
|
|
3735
|
+
// we've transmitted at least one fragment, so we need to destroy the driver afterwards
|
|
3736
|
+
destroy = true;
|
|
3737
|
+
continue transfer;
|
|
3738
|
+
}
|
|
3739
|
+
case serial_1.XModemMessageHeaders.NAK:
|
|
3740
|
+
// The fragment was rejected, try again
|
|
3741
|
+
continue retry;
|
|
3742
|
+
case serial_1.XModemMessageHeaders.CAN:
|
|
3743
|
+
// The bootloader aborted the update. We'll receive the reason afterwards as a message
|
|
3744
|
+
aborted = true;
|
|
3745
|
+
break transfer;
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
this.driver.controllerLog.print("OTW update failed: Maximum retry attempts reached", "error");
|
|
3749
|
+
this.emit("firmware update finished", {
|
|
3750
|
+
success: false,
|
|
3751
|
+
status: _Types_3.ControllerFirmwareUpdateStatus.Error_RetryLimitReached,
|
|
3752
|
+
});
|
|
3753
|
+
return false;
|
|
3754
|
+
}
|
|
3755
|
+
if (aborted) {
|
|
3756
|
+
// wait for the reason to craft a good error message
|
|
3757
|
+
const error = await this.driver
|
|
3758
|
+
.waitForBootloaderChunk((c) => c.type === serial_1.BootloaderChunkType.Message &&
|
|
3759
|
+
c.message.includes("error 0x"), 1000)
|
|
3760
|
+
.catch(() => undefined);
|
|
3761
|
+
// wait for the menu screen so it doesn't show up in logs
|
|
3762
|
+
await this.driver
|
|
3763
|
+
.waitForBootloaderChunk((c) => c.type === serial_1.BootloaderChunkType.Menu, 1000)
|
|
3764
|
+
.catch(() => undefined);
|
|
3765
|
+
let message = `OTW update was aborted by the bootloader.`;
|
|
3766
|
+
if (error) {
|
|
3767
|
+
message += ` ${error.message}`;
|
|
3768
|
+
// TODO: parse error code
|
|
3769
|
+
}
|
|
3770
|
+
this.driver.controllerLog.print(message, "error");
|
|
3771
|
+
this.emit("firmware update finished", {
|
|
3772
|
+
success: false,
|
|
3773
|
+
status: _Types_3.ControllerFirmwareUpdateStatus.Error_Aborted,
|
|
3774
|
+
});
|
|
3775
|
+
return false;
|
|
3776
|
+
}
|
|
3777
|
+
else {
|
|
3778
|
+
// We're done, send EOT and wait for the menu screen
|
|
3779
|
+
await this.driver.bootloader.finishUpload();
|
|
3780
|
+
try {
|
|
3781
|
+
await this.driver.waitForBootloaderChunk((c) => c.type === serial_1.BootloaderChunkType.Message &&
|
|
3782
|
+
c.message.includes("upload complete"), 1000);
|
|
3783
|
+
await this.driver.waitForBootloaderChunk((c) => c.type === serial_1.BootloaderChunkType.Menu, 1000);
|
|
3784
|
+
}
|
|
3785
|
+
catch (e) {
|
|
3786
|
+
this.driver.controllerLog.print("OTW update failed: The bootloader did not acknowledge the end of transfer.", "error");
|
|
3787
|
+
this.emit("firmware update finished", {
|
|
3788
|
+
success: false,
|
|
3789
|
+
status: _Types_3.ControllerFirmwareUpdateStatus.Error_Timeout,
|
|
3790
|
+
});
|
|
3791
|
+
return false;
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
this.driver.controllerLog.print("Firmware update succeeded");
|
|
3795
|
+
this.emit("firmware update finished", {
|
|
3796
|
+
success: true,
|
|
3797
|
+
status: _Types_3.ControllerFirmwareUpdateStatus.OK,
|
|
3798
|
+
});
|
|
3799
|
+
return true;
|
|
3800
|
+
}
|
|
3801
|
+
finally {
|
|
3802
|
+
await this.driver.leaveBootloader(destroy);
|
|
3803
|
+
this._firmwareUpdateInProgress = false;
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3157
3806
|
};
|
|
3158
3807
|
ZWaveController = __decorate([
|
|
3159
3808
|
(0, shared_1.Mixin)([ControllerStatistics_1.ControllerStatisticsHost])
|