zwave-js 13.2.0 → 13.3.1
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/bin/mock-server.js +2 -0
- package/build/lib/controller/Controller.d.ts +119 -9
- package/build/lib/controller/Controller.d.ts.map +1 -1
- package/build/lib/controller/Controller.js +1187 -81
- package/build/lib/controller/Controller.js.map +1 -1
- package/build/lib/controller/Inclusion.d.ts +44 -0
- package/build/lib/controller/Inclusion.d.ts.map +1 -1
- package/build/lib/controller/Inclusion.js +42 -1
- package/build/lib/controller/Inclusion.js.map +1 -1
- package/build/lib/driver/Driver.d.ts +16 -1
- package/build/lib/driver/Driver.d.ts.map +1 -1
- package/build/lib/driver/Driver.js +290 -97
- package/build/lib/driver/Driver.js.map +1 -1
- package/build/lib/driver/NetworkCache.d.ts +3 -0
- package/build/lib/driver/NetworkCache.d.ts.map +1 -1
- package/build/lib/driver/NetworkCache.js +37 -9
- package/build/lib/driver/NetworkCache.js.map +1 -1
- package/build/lib/driver/ZWaveOptions.d.ts +10 -3
- package/build/lib/driver/ZWaveOptions.d.ts.map +1 -1
- package/build/lib/driver/ZWaveOptions.js.map +1 -1
- package/build/lib/node/Node.d.ts +4 -3
- package/build/lib/node/Node.d.ts.map +1 -1
- package/build/lib/node/Node.js +130 -34
- package/build/lib/node/Node.js.map +1 -1
- package/build/lib/serialapi/application/ApplicationUpdateRequest.d.ts +5 -0
- package/build/lib/serialapi/application/ApplicationUpdateRequest.d.ts.map +1 -1
- package/build/lib/serialapi/application/ApplicationUpdateRequest.js +24 -1
- package/build/lib/serialapi/application/ApplicationUpdateRequest.js.map +1 -1
- package/build/lib/serialapi/capability/GetControllerCapabilitiesMessages.d.ts +2 -0
- package/build/lib/serialapi/capability/GetControllerCapabilitiesMessages.d.ts.map +1 -1
- package/build/lib/serialapi/capability/GetControllerCapabilitiesMessages.js +6 -0
- package/build/lib/serialapi/capability/GetControllerCapabilitiesMessages.js.map +1 -1
- package/build/lib/serialapi/capability/GetSerialApiInitDataMessages.d.ts +2 -9
- package/build/lib/serialapi/capability/GetSerialApiInitDataMessages.d.ts.map +1 -1
- package/build/lib/serialapi/capability/GetSerialApiInitDataMessages.js.map +1 -1
- package/build/lib/serialapi/capability/SerialAPISetupMessages.d.ts +27 -1
- package/build/lib/serialapi/capability/SerialAPISetupMessages.d.ts.map +1 -1
- package/build/lib/serialapi/capability/SerialAPISetupMessages.js +96 -1
- package/build/lib/serialapi/capability/SerialAPISetupMessages.js.map +1 -1
- package/build/lib/serialapi/network-mgmt/SetLearnModeMessages.d.ts +47 -0
- package/build/lib/serialapi/network-mgmt/SetLearnModeMessages.d.ts.map +1 -0
- package/build/lib/serialapi/network-mgmt/SetLearnModeMessages.js +137 -0
- package/build/lib/serialapi/network-mgmt/SetLearnModeMessages.js.map +1 -0
- package/build/lib/serialapi/nvm/ExtendedNVMOperationsMessages.d.ts +60 -0
- package/build/lib/serialapi/nvm/ExtendedNVMOperationsMessages.d.ts.map +1 -0
- package/build/lib/serialapi/nvm/ExtendedNVMOperationsMessages.js +201 -0
- package/build/lib/serialapi/nvm/ExtendedNVMOperationsMessages.js.map +1 -0
- package/package.json +9 -9
|
@@ -21,7 +21,6 @@ const deferred_promise_1 = require("alcalzone-shared/deferred-promise");
|
|
|
21
21
|
const math_1 = require("alcalzone-shared/math");
|
|
22
22
|
const typeguards_1 = require("alcalzone-shared/typeguards");
|
|
23
23
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
24
|
-
const node_util_1 = __importDefault(require("node:util"));
|
|
25
24
|
const NetworkCache_1 = require("../driver/NetworkCache");
|
|
26
25
|
const DeviceClass_1 = require("../node/DeviceClass");
|
|
27
26
|
const Node_1 = require("../node/Node");
|
|
@@ -60,12 +59,14 @@ const RemoveFailedNodeMessages_1 = require("../serialapi/network-mgmt/RemoveFail
|
|
|
60
59
|
const RemoveNodeFromNetworkRequest_1 = require("../serialapi/network-mgmt/RemoveNodeFromNetworkRequest");
|
|
61
60
|
const ReplaceFailedNodeRequest_1 = require("../serialapi/network-mgmt/ReplaceFailedNodeRequest");
|
|
62
61
|
const RequestNodeNeighborUpdateMessages_1 = require("../serialapi/network-mgmt/RequestNodeNeighborUpdateMessages");
|
|
62
|
+
const SetLearnModeMessages_1 = require("../serialapi/network-mgmt/SetLearnModeMessages");
|
|
63
63
|
const SetPriorityRouteMessages_1 = require("../serialapi/network-mgmt/SetPriorityRouteMessages");
|
|
64
64
|
const SetSUCNodeIDMessages_1 = require("../serialapi/network-mgmt/SetSUCNodeIDMessages");
|
|
65
65
|
const ExtNVMReadLongBufferMessages_1 = require("../serialapi/nvm/ExtNVMReadLongBufferMessages");
|
|
66
66
|
const ExtNVMReadLongByteMessages_1 = require("../serialapi/nvm/ExtNVMReadLongByteMessages");
|
|
67
67
|
const ExtNVMWriteLongBufferMessages_1 = require("../serialapi/nvm/ExtNVMWriteLongBufferMessages");
|
|
68
68
|
const ExtNVMWriteLongByteMessages_1 = require("../serialapi/nvm/ExtNVMWriteLongByteMessages");
|
|
69
|
+
const ExtendedNVMOperationsMessages_1 = require("../serialapi/nvm/ExtendedNVMOperationsMessages");
|
|
69
70
|
const FirmwareUpdateNVMMessages_1 = require("../serialapi/nvm/FirmwareUpdateNVMMessages");
|
|
70
71
|
const GetNVMIdMessages_1 = require("../serialapi/nvm/GetNVMIdMessages");
|
|
71
72
|
const NVMOperationsMessages_1 = require("../serialapi/nvm/NVMOperationsMessages");
|
|
@@ -93,6 +94,7 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
93
94
|
driver.registerRequestHandler(serial_1.FunctionType.AddNodeToNetwork, this.handleAddNodeStatusReport.bind(this));
|
|
94
95
|
driver.registerRequestHandler(serial_1.FunctionType.RemoveNodeFromNetwork, this.handleRemoveNodeStatusReport.bind(this));
|
|
95
96
|
driver.registerRequestHandler(serial_1.FunctionType.ReplaceFailedNode, this.handleReplaceNodeStatusReport.bind(this));
|
|
97
|
+
driver.registerRequestHandler(serial_1.FunctionType.SetLearnMode, this.handleLearnModeCallback.bind(this));
|
|
96
98
|
}
|
|
97
99
|
_type;
|
|
98
100
|
get type() {
|
|
@@ -124,11 +126,36 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
124
126
|
get ownNodeId() {
|
|
125
127
|
return this._ownNodeId;
|
|
126
128
|
}
|
|
127
|
-
|
|
129
|
+
_dsk;
|
|
130
|
+
/**
|
|
131
|
+
* The device specific key (DSK) of the controller in binary format.
|
|
132
|
+
*/
|
|
133
|
+
get dsk() {
|
|
134
|
+
if (this._dsk == undefined) {
|
|
135
|
+
const keyPair = this.driver.getLearnModeAuthenticatedKeyPair();
|
|
136
|
+
const publicKey = (0, core_1.extractRawECDHPublicKey)(keyPair.publicKey);
|
|
137
|
+
this._dsk = publicKey.subarray(0, 16);
|
|
138
|
+
}
|
|
139
|
+
return this._dsk;
|
|
140
|
+
}
|
|
141
|
+
/** @deprecated Use {@link role} instead */
|
|
128
142
|
get isPrimary() {
|
|
129
|
-
|
|
143
|
+
switch (this.role) {
|
|
144
|
+
case core_1.NOT_KNOWN:
|
|
145
|
+
return core_1.NOT_KNOWN;
|
|
146
|
+
case core_1.ControllerRole.Primary:
|
|
147
|
+
return true;
|
|
148
|
+
default:
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
130
151
|
}
|
|
152
|
+
// This seems odd, but isPrimary comes from the Serial API init data command,
|
|
153
|
+
// while isSecondary comes from the GetControllerCapabilities command. They don't really do what their name implies
|
|
154
|
+
// and sometimes contradict each other...
|
|
155
|
+
_isPrimary;
|
|
156
|
+
_isSecondary;
|
|
131
157
|
_isUsingHomeIdFromOtherNetwork;
|
|
158
|
+
/** @deprecated Use {@link role} instead */
|
|
132
159
|
get isUsingHomeIdFromOtherNetwork() {
|
|
133
160
|
return this._isUsingHomeIdFromOtherNetwork;
|
|
134
161
|
}
|
|
@@ -137,6 +164,7 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
137
164
|
return this._isSISPresent;
|
|
138
165
|
}
|
|
139
166
|
_wasRealPrimary;
|
|
167
|
+
/** @deprecated Use {@link role} instead */
|
|
140
168
|
get wasRealPrimary() {
|
|
141
169
|
return this._wasRealPrimary;
|
|
142
170
|
}
|
|
@@ -148,6 +176,7 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
148
176
|
get isSUC() {
|
|
149
177
|
return this._isSUC;
|
|
150
178
|
}
|
|
179
|
+
_noNodesIncluded;
|
|
151
180
|
_nodeType;
|
|
152
181
|
get nodeType() {
|
|
153
182
|
return this._nodeType;
|
|
@@ -269,6 +298,11 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
269
298
|
get supportsTimers() {
|
|
270
299
|
return this._supportsTimers;
|
|
271
300
|
}
|
|
301
|
+
_supportedRegions;
|
|
302
|
+
/** Which RF regions are supported by the controller, including information about them */
|
|
303
|
+
get supportedRegions() {
|
|
304
|
+
return this._supportedRegions;
|
|
305
|
+
}
|
|
272
306
|
_rfRegion;
|
|
273
307
|
/** 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}. */
|
|
274
308
|
get rfRegion() {
|
|
@@ -363,6 +397,34 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
363
397
|
set powerlevel(value) {
|
|
364
398
|
this._powerlevel = value;
|
|
365
399
|
}
|
|
400
|
+
/** The role of the controller on the network */
|
|
401
|
+
get role() {
|
|
402
|
+
if (this._wasRealPrimary)
|
|
403
|
+
return core_1.ControllerRole.Primary;
|
|
404
|
+
// Ideally we'd rely on wasRealPrimary, but there are some older controllers out there where this flag isn't set.
|
|
405
|
+
if (this._isPrimary && this._isSIS && this._isSecondary === false) {
|
|
406
|
+
return core_1.ControllerRole.Primary;
|
|
407
|
+
}
|
|
408
|
+
switch (this._isSecondary) {
|
|
409
|
+
case true:
|
|
410
|
+
return core_1.ControllerRole.Secondary;
|
|
411
|
+
case false:
|
|
412
|
+
return core_1.ControllerRole.Inclusion;
|
|
413
|
+
default:
|
|
414
|
+
return core_1.NOT_KNOWN;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/** Returns whether learn mode may be enabled on this controller */
|
|
418
|
+
get isLearnModePermitted() {
|
|
419
|
+
// The primary controller may only enter learn mode, if hasn't included nodes yet
|
|
420
|
+
if (this.role === core_1.ControllerRole.Primary) {
|
|
421
|
+
return !!this._noNodesIncluded;
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
// Secondary controllers may only enter learn mode if they are not the SUC
|
|
425
|
+
return this._isSUC === false;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
366
428
|
/**
|
|
367
429
|
* @internal
|
|
368
430
|
* Remembers the indicator values set by another node
|
|
@@ -549,28 +611,7 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
549
611
|
.map((fn) => `\n · ${serial_1.FunctionType[fn]} (${(0, shared_1.num2hex)(fn)})`)
|
|
550
612
|
.join("")}`);
|
|
551
613
|
// Request additional information about the controller/Z-Wave chip
|
|
552
|
-
this.
|
|
553
|
-
const initData = await this.driver.sendMessage(new GetSerialApiInitDataMessages_1.GetSerialApiInitDataRequest(this.driver));
|
|
554
|
-
// and remember the new info
|
|
555
|
-
this._zwaveApiVersion = initData.zwaveApiVersion;
|
|
556
|
-
this._zwaveChipType = initData.zwaveChipType;
|
|
557
|
-
this._isPrimary = initData.isPrimary;
|
|
558
|
-
this._isSIS = initData.isSIS;
|
|
559
|
-
this._nodeType = initData.nodeType;
|
|
560
|
-
this._supportsTimers = initData.supportsTimers;
|
|
561
|
-
// ignore the initVersion, no clue what to do with it
|
|
562
|
-
this.driver.controllerLog.print(`received additional controller information:
|
|
563
|
-
Z-Wave API version: ${this._zwaveApiVersion.version} (${this._zwaveApiVersion.kind})${this._zwaveChipType
|
|
564
|
-
? `
|
|
565
|
-
Z-Wave chip type: ${typeof this._zwaveChipType === "string"
|
|
566
|
-
? this._zwaveChipType
|
|
567
|
-
: `unknown (type: ${(0, shared_1.num2hex)(this._zwaveChipType.type)}, version: ${(0, shared_1.num2hex)(this._zwaveChipType.version)})`}`
|
|
568
|
-
: ""}
|
|
569
|
-
node type ${(0, shared_1.getEnumMemberName)(core_1.NodeType, this._nodeType)}
|
|
570
|
-
controller role: ${this._isPrimary ? "primary" : "secondary"}
|
|
571
|
-
controller is the SIS: ${this._isSIS}
|
|
572
|
-
controller supports timers: ${this._supportsTimers}
|
|
573
|
-
Z-Wave Classic nodes: ${initData.nodeIds.join(", ")}`);
|
|
614
|
+
const initData = await this.getSerialApiInitData();
|
|
574
615
|
// Get basic controller version info
|
|
575
616
|
this.driver.controllerLog.print(`querying version info...`);
|
|
576
617
|
const version = await this.driver.sendMessage(new GetControllerVersionMessages_1.GetControllerVersionRequest(this.driver), {
|
|
@@ -602,22 +643,7 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
602
643
|
// The SDK version cannot be queried directly, but we can deduce it from the protocol version
|
|
603
644
|
this._sdkVersion = (0, ZWaveSDKVersions_1.protocolVersionToSDKVersion)(this._protocolVersion);
|
|
604
645
|
// find out what the controller can do
|
|
605
|
-
this.
|
|
606
|
-
const ctrlCaps = await this.driver.sendMessage(new GetControllerCapabilitiesMessages_1.GetControllerCapabilitiesRequest(this.driver), {
|
|
607
|
-
supportCheck: false,
|
|
608
|
-
});
|
|
609
|
-
this._isPrimary = !ctrlCaps.isSecondary;
|
|
610
|
-
this._isUsingHomeIdFromOtherNetwork =
|
|
611
|
-
ctrlCaps.isUsingHomeIdFromOtherNetwork;
|
|
612
|
-
this._isSISPresent = ctrlCaps.isSISPresent;
|
|
613
|
-
this._wasRealPrimary = ctrlCaps.wasRealPrimary;
|
|
614
|
-
this._isSUC = ctrlCaps.isStaticUpdateController;
|
|
615
|
-
this.driver.controllerLog.print(`received controller capabilities:
|
|
616
|
-
controller role: ${this._isPrimary ? "primary" : "secondary"}
|
|
617
|
-
is the SUC: ${this._isSUC}
|
|
618
|
-
started this network: ${!this._isUsingHomeIdFromOtherNetwork}
|
|
619
|
-
SIS is present: ${this._isSISPresent}
|
|
620
|
-
was real primary: ${this._wasRealPrimary}`);
|
|
646
|
+
await this.getControllerCapabilities();
|
|
621
647
|
// If the serial API can be configured, figure out which sub commands are supported
|
|
622
648
|
// This MUST be done after querying the SDK version due to a bug in some 7.xx firmwares, which incorrectly encode the bitmask
|
|
623
649
|
if (this.isFunctionSupported(serial_1.FunctionType.SerialAPISetup)) {
|
|
@@ -679,8 +705,18 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
679
705
|
}
|
|
680
706
|
/** Tries to determine the LR capable replacement of the given region. If none is found, the given region is returned. */
|
|
681
707
|
tryGetLRCapableRegion(region) {
|
|
682
|
-
|
|
683
|
-
|
|
708
|
+
if (this._supportedRegions) {
|
|
709
|
+
// If the region supports LR, use it
|
|
710
|
+
if (this._supportedRegions.get(region)?.supportsLongRange) {
|
|
711
|
+
return region;
|
|
712
|
+
}
|
|
713
|
+
// Find a possible LR capable superset for this region
|
|
714
|
+
for (const info of this._supportedRegions.values()) {
|
|
715
|
+
if (info.supportsLongRange && info.includesRegion === region) {
|
|
716
|
+
return info.region;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
684
720
|
// US_LR is the first supported LR region, so if the controller supports LR, US_LR is supported
|
|
685
721
|
if (region === core_1.RFRegion.USA && this.isLongRangeCapable()) {
|
|
686
722
|
return core_1.RFRegion["USA (Long Range)"];
|
|
@@ -692,6 +728,38 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
692
728
|
* Queries the region and powerlevel settings and configures them if necessary
|
|
693
729
|
*/
|
|
694
730
|
async queryAndConfigureRF() {
|
|
731
|
+
// Figure out which regions are supported
|
|
732
|
+
if (this.isSerialAPISetupCommandSupported(SerialAPISetupMessages_1.SerialAPISetupCommand.GetSupportedRegions)) {
|
|
733
|
+
this.driver.controllerLog.print(`Querying supported RF regions and their information...`);
|
|
734
|
+
const supportedRegions = await this.querySupportedRFRegions().catch(() => []);
|
|
735
|
+
this._supportedRegions = new Map();
|
|
736
|
+
for (const region of supportedRegions) {
|
|
737
|
+
try {
|
|
738
|
+
const info = await this.queryRFRegionInfo(region);
|
|
739
|
+
if (info.region === core_1.RFRegion.Unknown)
|
|
740
|
+
continue;
|
|
741
|
+
this._supportedRegions.set(region, info);
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
this.driver.controllerLog.print(`supported regions:${[...this._supportedRegions.values()]
|
|
748
|
+
.map((info) => {
|
|
749
|
+
let ret = `\n· ${(0, shared_1.getEnumMemberName)(core_1.RFRegion, info.region)}`;
|
|
750
|
+
if (info.includesRegion != undefined) {
|
|
751
|
+
ret += ` · superset of ${(0, shared_1.getEnumMemberName)(core_1.RFRegion, info.includesRegion)}`;
|
|
752
|
+
}
|
|
753
|
+
if (info.supportsLongRange) {
|
|
754
|
+
ret += " · ZWLR";
|
|
755
|
+
if (!info.supportsZWave) {
|
|
756
|
+
ret += " only";
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return ret;
|
|
760
|
+
})
|
|
761
|
+
.join("")}`);
|
|
762
|
+
}
|
|
695
763
|
// Check and possibly update the RF region to the desired value
|
|
696
764
|
if (this.isSerialAPISetupCommandSupported(SerialAPISetupMessages_1.SerialAPISetupCommand.GetRFRegion)) {
|
|
697
765
|
this.driver.controllerLog.print(`Querying configured RF region...`);
|
|
@@ -862,8 +930,9 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
862
930
|
this.driver.controllerLog.print(`SUC has node ID ${this.sucNodeId}`);
|
|
863
931
|
}
|
|
864
932
|
// There needs to be a SUC/SIS in the network. If not, we promote ourselves to one if the following conditions are met:
|
|
865
|
-
// We are the primary controller, but we are not SUC, there is no SUC and there is no SIS
|
|
866
|
-
if (this.
|
|
933
|
+
// We are the primary controller, but we are not SUC, there is no SUC and there is no SIS, and there are no nodes in the network yet
|
|
934
|
+
if (this.role === core_1.ControllerRole.Primary
|
|
935
|
+
&& this._noNodesIncluded
|
|
867
936
|
&& this._sucNodeId === 0
|
|
868
937
|
&& !this._isSUC
|
|
869
938
|
&& !this._isSISPresent) {
|
|
@@ -872,6 +941,8 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
872
941
|
const result = await this.configureSUC(this._ownNodeId, true, true);
|
|
873
942
|
if (result) {
|
|
874
943
|
this._sucNodeId = this._ownNodeId;
|
|
944
|
+
this._isSUC = true;
|
|
945
|
+
this._isSISPresent = true;
|
|
875
946
|
}
|
|
876
947
|
this.driver.controllerLog.print(`Promotion to SUC/SIS ${result ? "succeeded" : "failed"}.`, result ? undefined : "warn");
|
|
877
948
|
}
|
|
@@ -879,11 +950,7 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
879
950
|
this.driver.controllerLog.print(`Error while promoting to SUC/SIS: ${(0, shared_1.getErrorMessage)(e)}`, "error");
|
|
880
951
|
}
|
|
881
952
|
}
|
|
882
|
-
// if it's a bridge controller, request the virtual nodes
|
|
883
|
-
if (this.type === _Types_2.ZWaveLibraryTypes["Bridge Controller"]
|
|
884
|
-
&& this.isFunctionSupported(serial_1.FunctionType.FUNC_ID_ZW_GET_VIRTUAL_NODES)) {
|
|
885
|
-
// TODO: send FUNC_ID_ZW_GET_VIRTUAL_NODES message
|
|
886
|
-
}
|
|
953
|
+
// TODO: if it's a bridge controller, request the virtual nodes
|
|
887
954
|
if (this.type !== _Types_2.ZWaveLibraryTypes["Bridge Controller"]
|
|
888
955
|
&& this.isFunctionSupported(serial_1.FunctionType.SetSerialApiTimeouts)) {
|
|
889
956
|
const { ack, byte } = this.driver.options.timeouts;
|
|
@@ -1640,6 +1707,10 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
1640
1707
|
}
|
|
1641
1708
|
});
|
|
1642
1709
|
}
|
|
1710
|
+
else if (msg instanceof ApplicationUpdateRequest_1.ApplicationUpdateRequestSUCIdChanged) {
|
|
1711
|
+
this._sucNodeId = msg.sucNodeID;
|
|
1712
|
+
// TODO: Emit event or what?
|
|
1713
|
+
}
|
|
1643
1714
|
}
|
|
1644
1715
|
/**
|
|
1645
1716
|
* @internal
|
|
@@ -1824,6 +1895,10 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
1824
1895
|
}
|
|
1825
1896
|
// Remember that the node was granted the S0 security class
|
|
1826
1897
|
node.securityClasses.set(core_1.SecurityClass.S0_Legacy, true);
|
|
1898
|
+
this.driver.controllerLog.logNode(node.id, {
|
|
1899
|
+
message: `Security S0 bootstrapping successful`,
|
|
1900
|
+
});
|
|
1901
|
+
// success 🎉
|
|
1827
1902
|
}
|
|
1828
1903
|
catch (e) {
|
|
1829
1904
|
let errorMessage = `Security S0 bootstrapping failed, the node was not granted the S0 security class`;
|
|
@@ -2090,11 +2165,8 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
2090
2165
|
const timerStartTAI2 = Date.now();
|
|
2091
2166
|
// Generate ECDH key pair. We need to immediately send the other node our public key,
|
|
2092
2167
|
// so it won't abort bootstrapping
|
|
2093
|
-
const keyPair =
|
|
2094
|
-
const publicKey = (0, core_1.
|
|
2095
|
-
type: "spki",
|
|
2096
|
-
format: "der",
|
|
2097
|
-
}));
|
|
2168
|
+
const keyPair = (0, core_1.generateECDHKeyPair)();
|
|
2169
|
+
const publicKey = (0, core_1.extractRawECDHPublicKey)(keyPair.publicKey);
|
|
2098
2170
|
await api.sendPublicKey(publicKey);
|
|
2099
2171
|
// After this, the node will start sending us a KEX SET every 10 seconds.
|
|
2100
2172
|
// We won't be able to decode it until the DSK was verified
|
|
@@ -2137,11 +2209,7 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
2137
2209
|
// After the user has verified the DSK, we can derive the shared secret
|
|
2138
2210
|
// Z-Wave works with the "raw" keys, so this is a tad complicated
|
|
2139
2211
|
const sharedSecret = node_crypto_1.default.diffieHellman({
|
|
2140
|
-
publicKey:
|
|
2141
|
-
key: (0, core_1.encodeX25519KeyDERSPKI)(nodePublicKey),
|
|
2142
|
-
format: "der",
|
|
2143
|
-
type: "spki",
|
|
2144
|
-
}),
|
|
2212
|
+
publicKey: (0, core_1.importRawECDHPublicKey)(nodePublicKey),
|
|
2145
2213
|
privateKey: keyPair.privateKey,
|
|
2146
2214
|
});
|
|
2147
2215
|
// Derive temporary key from ECDH key pair - this will allow us to receive the node's KEX SET commands
|
|
@@ -2221,7 +2289,7 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
2221
2289
|
return Inclusion_1.SecurityBootstrapFailure.S2WrongSecurityLevel;
|
|
2222
2290
|
}
|
|
2223
2291
|
// Confirm the keys - the node will start requesting the granted keys in response
|
|
2224
|
-
await api.
|
|
2292
|
+
await api.confirmRequestedKeys({
|
|
2225
2293
|
requestCSA: kexParams.requestCSA,
|
|
2226
2294
|
requestedKeys: [...kexParams.requestedKeys],
|
|
2227
2295
|
supportedECDHProfiles: [...kexParams.supportedECDHProfiles],
|
|
@@ -2272,11 +2340,12 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
|
|
|
2272
2340
|
await abort(cc_1.KEXFailType.KeyNotGranted);
|
|
2273
2341
|
return Inclusion_1.SecurityBootstrapFailure.S2WrongSecurityLevel;
|
|
2274
2342
|
}
|
|
2275
|
-
// Send the node the requested key
|
|
2276
|
-
await api.sendNetworkKey(securityClass, securityManager.getKeysForSecurityClass(securityClass).pnk);
|
|
2277
2343
|
// We need to temporarily mark this security class as granted, so the following exchange will use this
|
|
2278
2344
|
// key for decryption
|
|
2345
|
+
// FIXME: Is this actually necessary?
|
|
2279
2346
|
node.securityClasses.set(securityClass, true);
|
|
2347
|
+
// Send the node the requested key
|
|
2348
|
+
await api.sendNetworkKey(securityClass, securityManager.getKeysForSecurityClass(securityClass).pnk);
|
|
2280
2349
|
// And wait for verification
|
|
2281
2350
|
const verify = await this.driver.waitForCommand((cc) => cc instanceof cc_1.Security2CCNetworkKeyVerify
|
|
2282
2351
|
|| cc instanceof cc_1.Security2CCKEXFail, cc_1.inclusionTimeouts.TA4).catch(() => "timeout");
|
|
@@ -4014,6 +4083,35 @@ ${associatedNodes.join(", ")}`,
|
|
|
4014
4083
|
this._rfRegion = result.region;
|
|
4015
4084
|
return result.region;
|
|
4016
4085
|
}
|
|
4086
|
+
/**
|
|
4087
|
+
* Query the supported regions of the Z-Wave API Module
|
|
4088
|
+
*
|
|
4089
|
+
* **Note:** Applications should prefer using {@link getSupportedRFRegions} instead
|
|
4090
|
+
*/
|
|
4091
|
+
async querySupportedRFRegions() {
|
|
4092
|
+
const result = await this.driver.sendMessage(new SerialAPISetupMessages_1.SerialAPISetup_GetSupportedRegionsRequest(this.driver));
|
|
4093
|
+
if (result instanceof SerialAPISetupMessages_1.SerialAPISetup_CommandUnsupportedResponse) {
|
|
4094
|
+
throw new core_1.ZWaveError(`Your hardware does not support getting the supported RF regions!`, core_1.ZWaveErrorCodes.Driver_NotSupported);
|
|
4095
|
+
}
|
|
4096
|
+
return result.supportedRegions;
|
|
4097
|
+
}
|
|
4098
|
+
/**
|
|
4099
|
+
* Query the supported regions of the Z-Wave API Module
|
|
4100
|
+
*
|
|
4101
|
+
* **Note:** Applications should prefer reading the cached value from {@link supportedRFRegions} instead
|
|
4102
|
+
*/
|
|
4103
|
+
async queryRFRegionInfo(region) {
|
|
4104
|
+
const result = await this.driver.sendMessage(new SerialAPISetupMessages_1.SerialAPISetup_GetRegionInfoRequest(this.driver, { region }));
|
|
4105
|
+
if (result instanceof SerialAPISetupMessages_1.SerialAPISetup_CommandUnsupportedResponse) {
|
|
4106
|
+
throw new core_1.ZWaveError(`Your hardware does not support getting the RF region info!`, core_1.ZWaveErrorCodes.Driver_NotSupported);
|
|
4107
|
+
}
|
|
4108
|
+
return (0, shared_1.pick)(result, [
|
|
4109
|
+
"region",
|
|
4110
|
+
"supportsZWave",
|
|
4111
|
+
"supportsLongRange",
|
|
4112
|
+
"includesRegion",
|
|
4113
|
+
]);
|
|
4114
|
+
}
|
|
4017
4115
|
/**
|
|
4018
4116
|
* Returns the RF regions supported by this controller, or `undefined` if the information is not known yet.
|
|
4019
4117
|
*
|
|
@@ -4021,7 +4119,21 @@ ${associatedNodes.join(", ")}`,
|
|
|
4021
4119
|
* for example `USA` which is a subset of `USA (Long Range)`
|
|
4022
4120
|
*/
|
|
4023
4121
|
getSupportedRFRegions(filterSubsets = true) {
|
|
4024
|
-
//
|
|
4122
|
+
// If supported by the firmware, rely on the queried information
|
|
4123
|
+
if (this.isSerialAPISetupCommandSupported(SerialAPISetupMessages_1.SerialAPISetupCommand.GetSupportedRegions)) {
|
|
4124
|
+
if (this._supportedRegions == core_1.NOT_KNOWN)
|
|
4125
|
+
return core_1.NOT_KNOWN;
|
|
4126
|
+
const allRegions = new Set(this._supportedRegions.keys());
|
|
4127
|
+
if (filterSubsets) {
|
|
4128
|
+
for (const region of this._supportedRegions.values()) {
|
|
4129
|
+
if (region.includesRegion != undefined) {
|
|
4130
|
+
allRegions.delete(region.includesRegion);
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
return [...allRegions].sort((a, b) => a - b);
|
|
4135
|
+
}
|
|
4136
|
+
// Fallback: Hardcoded list of known supported regions
|
|
4025
4137
|
const ret = new Set([
|
|
4026
4138
|
// Always supported
|
|
4027
4139
|
core_1.RFRegion.Europe,
|
|
@@ -4037,9 +4149,19 @@ ${associatedNodes.join(", ")}`,
|
|
|
4037
4149
|
core_1.RFRegion["Default (EU)"],
|
|
4038
4150
|
]);
|
|
4039
4151
|
if (this.isLongRangeCapable()) {
|
|
4152
|
+
// All LR capable controllers support USA Long Range
|
|
4040
4153
|
ret.add(core_1.RFRegion["USA (Long Range)"]);
|
|
4041
|
-
if (filterSubsets)
|
|
4154
|
+
if (filterSubsets)
|
|
4042
4155
|
ret.delete(core_1.RFRegion.USA);
|
|
4156
|
+
// EU Long Range was added in SDK 7.22 for 800 series chips
|
|
4157
|
+
// 7.22.1 adds support for querying the supported regions, so the following
|
|
4158
|
+
// is really only necessary for 7.22.0.
|
|
4159
|
+
if (typeof this._zwaveChipType === "string"
|
|
4160
|
+
&& (0, core_1.getChipTypeAndVersion)(this._zwaveChipType)?.type === 8
|
|
4161
|
+
&& this.sdkVersionGte("7.22")) {
|
|
4162
|
+
ret.add(core_1.RFRegion["Europe (Long Range)"]);
|
|
4163
|
+
if (filterSubsets)
|
|
4164
|
+
ret.delete(core_1.RFRegion.Europe);
|
|
4043
4165
|
}
|
|
4044
4166
|
}
|
|
4045
4167
|
return [...ret].sort((a, b) => a - b);
|
|
@@ -4269,6 +4391,69 @@ ${associatedNodes.join(", ")}`,
|
|
|
4269
4391
|
}
|
|
4270
4392
|
return ret;
|
|
4271
4393
|
}
|
|
4394
|
+
/** Request additional information about the controller/Z-Wave chip */
|
|
4395
|
+
async getSerialApiInitData() {
|
|
4396
|
+
this.driver.controllerLog.print(`querying additional controller information...`);
|
|
4397
|
+
const initData = await this.driver.sendMessage(new GetSerialApiInitDataMessages_1.GetSerialApiInitDataRequest(this.driver));
|
|
4398
|
+
this.driver.controllerLog.print(`received additional controller information:
|
|
4399
|
+
Z-Wave API version: ${initData.zwaveApiVersion.version} (${initData.zwaveApiVersion.kind})${initData.zwaveChipType
|
|
4400
|
+
? `
|
|
4401
|
+
Z-Wave chip type: ${typeof initData.zwaveChipType === "string"
|
|
4402
|
+
? initData.zwaveChipType
|
|
4403
|
+
: `unknown (type: ${(0, shared_1.num2hex)(initData.zwaveChipType.type)}, version: ${(0, shared_1.num2hex)(initData.zwaveChipType.version)})`}`
|
|
4404
|
+
: ""}
|
|
4405
|
+
node type ${(0, shared_1.getEnumMemberName)(core_1.NodeType, initData.nodeType)}
|
|
4406
|
+
controller role: ${initData.isPrimary ? "primary" : "secondary"}
|
|
4407
|
+
controller is the SIS: ${initData.isSIS}
|
|
4408
|
+
controller supports timers: ${initData.supportsTimers}
|
|
4409
|
+
Z-Wave Classic nodes: ${initData.nodeIds.join(", ")}`);
|
|
4410
|
+
const ret = {
|
|
4411
|
+
...(0, shared_1.pick)(initData, [
|
|
4412
|
+
"zwaveApiVersion",
|
|
4413
|
+
"zwaveChipType",
|
|
4414
|
+
"isPrimary",
|
|
4415
|
+
"isSIS",
|
|
4416
|
+
"nodeType",
|
|
4417
|
+
"supportsTimers",
|
|
4418
|
+
]),
|
|
4419
|
+
nodeIds: [...initData.nodeIds],
|
|
4420
|
+
// ignore the initVersion, no clue what to do with it
|
|
4421
|
+
};
|
|
4422
|
+
// and remember the new info
|
|
4423
|
+
this._zwaveApiVersion = initData.zwaveApiVersion;
|
|
4424
|
+
this._zwaveChipType = initData.zwaveChipType;
|
|
4425
|
+
this._isPrimary = initData.isPrimary;
|
|
4426
|
+
this._isSIS = initData.isSIS;
|
|
4427
|
+
this._nodeType = initData.nodeType;
|
|
4428
|
+
this._supportsTimers = initData.supportsTimers;
|
|
4429
|
+
return ret;
|
|
4430
|
+
}
|
|
4431
|
+
/** Determines the controller's network role/capabilities */
|
|
4432
|
+
async getControllerCapabilities() {
|
|
4433
|
+
this.driver.controllerLog.print(`querying controller capabilities...`);
|
|
4434
|
+
const result = await this.driver.sendMessage(new GetControllerCapabilitiesMessages_1.GetControllerCapabilitiesRequest(this.driver), { supportCheck: false });
|
|
4435
|
+
const ret = {
|
|
4436
|
+
isSecondary: result.isSecondary,
|
|
4437
|
+
isUsingHomeIdFromOtherNetwork: result.isUsingHomeIdFromOtherNetwork,
|
|
4438
|
+
isSISPresent: result.isSISPresent,
|
|
4439
|
+
wasRealPrimary: result.wasRealPrimary,
|
|
4440
|
+
isSUC: result.isStaticUpdateController,
|
|
4441
|
+
noNodesIncluded: result.noNodesIncluded,
|
|
4442
|
+
};
|
|
4443
|
+
this._isSecondary = ret.isSecondary;
|
|
4444
|
+
this._isUsingHomeIdFromOtherNetwork = ret.isUsingHomeIdFromOtherNetwork;
|
|
4445
|
+
this._isSISPresent = ret.isSISPresent;
|
|
4446
|
+
this._wasRealPrimary = ret.wasRealPrimary;
|
|
4447
|
+
this._isSUC = ret.isSUC;
|
|
4448
|
+
this._noNodesIncluded = ret.noNodesIncluded;
|
|
4449
|
+
this.driver.controllerLog.print(`received controller capabilities:
|
|
4450
|
+
controller role: ${(0, shared_1.getEnumMemberName)(core_1.ControllerRole, this.role)}
|
|
4451
|
+
is the SUC: ${ret.isSUC}
|
|
4452
|
+
started this network: ${!ret.isUsingHomeIdFromOtherNetwork}
|
|
4453
|
+
SIS is present: ${ret.isSISPresent}
|
|
4454
|
+
was real primary: ${ret.wasRealPrimary}`);
|
|
4455
|
+
return ret;
|
|
4456
|
+
}
|
|
4272
4457
|
/**
|
|
4273
4458
|
* @internal
|
|
4274
4459
|
* Deserializes the controller information and all nodes from the cache.
|
|
@@ -4407,9 +4592,11 @@ ${associatedNodes.join(", ")}`,
|
|
|
4407
4592
|
return ret.buffer;
|
|
4408
4593
|
}
|
|
4409
4594
|
/**
|
|
4410
|
-
* **Z-Wave 700 series only**
|
|
4595
|
+
* **Z-Wave 700+ series only**
|
|
4411
4596
|
*
|
|
4412
4597
|
* Reads a buffer from the external NVM at the given offset
|
|
4598
|
+
*
|
|
4599
|
+
* **Note:** Prefer {@link externalNVMReadBufferExt} if supported, as that command supports larger NVMs than 64 KiB.
|
|
4413
4600
|
*/
|
|
4414
4601
|
async externalNVMReadBuffer700(offset, length) {
|
|
4415
4602
|
const ret = await this.driver.sendMessage(new NVMOperationsMessages_1.NVMOperationsReadRequest(this.driver, {
|
|
@@ -4431,6 +4618,35 @@ ${associatedNodes.join(", ")}`,
|
|
|
4431
4618
|
endOfFile: ret.status === NVMOperationsMessages_1.NVMOperationStatus.EndOfFile,
|
|
4432
4619
|
};
|
|
4433
4620
|
}
|
|
4621
|
+
/**
|
|
4622
|
+
* **Z-Wave 700+ series only**
|
|
4623
|
+
*
|
|
4624
|
+
* Reads a buffer from the external NVM at the given offset
|
|
4625
|
+
*
|
|
4626
|
+
* **Note:** If supported, this command should be preferred over {@link externalNVMReadBuffer700} as it supports larger NVMs than 64 KiB.
|
|
4627
|
+
*/
|
|
4628
|
+
async externalNVMReadBufferExt(offset, length) {
|
|
4629
|
+
const ret = await this.driver.sendMessage(new ExtendedNVMOperationsMessages_1.ExtendedNVMOperationsReadRequest(this.driver, {
|
|
4630
|
+
offset,
|
|
4631
|
+
length,
|
|
4632
|
+
}));
|
|
4633
|
+
if (!ret.isOK()) {
|
|
4634
|
+
let message = "Could not read from the external NVM";
|
|
4635
|
+
if (ret.status
|
|
4636
|
+
=== ExtendedNVMOperationsMessages_1.ExtendedNVMOperationStatus.Error_OperationInterference) {
|
|
4637
|
+
message += ": interference between read and write operation.";
|
|
4638
|
+
}
|
|
4639
|
+
else if (ret.status
|
|
4640
|
+
=== ExtendedNVMOperationsMessages_1.ExtendedNVMOperationStatus.Error_OperationMismatch) {
|
|
4641
|
+
message += ": wrong operation requested.";
|
|
4642
|
+
}
|
|
4643
|
+
throw new core_1.ZWaveError(message, core_1.ZWaveErrorCodes.Controller_CommandError);
|
|
4644
|
+
}
|
|
4645
|
+
return {
|
|
4646
|
+
buffer: ret.bufferOrBitmask,
|
|
4647
|
+
endOfFile: ret.status === ExtendedNVMOperationsMessages_1.ExtendedNVMOperationStatus.EndOfFile,
|
|
4648
|
+
};
|
|
4649
|
+
}
|
|
4434
4650
|
/**
|
|
4435
4651
|
* **Z-Wave 500 series only**
|
|
4436
4652
|
*
|
|
@@ -4448,9 +4664,12 @@ ${associatedNodes.join(", ")}`,
|
|
|
4448
4664
|
return ret.success;
|
|
4449
4665
|
}
|
|
4450
4666
|
/**
|
|
4451
|
-
* **Z-Wave 700 series only**
|
|
4667
|
+
* **Z-Wave 700+ series only**
|
|
4452
4668
|
*
|
|
4453
4669
|
* Writes a buffer to the external NVM at the given offset
|
|
4670
|
+
*
|
|
4671
|
+
* **Note:** Prefer {@link externalNVMWriteBufferExt} if supported, as that command supports larger NVMs than 64 KiB.
|
|
4672
|
+
*
|
|
4454
4673
|
* **WARNING:** This function can write in the full NVM address space and is not offset to start at the application area.
|
|
4455
4674
|
* Take care not to accidentally overwrite the protocol NVM area!
|
|
4456
4675
|
*/
|
|
@@ -4474,9 +4693,46 @@ ${associatedNodes.join(", ")}`,
|
|
|
4474
4693
|
};
|
|
4475
4694
|
}
|
|
4476
4695
|
/**
|
|
4477
|
-
* **Z-Wave 700 series only**
|
|
4696
|
+
* **Z-Wave 700+ series only**
|
|
4697
|
+
*
|
|
4698
|
+
* Writes a buffer to the external NVM at the given offset
|
|
4699
|
+
*
|
|
4700
|
+
* **Note:** If supported, this command should be preferred over {@link externalNVMWriteBuffer700} as it supports larger NVMs than 64 KiB.
|
|
4701
|
+
*
|
|
4702
|
+
* **WARNING:** This function can write in the full NVM address space and is not offset to start at the application area.
|
|
4703
|
+
* Take care not to accidentally overwrite the protocol NVM area!
|
|
4704
|
+
*/
|
|
4705
|
+
async externalNVMWriteBufferExt(offset, buffer) {
|
|
4706
|
+
const ret = await this.driver.sendMessage(new ExtendedNVMOperationsMessages_1.ExtendedNVMOperationsWriteRequest(this.driver, {
|
|
4707
|
+
offset,
|
|
4708
|
+
buffer,
|
|
4709
|
+
}));
|
|
4710
|
+
if (!ret.isOK()) {
|
|
4711
|
+
let message = "Could not write to the external NVM";
|
|
4712
|
+
if (ret.status
|
|
4713
|
+
=== ExtendedNVMOperationsMessages_1.ExtendedNVMOperationStatus.Error_OperationInterference) {
|
|
4714
|
+
message += ": interference between read and write operation.";
|
|
4715
|
+
}
|
|
4716
|
+
else if (ret.status
|
|
4717
|
+
=== ExtendedNVMOperationsMessages_1.ExtendedNVMOperationStatus.Error_OperationMismatch) {
|
|
4718
|
+
message += ": wrong operation requested.";
|
|
4719
|
+
}
|
|
4720
|
+
else if (ret.status
|
|
4721
|
+
=== ExtendedNVMOperationsMessages_1.ExtendedNVMOperationStatus.Error_SubCommandNotSupported) {
|
|
4722
|
+
message += ": sub-command not supported.";
|
|
4723
|
+
}
|
|
4724
|
+
throw new core_1.ZWaveError(message, core_1.ZWaveErrorCodes.Controller_CommandError);
|
|
4725
|
+
}
|
|
4726
|
+
return {
|
|
4727
|
+
endOfFile: ret.status === ExtendedNVMOperationsMessages_1.ExtendedNVMOperationStatus.EndOfFile,
|
|
4728
|
+
};
|
|
4729
|
+
}
|
|
4730
|
+
/**
|
|
4731
|
+
* **Z-Wave 700+ series only**
|
|
4478
4732
|
*
|
|
4479
4733
|
* Opens the controller's external NVM for reading/writing and returns the NVM size
|
|
4734
|
+
*
|
|
4735
|
+
* **Note:** Prefer {@link externalNVMOpenExt} if supported, as that command supports larger NVMs than 64 KiB.
|
|
4480
4736
|
*/
|
|
4481
4737
|
async externalNVMOpen() {
|
|
4482
4738
|
const ret = await this.driver.sendMessage(new NVMOperationsMessages_1.NVMOperationsOpenRequest(this.driver));
|
|
@@ -4486,9 +4742,30 @@ ${associatedNodes.join(", ")}`,
|
|
|
4486
4742
|
return ret.offsetOrSize;
|
|
4487
4743
|
}
|
|
4488
4744
|
/**
|
|
4489
|
-
* **Z-Wave 700 series only**
|
|
4745
|
+
* **Z-Wave 700+ series only**
|
|
4746
|
+
*
|
|
4747
|
+
* Opens the controller's external NVM for reading/writing and returns the NVM size and supported operations.
|
|
4748
|
+
*
|
|
4749
|
+
* **Note:** If supported, this command should be preferred over {@link externalNVMOpen} as it supports larger NVMs than 64 KiB.
|
|
4750
|
+
*/
|
|
4751
|
+
async externalNVMOpenExt() {
|
|
4752
|
+
const ret = await this.driver.sendMessage(new ExtendedNVMOperationsMessages_1.ExtendedNVMOperationsOpenRequest(this.driver));
|
|
4753
|
+
if (!ret.isOK()) {
|
|
4754
|
+
throw new core_1.ZWaveError("Failed to open the external NVM", core_1.ZWaveErrorCodes.Controller_CommandError);
|
|
4755
|
+
}
|
|
4756
|
+
const size = ret.offsetOrSize;
|
|
4757
|
+
const supportedOperations = (0, core_1.parseBitMask)(ret.bufferOrBitmask, ExtendedNVMOperationsMessages_1.ExtendedNVMOperationsCommand.Open);
|
|
4758
|
+
return {
|
|
4759
|
+
size,
|
|
4760
|
+
supportedOperations,
|
|
4761
|
+
};
|
|
4762
|
+
}
|
|
4763
|
+
/**
|
|
4764
|
+
* **Z-Wave 700+ series only**
|
|
4490
4765
|
*
|
|
4491
4766
|
* Closes the controller's external NVM
|
|
4767
|
+
*
|
|
4768
|
+
* **Note:** Prefer {@link externalNVMCloseExt} if supported, as that command supports larger NVMs than 64 KiB.
|
|
4492
4769
|
*/
|
|
4493
4770
|
async externalNVMClose() {
|
|
4494
4771
|
const ret = await this.driver.sendMessage(new NVMOperationsMessages_1.NVMOperationsCloseRequest(this.driver));
|
|
@@ -4496,6 +4773,19 @@ ${associatedNodes.join(", ")}`,
|
|
|
4496
4773
|
throw new core_1.ZWaveError("Failed to close the external NVM", core_1.ZWaveErrorCodes.Controller_CommandError);
|
|
4497
4774
|
}
|
|
4498
4775
|
}
|
|
4776
|
+
/**
|
|
4777
|
+
* **Z-Wave 700+ series only**
|
|
4778
|
+
*
|
|
4779
|
+
* Closes the controller's external NVM
|
|
4780
|
+
*
|
|
4781
|
+
* **Note:** If supported, this command should be preferred over {@link externalNVMClose} as it supports larger NVMs than 64 KiB.
|
|
4782
|
+
*/
|
|
4783
|
+
async externalNVMCloseExt() {
|
|
4784
|
+
const ret = await this.driver.sendMessage(new ExtendedNVMOperationsMessages_1.ExtendedNVMOperationsCloseRequest(this.driver));
|
|
4785
|
+
if (!ret.isOK()) {
|
|
4786
|
+
throw new core_1.ZWaveError("Failed to close the external NVM", core_1.ZWaveErrorCodes.Controller_CommandError);
|
|
4787
|
+
}
|
|
4788
|
+
}
|
|
4499
4789
|
/**
|
|
4500
4790
|
* Creates a backup of the NVM and returns the raw data as a Buffer. The Z-Wave radio is turned off/on automatically.
|
|
4501
4791
|
* @param onProgress Can be used to monitor the progress of the operation, which may take several seconds up to a few minutes depending on the NVM size
|
|
@@ -4561,8 +4851,24 @@ ${associatedNodes.join(", ")}`,
|
|
|
4561
4851
|
return ret;
|
|
4562
4852
|
}
|
|
4563
4853
|
async backupNVMRaw700(onProgress) {
|
|
4854
|
+
let open;
|
|
4855
|
+
let read;
|
|
4856
|
+
let close;
|
|
4857
|
+
if (this.supportedFunctionTypes?.includes(serial_1.FunctionType.ExtendedNVMOperations)) {
|
|
4858
|
+
open = async () => {
|
|
4859
|
+
const { size } = await this.externalNVMOpenExt();
|
|
4860
|
+
return size;
|
|
4861
|
+
};
|
|
4862
|
+
read = (offset, length) => this.externalNVMReadBufferExt(offset, length);
|
|
4863
|
+
close = () => this.externalNVMCloseExt();
|
|
4864
|
+
}
|
|
4865
|
+
else {
|
|
4866
|
+
open = () => this.externalNVMOpen();
|
|
4867
|
+
read = (offset, length) => this.externalNVMReadBuffer700(offset, length);
|
|
4868
|
+
close = () => this.externalNVMClose();
|
|
4869
|
+
}
|
|
4564
4870
|
// Open NVM for reading
|
|
4565
|
-
const size = await
|
|
4871
|
+
const size = await open();
|
|
4566
4872
|
const ret = Buffer.allocUnsafe(size);
|
|
4567
4873
|
let offset = 0;
|
|
4568
4874
|
// Try reading the maximum size at first, the Serial API should return chunks in a size it supports
|
|
@@ -4570,8 +4876,7 @@ ${associatedNodes.join(", ")}`,
|
|
|
4570
4876
|
let chunkSize = Math.min(0xff, ret.length);
|
|
4571
4877
|
try {
|
|
4572
4878
|
while (offset < ret.length) {
|
|
4573
|
-
const { buffer: chunk, endOfFile } = await
|
|
4574
|
-
.externalNVMReadBuffer700(offset, Math.min(chunkSize, ret.length - offset));
|
|
4879
|
+
const { buffer: chunk, endOfFile } = await read(offset, Math.min(chunkSize, ret.length - offset));
|
|
4575
4880
|
if (chunkSize === 0xff && chunk.length === 0) {
|
|
4576
4881
|
// Some SDK versions return an empty buffer when trying to read a buffer that is too long
|
|
4577
4882
|
// Fallback to a sane (but maybe slow) size
|
|
@@ -4591,7 +4896,7 @@ ${associatedNodes.join(", ")}`,
|
|
|
4591
4896
|
}
|
|
4592
4897
|
finally {
|
|
4593
4898
|
// Whatever happens, close the NVM
|
|
4594
|
-
await
|
|
4899
|
+
await close();
|
|
4595
4900
|
}
|
|
4596
4901
|
return ret;
|
|
4597
4902
|
}
|
|
@@ -4735,8 +5040,27 @@ ${associatedNodes.join(", ")}`,
|
|
|
4735
5040
|
}
|
|
4736
5041
|
}
|
|
4737
5042
|
async restoreNVMRaw700(nvmData, onProgress) {
|
|
5043
|
+
let open;
|
|
5044
|
+
let read;
|
|
5045
|
+
let write;
|
|
5046
|
+
let close;
|
|
5047
|
+
if (this.supportedFunctionTypes?.includes(serial_1.FunctionType.ExtendedNVMOperations)) {
|
|
5048
|
+
open = async () => {
|
|
5049
|
+
const { size } = await this.externalNVMOpenExt();
|
|
5050
|
+
return size;
|
|
5051
|
+
};
|
|
5052
|
+
read = (offset, length) => this.externalNVMReadBufferExt(offset, length);
|
|
5053
|
+
write = (offset, buffer) => this.externalNVMWriteBufferExt(offset, buffer);
|
|
5054
|
+
close = () => this.externalNVMCloseExt();
|
|
5055
|
+
}
|
|
5056
|
+
else {
|
|
5057
|
+
open = () => this.externalNVMOpen();
|
|
5058
|
+
read = (offset, length) => this.externalNVMReadBuffer700(offset, length);
|
|
5059
|
+
write = (offset, buffer) => this.externalNVMWriteBuffer700(offset, buffer);
|
|
5060
|
+
close = () => this.externalNVMClose();
|
|
5061
|
+
}
|
|
4738
5062
|
// Open NVM for reading
|
|
4739
|
-
const size = await
|
|
5063
|
+
const size = await open();
|
|
4740
5064
|
if (size !== nvmData.length) {
|
|
4741
5065
|
throw new core_1.ZWaveError("The given data does not match the NVM size - cannot restore!", core_1.ZWaveErrorCodes.Argument_Invalid);
|
|
4742
5066
|
}
|
|
@@ -4744,12 +5068,12 @@ ${associatedNodes.join(", ")}`,
|
|
|
4744
5068
|
// For some reason, there is no documentation and no official command for this
|
|
4745
5069
|
// The write requests have the same size as the read response - if this yields no
|
|
4746
5070
|
// data, default to a sane (but maybe slow) size
|
|
4747
|
-
const chunkSize = (await
|
|
5071
|
+
const chunkSize = (await read(0, 0xff)).buffer.length || 48;
|
|
4748
5072
|
// Close NVM and re-open again for writing
|
|
4749
|
-
await
|
|
4750
|
-
await
|
|
5073
|
+
await close();
|
|
5074
|
+
await open();
|
|
4751
5075
|
for (let offset = 0; offset < nvmData.length; offset += chunkSize) {
|
|
4752
|
-
const { endOfFile } = await
|
|
5076
|
+
const { endOfFile } = await write(offset, nvmData.subarray(offset, offset + chunkSize));
|
|
4753
5077
|
// Report progress for listeners
|
|
4754
5078
|
if (onProgress)
|
|
4755
5079
|
setImmediate(() => onProgress(offset, size));
|
|
@@ -4757,7 +5081,7 @@ ${associatedNodes.join(", ")}`,
|
|
|
4757
5081
|
break;
|
|
4758
5082
|
}
|
|
4759
5083
|
// Close NVM
|
|
4760
|
-
await
|
|
5084
|
+
await close();
|
|
4761
5085
|
}
|
|
4762
5086
|
/**
|
|
4763
5087
|
* Request the most recent background RSSI levels detected by the controller.
|
|
@@ -4907,7 +5231,7 @@ ${associatedNodes.join(", ")}`,
|
|
|
4907
5231
|
* The return value indicates whether the update was successful.
|
|
4908
5232
|
* **WARNING:** This method will throw instead of returning `false` if invalid arguments are passed or downloading files or starting an update fails.
|
|
4909
5233
|
*/
|
|
4910
|
-
async firmwareUpdateOTA(nodeId, updateInfo) {
|
|
5234
|
+
async firmwareUpdateOTA(nodeId, updateInfo, options) {
|
|
4911
5235
|
// Don't let two firmware updates happen in parallel
|
|
4912
5236
|
if (this.isAnyOTAFirmwareUpdateInProgress()) {
|
|
4913
5237
|
const message = `Failed to start the update: A firmware update is already in progress on this network!`;
|
|
@@ -4977,7 +5301,7 @@ ${associatedNodes.join(", ")}`,
|
|
|
4977
5301
|
else {
|
|
4978
5302
|
this.driver.controllerLog.logNode(nodeId, `All updates downloaded, installing...`);
|
|
4979
5303
|
}
|
|
4980
|
-
return node.updateFirmware(firmwares);
|
|
5304
|
+
return node.updateFirmware(firmwares, options);
|
|
4981
5305
|
}
|
|
4982
5306
|
_firmwareUpdateInProgress = false;
|
|
4983
5307
|
/**
|
|
@@ -5231,6 +5555,788 @@ ${associatedNodes.join(", ")}`,
|
|
|
5231
5555
|
this._firmwareUpdateInProgress = false;
|
|
5232
5556
|
}
|
|
5233
5557
|
}
|
|
5558
|
+
_currentLearnMode;
|
|
5559
|
+
_joinNetworkOptions;
|
|
5560
|
+
async beginJoiningNetwork(options) {
|
|
5561
|
+
if (this._currentLearnMode != undefined) {
|
|
5562
|
+
return Inclusion_1.JoinNetworkResult.Error_Busy;
|
|
5563
|
+
}
|
|
5564
|
+
else if (!this.isLearnModePermitted) {
|
|
5565
|
+
return Inclusion_1.JoinNetworkResult.Error_NotPermitted;
|
|
5566
|
+
}
|
|
5567
|
+
// FIXME: If the join strategy says S0, remove S2 from the NIF before joining
|
|
5568
|
+
try {
|
|
5569
|
+
const result = await this.driver.sendMessage(new SetLearnModeMessages_1.SetLearnModeRequest(this.driver, {
|
|
5570
|
+
intent: SetLearnModeMessages_1.LearnModeIntent.Inclusion,
|
|
5571
|
+
}));
|
|
5572
|
+
if (result.isOK()) {
|
|
5573
|
+
this._currentLearnMode = SetLearnModeMessages_1.LearnModeIntent.Inclusion;
|
|
5574
|
+
this._joinNetworkOptions = options;
|
|
5575
|
+
return Inclusion_1.JoinNetworkResult.OK;
|
|
5576
|
+
}
|
|
5577
|
+
}
|
|
5578
|
+
catch (e) {
|
|
5579
|
+
this.driver.controllerLog.print(`Joining a network failed: ${(0, shared_1.getErrorMessage)(e)}`, "error");
|
|
5580
|
+
}
|
|
5581
|
+
this._currentLearnMode = undefined;
|
|
5582
|
+
return Inclusion_1.JoinNetworkResult.Error_Failed;
|
|
5583
|
+
}
|
|
5584
|
+
async stopJoiningNetwork() {
|
|
5585
|
+
if (this._currentLearnMode !== SetLearnModeMessages_1.LearnModeIntent.LegacyInclusionExclusion
|
|
5586
|
+
// FIXME: ^ only for actual exclusion
|
|
5587
|
+
&& this._currentLearnMode !== SetLearnModeMessages_1.LearnModeIntent.Inclusion) {
|
|
5588
|
+
return false;
|
|
5589
|
+
}
|
|
5590
|
+
try {
|
|
5591
|
+
const result = await this.driver.sendMessage(new SetLearnModeMessages_1.SetLearnModeRequest(this.driver, {
|
|
5592
|
+
// TODO: We should be using .Stop here for the non-legacy
|
|
5593
|
+
// inclusion/exclusion, but that command results in a
|
|
5594
|
+
// negative response on current firmwares, even though it works.
|
|
5595
|
+
// Using LegacyStop avoids that, but results in an unexpected
|
|
5596
|
+
// LearnModeFailed callback.
|
|
5597
|
+
intent: SetLearnModeMessages_1.LearnModeIntent.LegacyStop,
|
|
5598
|
+
}));
|
|
5599
|
+
if (result.isOK()) {
|
|
5600
|
+
this._currentLearnMode = undefined;
|
|
5601
|
+
this._joinNetworkOptions = undefined;
|
|
5602
|
+
return true;
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
catch (e) {
|
|
5606
|
+
this.driver.controllerLog.print(`Failed to stop joining a network: ${(0, shared_1.getErrorMessage)(e)}`, "error");
|
|
5607
|
+
}
|
|
5608
|
+
return false;
|
|
5609
|
+
}
|
|
5610
|
+
async beginLeavingNetwork() {
|
|
5611
|
+
if (this._currentLearnMode != undefined) {
|
|
5612
|
+
return Inclusion_1.LeaveNetworkResult.Error_Busy;
|
|
5613
|
+
}
|
|
5614
|
+
else if (!this.isLearnModePermitted) {
|
|
5615
|
+
return Inclusion_1.LeaveNetworkResult.Error_NotPermitted;
|
|
5616
|
+
}
|
|
5617
|
+
try {
|
|
5618
|
+
const result = await this.driver.sendMessage(new SetLearnModeMessages_1.SetLearnModeRequest(this.driver, {
|
|
5619
|
+
intent: SetLearnModeMessages_1.LearnModeIntent.NetworkWideExclusion,
|
|
5620
|
+
}));
|
|
5621
|
+
if (result.isOK()) {
|
|
5622
|
+
this._currentLearnMode = SetLearnModeMessages_1.LearnModeIntent.NetworkWideExclusion;
|
|
5623
|
+
return Inclusion_1.LeaveNetworkResult.OK;
|
|
5624
|
+
}
|
|
5625
|
+
}
|
|
5626
|
+
catch (e) {
|
|
5627
|
+
this.driver.controllerLog.print(`Leaving the current network failed: ${(0, shared_1.getErrorMessage)(e)}`, "error");
|
|
5628
|
+
}
|
|
5629
|
+
this._currentLearnMode = undefined;
|
|
5630
|
+
return Inclusion_1.LeaveNetworkResult.Error_Failed;
|
|
5631
|
+
}
|
|
5632
|
+
async stopLeavingNetwork() {
|
|
5633
|
+
if (this._currentLearnMode !== SetLearnModeMessages_1.LearnModeIntent.LegacyInclusionExclusion
|
|
5634
|
+
// FIXME: ^ only for actual exclusion
|
|
5635
|
+
&& this._currentLearnMode
|
|
5636
|
+
!== SetLearnModeMessages_1.LearnModeIntent.LegacyNetworkWideExclusion
|
|
5637
|
+
&& this._currentLearnMode !== SetLearnModeMessages_1.LearnModeIntent.DirectExclusion
|
|
5638
|
+
&& this._currentLearnMode !== SetLearnModeMessages_1.LearnModeIntent.NetworkWideExclusion) {
|
|
5639
|
+
return false;
|
|
5640
|
+
}
|
|
5641
|
+
try {
|
|
5642
|
+
const result = await this.driver.sendMessage(new SetLearnModeMessages_1.SetLearnModeRequest(this.driver, {
|
|
5643
|
+
// TODO: We should be using .Stop here for the non-legacy
|
|
5644
|
+
// inclusion/exclusion, but that command results in a
|
|
5645
|
+
// negative response on current firmwares, even though it works.
|
|
5646
|
+
// Using LegacyStop avoids that, but results in an unexpected
|
|
5647
|
+
// LearnModeFailed callback.
|
|
5648
|
+
intent: SetLearnModeMessages_1.LearnModeIntent.LegacyStop,
|
|
5649
|
+
}));
|
|
5650
|
+
if (result.isOK()) {
|
|
5651
|
+
this._currentLearnMode = undefined;
|
|
5652
|
+
return true;
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5655
|
+
catch (e) {
|
|
5656
|
+
this.driver.controllerLog.print(`Failed to stop leaving a network: ${(0, shared_1.getErrorMessage)(e)}`, "error");
|
|
5657
|
+
}
|
|
5658
|
+
return false;
|
|
5659
|
+
}
|
|
5660
|
+
/**
|
|
5661
|
+
* Is called when a RemoveNode request is received from the controller.
|
|
5662
|
+
* Handles and controls the exclusion process.
|
|
5663
|
+
*/
|
|
5664
|
+
handleLearnModeCallback(msg) {
|
|
5665
|
+
// not sure what to do with this message, we're not in learn mode
|
|
5666
|
+
if (this._currentLearnMode == undefined)
|
|
5667
|
+
return false;
|
|
5668
|
+
// FIXME: Reset security manager on successful join or leave
|
|
5669
|
+
const wasJoining = this._currentLearnMode === SetLearnModeMessages_1.LearnModeIntent.Inclusion
|
|
5670
|
+
|| this._currentLearnMode === SetLearnModeMessages_1.LearnModeIntent.SmartStart
|
|
5671
|
+
|| this._currentLearnMode
|
|
5672
|
+
=== SetLearnModeMessages_1.LearnModeIntent.LegacyNetworkWideInclusion
|
|
5673
|
+
|| (this._currentLearnMode
|
|
5674
|
+
=== SetLearnModeMessages_1.LearnModeIntent.LegacyInclusionExclusion
|
|
5675
|
+
// TODO: Secondary controller may also use this to accept controller shift
|
|
5676
|
+
// Figure out how to detect that.
|
|
5677
|
+
&& this.role === core_1.ControllerRole.Primary);
|
|
5678
|
+
const wasLeaving = this._currentLearnMode === SetLearnModeMessages_1.LearnModeIntent.DirectExclusion
|
|
5679
|
+
|| this._currentLearnMode
|
|
5680
|
+
=== SetLearnModeMessages_1.LearnModeIntent.NetworkWideExclusion
|
|
5681
|
+
|| this._currentLearnMode
|
|
5682
|
+
=== SetLearnModeMessages_1.LearnModeIntent.LegacyNetworkWideExclusion
|
|
5683
|
+
|| (this._currentLearnMode
|
|
5684
|
+
=== SetLearnModeMessages_1.LearnModeIntent.LegacyInclusionExclusion
|
|
5685
|
+
&& this.role !== core_1.ControllerRole.Primary);
|
|
5686
|
+
if (msg.status === SetLearnModeMessages_1.LearnModeStatus.Started) {
|
|
5687
|
+
// cool, cool, cool...
|
|
5688
|
+
return true;
|
|
5689
|
+
}
|
|
5690
|
+
else if (msg.status === SetLearnModeMessages_1.LearnModeStatus.Failed) {
|
|
5691
|
+
if (wasJoining) {
|
|
5692
|
+
this._currentLearnMode = undefined;
|
|
5693
|
+
this._joinNetworkOptions = undefined;
|
|
5694
|
+
this.emit("joining network failed");
|
|
5695
|
+
return true;
|
|
5696
|
+
}
|
|
5697
|
+
else if (wasLeaving) {
|
|
5698
|
+
this._currentLearnMode = undefined;
|
|
5699
|
+
this.emit("leaving network failed");
|
|
5700
|
+
return true;
|
|
5701
|
+
}
|
|
5702
|
+
}
|
|
5703
|
+
else if (msg.status === SetLearnModeMessages_1.LearnModeStatus.Completed
|
|
5704
|
+
|| (this._currentLearnMode >= SetLearnModeMessages_1.LearnModeIntent.Inclusion
|
|
5705
|
+
&& msg.status === SetLearnModeMessages_1.LearnModeStatus.ProtocolDone)) {
|
|
5706
|
+
if (wasJoining) {
|
|
5707
|
+
this._currentLearnMode = undefined;
|
|
5708
|
+
this.driver["_securityManager"] = undefined;
|
|
5709
|
+
this.driver["_securityManager2"] = new core_1.SecurityManager2();
|
|
5710
|
+
this.driver["_securityManagerLR"] = new core_1.SecurityManager2();
|
|
5711
|
+
this._nodes.clear();
|
|
5712
|
+
process.nextTick(() => this.afterJoiningNetwork().catch(shared_1.noop));
|
|
5713
|
+
return true;
|
|
5714
|
+
}
|
|
5715
|
+
else if (wasLeaving) {
|
|
5716
|
+
this._currentLearnMode = undefined;
|
|
5717
|
+
this.emit("network left");
|
|
5718
|
+
return true;
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
// not sure what to do with this message
|
|
5722
|
+
return false;
|
|
5723
|
+
}
|
|
5724
|
+
async expectSecurityBootstrapS0(bootstrappingNode) {
|
|
5725
|
+
// When bootstrapping with S0, no other keys are granted
|
|
5726
|
+
for (const secClass of core_1.securityClassOrder) {
|
|
5727
|
+
if (secClass !== core_1.SecurityClass.S0_Legacy) {
|
|
5728
|
+
bootstrappingNode.securityClasses.set(secClass, false);
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
const unGrantSecurityClass = () => {
|
|
5732
|
+
this.driver["_securityManager"] = undefined;
|
|
5733
|
+
bootstrappingNode.securityClasses.set(core_1.SecurityClass.S0_Legacy, false);
|
|
5734
|
+
};
|
|
5735
|
+
const abortTimeout = () => {
|
|
5736
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
5737
|
+
message: `Security S0 bootstrapping failed: a secure inclusion timer has elapsed`,
|
|
5738
|
+
level: "warn",
|
|
5739
|
+
});
|
|
5740
|
+
unGrantSecurityClass();
|
|
5741
|
+
return Inclusion_1.SecurityBootstrapFailure.Timeout;
|
|
5742
|
+
};
|
|
5743
|
+
try {
|
|
5744
|
+
const api = bootstrappingNode.commandClasses.Security;
|
|
5745
|
+
// For the first part of the bootstrapping, a temporary key needs to be used
|
|
5746
|
+
this.driver["_securityManager"] = new core_1.SecurityManager({
|
|
5747
|
+
ownNodeId: this._ownNodeId,
|
|
5748
|
+
networkKey: Buffer.alloc(16, 0),
|
|
5749
|
+
nonceTimeout: this.driver.options.timeouts.nonce,
|
|
5750
|
+
});
|
|
5751
|
+
// Report the supported schemes
|
|
5752
|
+
await api.reportSecurityScheme(false);
|
|
5753
|
+
// Expect a NonceGet within 10 seconds
|
|
5754
|
+
let nonceGet = await this.driver.waitForCommand((cc) => cc instanceof cc_1.SecurityCCNonceGet, 10000).catch(() => "timeout");
|
|
5755
|
+
if (nonceGet === "timeout")
|
|
5756
|
+
return abortTimeout();
|
|
5757
|
+
// Send nonce
|
|
5758
|
+
await api.sendNonce();
|
|
5759
|
+
// Expect NetworkKeySet within 10 seconds
|
|
5760
|
+
const networkKeySet = await this.driver.waitForCommand((cc) => cc instanceof cc_1.SecurityCCNetworkKeySet, 10000).catch(() => "timeout");
|
|
5761
|
+
if (networkKeySet === "timeout")
|
|
5762
|
+
return abortTimeout();
|
|
5763
|
+
// Now that the key is known, we can create the real security manager
|
|
5764
|
+
this.driver["_securityManager"] = new core_1.SecurityManager({
|
|
5765
|
+
ownNodeId: this._ownNodeId,
|
|
5766
|
+
networkKey: networkKeySet.networkKey,
|
|
5767
|
+
nonceTimeout: this.driver.options.timeouts.nonce,
|
|
5768
|
+
});
|
|
5769
|
+
// Request a new nonce to respond, which should be answered within 10 seconds
|
|
5770
|
+
let nonce = await api.withOptions({ reportTimeoutMs: 10000 })
|
|
5771
|
+
.getNonce();
|
|
5772
|
+
if (!nonce)
|
|
5773
|
+
return abortTimeout();
|
|
5774
|
+
// Verify the key
|
|
5775
|
+
await api.verifyNetworkKey();
|
|
5776
|
+
// We are a controller, so continue with scheme inherit
|
|
5777
|
+
// Expect a NonceGet within 10 seconds
|
|
5778
|
+
nonceGet = await this.driver.waitForCommand((cc) => cc instanceof cc_1.SecurityCCNonceGet, 10000).catch(() => "timeout");
|
|
5779
|
+
if (nonceGet === "timeout")
|
|
5780
|
+
return abortTimeout();
|
|
5781
|
+
// Send nonce
|
|
5782
|
+
await api.sendNonce();
|
|
5783
|
+
// Expect SchemeInherit within 10 seconds
|
|
5784
|
+
const schemeInherit = await this.driver.waitForCommand((cc) => cc instanceof cc_1.SecurityCCSchemeInherit, 10000).catch(() => "timeout");
|
|
5785
|
+
if (schemeInherit === "timeout")
|
|
5786
|
+
return abortTimeout();
|
|
5787
|
+
// Request a new nonce to respond, which should be answered within 10 seconds
|
|
5788
|
+
nonce = await api.withOptions({ reportTimeoutMs: 10000 })
|
|
5789
|
+
.getNonce();
|
|
5790
|
+
if (!nonce)
|
|
5791
|
+
return abortTimeout();
|
|
5792
|
+
// Report the supported schemes. This isn't technically correct, but since
|
|
5793
|
+
// S0 won't get any extensions, we can just report the default scheme again
|
|
5794
|
+
await api.reportSecurityScheme(true);
|
|
5795
|
+
// Remember that the S0 key was granted
|
|
5796
|
+
bootstrappingNode.securityClasses.set(core_1.SecurityClass.S0_Legacy, true);
|
|
5797
|
+
// Store the key
|
|
5798
|
+
this.driver.cacheSet(NetworkCache_1.cacheKeys.controller.securityKeys(core_1.SecurityClass.S0_Legacy), networkKeySet.networkKey);
|
|
5799
|
+
this.driver.driverLog.print(`Security S0 bootstrapping successful`);
|
|
5800
|
+
// success 🎉
|
|
5801
|
+
}
|
|
5802
|
+
catch (e) {
|
|
5803
|
+
let errorMessage = `Security S0 bootstrapping failed`;
|
|
5804
|
+
let result = Inclusion_1.SecurityBootstrapFailure.Unknown;
|
|
5805
|
+
if (!(0, core_1.isZWaveError)(e)) {
|
|
5806
|
+
errorMessage += `: ${e}`;
|
|
5807
|
+
}
|
|
5808
|
+
else if (e.code === core_1.ZWaveErrorCodes.Controller_MessageExpired) {
|
|
5809
|
+
errorMessage += ": a secure inclusion timer has elapsed.";
|
|
5810
|
+
result = Inclusion_1.SecurityBootstrapFailure.Timeout;
|
|
5811
|
+
}
|
|
5812
|
+
else if (e.code !== core_1.ZWaveErrorCodes.Controller_MessageDropped
|
|
5813
|
+
&& e.code !== core_1.ZWaveErrorCodes.Controller_NodeTimeout) {
|
|
5814
|
+
errorMessage += `: ${e.message}`;
|
|
5815
|
+
}
|
|
5816
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, errorMessage, "warn");
|
|
5817
|
+
unGrantSecurityClass();
|
|
5818
|
+
return result;
|
|
5819
|
+
}
|
|
5820
|
+
}
|
|
5821
|
+
async expectSecurityBootstrapS2(bootstrappingNode, requested, userCallbacks = this.driver.options.joinNetworkUserCallbacks) {
|
|
5822
|
+
const api = bootstrappingNode.commandClasses["Security 2"]
|
|
5823
|
+
.withOptions({
|
|
5824
|
+
// Do not wait for Nonce Reports after SET-type commands.
|
|
5825
|
+
// Timing is critical here
|
|
5826
|
+
s2VerifyDelivery: false,
|
|
5827
|
+
});
|
|
5828
|
+
const unGrantSecurityClasses = () => {
|
|
5829
|
+
for (const secClass of core_1.securityClassOrder) {
|
|
5830
|
+
bootstrappingNode.securityClasses.set(secClass, false);
|
|
5831
|
+
}
|
|
5832
|
+
};
|
|
5833
|
+
// FIXME: Abstract this out so it can be reused as primary and secondary
|
|
5834
|
+
const securityManager = (0, core_1.isLongRangeNodeId)(this._ownNodeId)
|
|
5835
|
+
? this.driver.securityManagerLR
|
|
5836
|
+
: this.driver.securityManager2;
|
|
5837
|
+
if (!securityManager) {
|
|
5838
|
+
// This should not happen when joining a network.
|
|
5839
|
+
unGrantSecurityClasses();
|
|
5840
|
+
return Inclusion_1.SecurityBootstrapFailure.NoKeysConfigured;
|
|
5841
|
+
}
|
|
5842
|
+
const receivedKeys = new Map();
|
|
5843
|
+
const deleteTempKey = () => {
|
|
5844
|
+
// Whatever happens, no further communication needs the temporary key
|
|
5845
|
+
securityManager.deleteNonce(bootstrappingNode.id);
|
|
5846
|
+
securityManager.tempKeys.delete(bootstrappingNode.id);
|
|
5847
|
+
};
|
|
5848
|
+
let dskHidden = false;
|
|
5849
|
+
const applicationHideDSK = () => {
|
|
5850
|
+
if (dskHidden)
|
|
5851
|
+
return;
|
|
5852
|
+
dskHidden = true;
|
|
5853
|
+
try {
|
|
5854
|
+
userCallbacks?.done();
|
|
5855
|
+
}
|
|
5856
|
+
catch {
|
|
5857
|
+
// ignore application-level errors
|
|
5858
|
+
}
|
|
5859
|
+
};
|
|
5860
|
+
const abort = async (failType) => {
|
|
5861
|
+
applicationHideDSK();
|
|
5862
|
+
if (failType != undefined) {
|
|
5863
|
+
try {
|
|
5864
|
+
await api.abortKeyExchange(failType);
|
|
5865
|
+
}
|
|
5866
|
+
catch {
|
|
5867
|
+
// ignore
|
|
5868
|
+
}
|
|
5869
|
+
}
|
|
5870
|
+
// Un-grant S2 security classes we might have granted
|
|
5871
|
+
unGrantSecurityClasses();
|
|
5872
|
+
deleteTempKey();
|
|
5873
|
+
};
|
|
5874
|
+
const abortTimeout = async () => {
|
|
5875
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
5876
|
+
message: `Security S2 bootstrapping failed: a secure inclusion timer has elapsed`,
|
|
5877
|
+
level: "warn",
|
|
5878
|
+
});
|
|
5879
|
+
await abort();
|
|
5880
|
+
return Inclusion_1.SecurityBootstrapFailure.Timeout;
|
|
5881
|
+
};
|
|
5882
|
+
const abortCanceled = async () => {
|
|
5883
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
5884
|
+
message: `The including node canceled the Security S2 bootstrapping.`,
|
|
5885
|
+
direction: "inbound",
|
|
5886
|
+
level: "warn",
|
|
5887
|
+
});
|
|
5888
|
+
await abort();
|
|
5889
|
+
return Inclusion_1.SecurityBootstrapFailure.NodeCanceled;
|
|
5890
|
+
};
|
|
5891
|
+
try {
|
|
5892
|
+
// Send with our desired keys
|
|
5893
|
+
await api.requestKeys({
|
|
5894
|
+
requestedKeys: requested.securityClasses,
|
|
5895
|
+
requestCSA: false,
|
|
5896
|
+
supportedECDHProfiles: [cc_1.ECDHProfiles.Curve25519],
|
|
5897
|
+
supportedKEXSchemes: [cc_1.KEXSchemes.KEXScheme1],
|
|
5898
|
+
});
|
|
5899
|
+
// Wait for including node to grant keys
|
|
5900
|
+
const kexSet = await this.driver.waitForCommand((cc) => cc instanceof cc_1.Security2CCKEXSet
|
|
5901
|
+
|| cc instanceof cc_1.Security2CCKEXFail, cc_1.inclusionTimeouts.TB2).catch(() => "timeout");
|
|
5902
|
+
if (kexSet === "timeout")
|
|
5903
|
+
return abortTimeout();
|
|
5904
|
+
if (kexSet instanceof cc_1.Security2CCKEXFail) {
|
|
5905
|
+
return abortCanceled();
|
|
5906
|
+
}
|
|
5907
|
+
// Validate the command
|
|
5908
|
+
// Echo flag must be false
|
|
5909
|
+
if (kexSet.echo) {
|
|
5910
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
5911
|
+
message: `Security S2 bootstrapping failed: KEX Set unexpectedly has the echo flag set.`,
|
|
5912
|
+
level: "warn",
|
|
5913
|
+
});
|
|
5914
|
+
await abort(cc_1.KEXFailType.NoVerify);
|
|
5915
|
+
return Inclusion_1.SecurityBootstrapFailure.ParameterMismatch;
|
|
5916
|
+
}
|
|
5917
|
+
else if (kexSet.selectedKEXScheme !== cc_1.KEXSchemes.KEXScheme1) {
|
|
5918
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
5919
|
+
message: `Security S2 bootstrapping failed: Unsupported key exchange scheme.`,
|
|
5920
|
+
level: "warn",
|
|
5921
|
+
});
|
|
5922
|
+
await abort(cc_1.KEXFailType.NoSupportedScheme);
|
|
5923
|
+
return Inclusion_1.SecurityBootstrapFailure.ParameterMismatch;
|
|
5924
|
+
}
|
|
5925
|
+
else if (kexSet.selectedECDHProfile !== cc_1.ECDHProfiles.Curve25519) {
|
|
5926
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
5927
|
+
message: `Security S2 bootstrapping failed: Unsupported ECDH profile.`,
|
|
5928
|
+
level: "warn",
|
|
5929
|
+
});
|
|
5930
|
+
await abort(cc_1.KEXFailType.NoSupportedCurve);
|
|
5931
|
+
return Inclusion_1.SecurityBootstrapFailure.ParameterMismatch;
|
|
5932
|
+
}
|
|
5933
|
+
else if (kexSet.permitCSA !== false) {
|
|
5934
|
+
// We do not support CSA at the moment, so it is never requested.
|
|
5935
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
5936
|
+
message: `Security S2 bootstrapping failed: CSA granted but not requested.`,
|
|
5937
|
+
level: "warn",
|
|
5938
|
+
});
|
|
5939
|
+
await abort(cc_1.KEXFailType.BootstrappingCanceled);
|
|
5940
|
+
return Inclusion_1.SecurityBootstrapFailure.ParameterMismatch;
|
|
5941
|
+
}
|
|
5942
|
+
const matchingKeys = kexSet.grantedKeys.filter((k) => core_1.securityClassOrder.includes(k)
|
|
5943
|
+
&& requested.securityClasses.includes(k));
|
|
5944
|
+
if (!matchingKeys.length) {
|
|
5945
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
5946
|
+
message: `Security S2 bootstrapping failed: None of the requested security classes are granted.`,
|
|
5947
|
+
level: "warn",
|
|
5948
|
+
});
|
|
5949
|
+
await abort(cc_1.KEXFailType.NoKeyMatch);
|
|
5950
|
+
return Inclusion_1.SecurityBootstrapFailure.ParameterMismatch;
|
|
5951
|
+
}
|
|
5952
|
+
const highestGranted = (0, core_1.getHighestSecurityClass)(matchingKeys);
|
|
5953
|
+
const requiresAuthentication = highestGranted === core_1.SecurityClass.S2_AccessControl
|
|
5954
|
+
|| highestGranted === core_1.SecurityClass.S2_Authenticated;
|
|
5955
|
+
// If authentication is required, use the (static) authenticated ECDH key pair,
|
|
5956
|
+
// otherwise generate a new one
|
|
5957
|
+
const keyPair = requiresAuthentication
|
|
5958
|
+
? this.driver.getLearnModeAuthenticatedKeyPair()
|
|
5959
|
+
: (0, core_1.generateECDHKeyPair)();
|
|
5960
|
+
const publicKey = (0, core_1.extractRawECDHPublicKey)(keyPair.publicKey);
|
|
5961
|
+
const transmittedPublicKey = Buffer.from(publicKey);
|
|
5962
|
+
if (requiresAuthentication) {
|
|
5963
|
+
// Authentication requires obfuscating the public key
|
|
5964
|
+
transmittedPublicKey.writeUInt16BE(0x0000, 0);
|
|
5965
|
+
// Show the DSK to the user
|
|
5966
|
+
const dsk = (0, core_1.dskToString)(publicKey.subarray(0, 16));
|
|
5967
|
+
try {
|
|
5968
|
+
userCallbacks?.showDSK(dsk);
|
|
5969
|
+
}
|
|
5970
|
+
catch {
|
|
5971
|
+
// ignore application-level errors
|
|
5972
|
+
}
|
|
5973
|
+
}
|
|
5974
|
+
await api.sendPublicKey(transmittedPublicKey, false);
|
|
5975
|
+
// Wait for including node to send its public key
|
|
5976
|
+
const pubKeyReport = await this.driver.waitForCommand((cc) => cc instanceof cc_1.Security2CCPublicKeyReport
|
|
5977
|
+
|| cc instanceof cc_1.Security2CCKEXFail, cc_1.inclusionTimeouts.TB3).catch(() => "timeout");
|
|
5978
|
+
if (pubKeyReport === "timeout")
|
|
5979
|
+
return abortTimeout();
|
|
5980
|
+
if (pubKeyReport instanceof cc_1.Security2CCKEXFail) {
|
|
5981
|
+
return abortCanceled();
|
|
5982
|
+
}
|
|
5983
|
+
const includingNodePubKey = pubKeyReport.publicKey;
|
|
5984
|
+
const sharedSecret = node_crypto_1.default.diffieHellman({
|
|
5985
|
+
publicKey: (0, core_1.importRawECDHPublicKey)(includingNodePubKey),
|
|
5986
|
+
privateKey: keyPair.privateKey,
|
|
5987
|
+
});
|
|
5988
|
+
// Derive temporary key from ECDH key pair - this will allow us to receive the node's KEX SET commands
|
|
5989
|
+
const tempKeys = (0, core_1.deriveTempKeys)((0, core_1.computePRK)(sharedSecret, includingNodePubKey, publicKey));
|
|
5990
|
+
securityManager.deleteNonce(bootstrappingNode.id);
|
|
5991
|
+
securityManager.tempKeys.set(bootstrappingNode.id, {
|
|
5992
|
+
keyCCM: tempKeys.tempKeyCCM,
|
|
5993
|
+
personalizationString: tempKeys.tempPersonalizationString,
|
|
5994
|
+
});
|
|
5995
|
+
// Wait for the confirmation of the requested keys and
|
|
5996
|
+
// retransmit the KEXSet echo every 10 seconds until a response is
|
|
5997
|
+
// received or the process timed out.
|
|
5998
|
+
const confirmKeysStartTime = Date.now();
|
|
5999
|
+
let kexReportEcho;
|
|
6000
|
+
for (let i = 0; i <= 25; i++) {
|
|
6001
|
+
try {
|
|
6002
|
+
kexReportEcho = await api.withOptions({
|
|
6003
|
+
reportTimeoutMs: 10000,
|
|
6004
|
+
}).confirmGrantedKeys({
|
|
6005
|
+
grantedKeys: kexSet.grantedKeys,
|
|
6006
|
+
permitCSA: kexSet.permitCSA,
|
|
6007
|
+
selectedECDHProfile: kexSet.selectedECDHProfile,
|
|
6008
|
+
selectedKEXScheme: kexSet.selectedKEXScheme,
|
|
6009
|
+
_reserved: kexSet._reserved,
|
|
6010
|
+
});
|
|
6011
|
+
}
|
|
6012
|
+
catch {
|
|
6013
|
+
// ignore
|
|
6014
|
+
}
|
|
6015
|
+
if (kexReportEcho != undefined)
|
|
6016
|
+
break;
|
|
6017
|
+
if (Date.now() - confirmKeysStartTime > 240000)
|
|
6018
|
+
break;
|
|
6019
|
+
}
|
|
6020
|
+
if (!kexReportEcho || kexReportEcho === "timeout") {
|
|
6021
|
+
return abortTimeout();
|
|
6022
|
+
}
|
|
6023
|
+
else if (kexReportEcho instanceof cc_1.Security2CCKEXFail) {
|
|
6024
|
+
return abortCanceled();
|
|
6025
|
+
}
|
|
6026
|
+
// The application no longer needs to show the DSK
|
|
6027
|
+
applicationHideDSK();
|
|
6028
|
+
// Validate the response
|
|
6029
|
+
if (!kexReportEcho.echo) {
|
|
6030
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
6031
|
+
message: `Security S2 bootstrapping failed: KEXReport received without echo flag`,
|
|
6032
|
+
direction: "inbound",
|
|
6033
|
+
level: "warn",
|
|
6034
|
+
});
|
|
6035
|
+
await abort(cc_1.KEXFailType.WrongSecurityLevel);
|
|
6036
|
+
return Inclusion_1.SecurityBootstrapFailure.NodeCanceled;
|
|
6037
|
+
}
|
|
6038
|
+
else if (kexReportEcho.requestCSA !== false) {
|
|
6039
|
+
// We don't request CSA
|
|
6040
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
6041
|
+
message: `Security S2 bootstrapping failed: Invalid KEXReport received`,
|
|
6042
|
+
level: "warn",
|
|
6043
|
+
});
|
|
6044
|
+
await abort(cc_1.KEXFailType.WrongSecurityLevel);
|
|
6045
|
+
return Inclusion_1.SecurityBootstrapFailure.NodeCanceled;
|
|
6046
|
+
}
|
|
6047
|
+
else if (kexReportEcho._reserved !== 0) {
|
|
6048
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
6049
|
+
message: `Security S2 bootstrapping failed: Invalid KEXReport received`,
|
|
6050
|
+
direction: "inbound",
|
|
6051
|
+
level: "warn",
|
|
6052
|
+
});
|
|
6053
|
+
await abort(cc_1.KEXFailType.WrongSecurityLevel);
|
|
6054
|
+
return Inclusion_1.SecurityBootstrapFailure.NodeCanceled;
|
|
6055
|
+
}
|
|
6056
|
+
else if (!kexReportEcho.isEncapsulatedWith(core_1.CommandClasses["Security 2"], cc_1.Security2Command.MessageEncapsulation)) {
|
|
6057
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
6058
|
+
message: `Security S2 bootstrapping failed: Command received without encryption`,
|
|
6059
|
+
direction: "inbound",
|
|
6060
|
+
level: "warn",
|
|
6061
|
+
});
|
|
6062
|
+
await abort(cc_1.KEXFailType.WrongSecurityLevel);
|
|
6063
|
+
return Inclusion_1.SecurityBootstrapFailure.S2WrongSecurityLevel;
|
|
6064
|
+
}
|
|
6065
|
+
else if (kexReportEcho.requestedKeys.length
|
|
6066
|
+
!== requested.securityClasses.length
|
|
6067
|
+
|| !kexReportEcho.requestedKeys.every((k) => requested.securityClasses.includes(k))) {
|
|
6068
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
6069
|
+
message: `Security S2 bootstrapping failed: Granted key mismatch.`,
|
|
6070
|
+
level: "warn",
|
|
6071
|
+
});
|
|
6072
|
+
await abort(cc_1.KEXFailType.WrongSecurityLevel);
|
|
6073
|
+
return Inclusion_1.SecurityBootstrapFailure.S2WrongSecurityLevel;
|
|
6074
|
+
}
|
|
6075
|
+
for (const key of kexSet.grantedKeys) {
|
|
6076
|
+
// Request network key and wait for including node to respond
|
|
6077
|
+
const keyReportPromise = this.driver.waitForCommand((cc) => cc instanceof cc_1.Security2CCNetworkKeyReport
|
|
6078
|
+
|| cc instanceof cc_1.Security2CCKEXFail, cc_1.inclusionTimeouts.TB4).catch(() => "timeout");
|
|
6079
|
+
await api.requestNetworkKey(key);
|
|
6080
|
+
const keyReport = await keyReportPromise;
|
|
6081
|
+
if (keyReport === "timeout")
|
|
6082
|
+
return abortTimeout();
|
|
6083
|
+
if (keyReport instanceof cc_1.Security2CCKEXFail) {
|
|
6084
|
+
return abortCanceled();
|
|
6085
|
+
}
|
|
6086
|
+
if (!keyReport.isEncapsulatedWith(core_1.CommandClasses["Security 2"], cc_1.Security2Command.MessageEncapsulation)) {
|
|
6087
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
6088
|
+
message: `Security S2 bootstrapping failed: Command received without encryption`,
|
|
6089
|
+
direction: "inbound",
|
|
6090
|
+
level: "warn",
|
|
6091
|
+
});
|
|
6092
|
+
await abort(cc_1.KEXFailType.WrongSecurityLevel);
|
|
6093
|
+
return Inclusion_1.SecurityBootstrapFailure.S2WrongSecurityLevel;
|
|
6094
|
+
}
|
|
6095
|
+
// Ensure it was received encrypted with the temporary key
|
|
6096
|
+
if (!securityManager.hasUsedSecurityClass(bootstrappingNode.id, core_1.SecurityClass.Temporary)) {
|
|
6097
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
6098
|
+
message: `Security S2 bootstrapping failed: Node used wrong key to communicate.`,
|
|
6099
|
+
level: "warn",
|
|
6100
|
+
});
|
|
6101
|
+
await abort(cc_1.KEXFailType.WrongSecurityLevel);
|
|
6102
|
+
return Inclusion_1.SecurityBootstrapFailure.S2WrongSecurityLevel;
|
|
6103
|
+
}
|
|
6104
|
+
const securityClass = keyReport.grantedKey;
|
|
6105
|
+
if (securityClass !== key) {
|
|
6106
|
+
// and that the granted key is the requested key
|
|
6107
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
6108
|
+
message: `Security S2 bootstrapping failed: Received key for wrong security class`,
|
|
6109
|
+
direction: "inbound",
|
|
6110
|
+
level: "warn",
|
|
6111
|
+
});
|
|
6112
|
+
await abort(cc_1.KEXFailType.DifferentKey);
|
|
6113
|
+
return Inclusion_1.SecurityBootstrapFailure.ParameterMismatch;
|
|
6114
|
+
}
|
|
6115
|
+
// Store the network key
|
|
6116
|
+
receivedKeys.set(securityClass, keyReport.networkKey);
|
|
6117
|
+
securityManager.setKey(securityClass, keyReport.networkKey);
|
|
6118
|
+
if (securityClass === core_1.SecurityClass.S0_Legacy) {
|
|
6119
|
+
// TODO: This is awkward to have here
|
|
6120
|
+
this.driver["_securityManager"] = new core_1.SecurityManager({
|
|
6121
|
+
ownNodeId: this._ownNodeId,
|
|
6122
|
+
networkKey: keyReport.networkKey,
|
|
6123
|
+
nonceTimeout: this.driver.options.timeouts.nonce,
|
|
6124
|
+
});
|
|
6125
|
+
}
|
|
6126
|
+
// Force nonce synchronization, then verify the network key
|
|
6127
|
+
securityManager.deleteNonce(bootstrappingNode.id);
|
|
6128
|
+
await api.withOptions({
|
|
6129
|
+
s2OverrideSecurityClass: securityClass,
|
|
6130
|
+
}).verifyNetworkKey();
|
|
6131
|
+
// Force nonce synchronization again for the temporary key
|
|
6132
|
+
securityManager.deleteNonce(bootstrappingNode.id);
|
|
6133
|
+
// Wait for including node to send its public key
|
|
6134
|
+
const transferEnd = await this.driver.waitForCommand((cc) => cc instanceof cc_1.Security2CCTransferEnd
|
|
6135
|
+
|| cc instanceof cc_1.Security2CCKEXFail, cc_1.inclusionTimeouts.TB5).catch(() => "timeout");
|
|
6136
|
+
if (transferEnd === "timeout")
|
|
6137
|
+
return abortTimeout();
|
|
6138
|
+
if (transferEnd instanceof cc_1.Security2CCKEXFail) {
|
|
6139
|
+
return abortCanceled();
|
|
6140
|
+
}
|
|
6141
|
+
if (!keyReport.isEncapsulatedWith(core_1.CommandClasses["Security 2"], cc_1.Security2Command.MessageEncapsulation)) {
|
|
6142
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
6143
|
+
message: `Security S2 bootstrapping failed: Command received without encryption`,
|
|
6144
|
+
direction: "inbound",
|
|
6145
|
+
level: "warn",
|
|
6146
|
+
});
|
|
6147
|
+
await abort(cc_1.KEXFailType.WrongSecurityLevel);
|
|
6148
|
+
return Inclusion_1.SecurityBootstrapFailure.S2WrongSecurityLevel;
|
|
6149
|
+
}
|
|
6150
|
+
else if (!transferEnd.keyVerified || transferEnd.keyRequestComplete) {
|
|
6151
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, {
|
|
6152
|
+
message: `Security S2 bootstrapping failed: Invalid TransferEnd received`,
|
|
6153
|
+
direction: "inbound",
|
|
6154
|
+
level: "warn",
|
|
6155
|
+
});
|
|
6156
|
+
await abort(cc_1.KEXFailType.WrongSecurityLevel);
|
|
6157
|
+
return Inclusion_1.SecurityBootstrapFailure.NodeCanceled;
|
|
6158
|
+
}
|
|
6159
|
+
}
|
|
6160
|
+
// Confirm end of bootstrapping
|
|
6161
|
+
await api.endKeyExchange();
|
|
6162
|
+
// Remember all security classes we were granted
|
|
6163
|
+
for (const securityClass of core_1.securityClassOrder) {
|
|
6164
|
+
bootstrappingNode.securityClasses.set(securityClass, kexSet.grantedKeys.includes(securityClass));
|
|
6165
|
+
}
|
|
6166
|
+
// And store the keys
|
|
6167
|
+
for (const [secClass, key] of receivedKeys) {
|
|
6168
|
+
this.driver.cacheSet(NetworkCache_1.cacheKeys.controller.securityKeys(secClass), key);
|
|
6169
|
+
}
|
|
6170
|
+
this.driver.driverLog.print(`Security S2 bootstrapping successful with these security classes:${[
|
|
6171
|
+
...bootstrappingNode.securityClasses.entries(),
|
|
6172
|
+
]
|
|
6173
|
+
.filter(([, v]) => v)
|
|
6174
|
+
.map(([k]) => `\n· ${(0, shared_1.getEnumMemberName)(core_1.SecurityClass, k)}`)
|
|
6175
|
+
.join("")}`);
|
|
6176
|
+
// success 🎉
|
|
6177
|
+
}
|
|
6178
|
+
catch (e) {
|
|
6179
|
+
let errorMessage = `Security S2 bootstrapping failed, no S2 security classes were granted`;
|
|
6180
|
+
let result = Inclusion_1.SecurityBootstrapFailure.Unknown;
|
|
6181
|
+
if (!(0, core_1.isZWaveError)(e)) {
|
|
6182
|
+
errorMessage += `: ${e}`;
|
|
6183
|
+
}
|
|
6184
|
+
else if (e.code === core_1.ZWaveErrorCodes.Controller_MessageExpired) {
|
|
6185
|
+
errorMessage += ": a secure inclusion timer has elapsed.";
|
|
6186
|
+
result = Inclusion_1.SecurityBootstrapFailure.Timeout;
|
|
6187
|
+
}
|
|
6188
|
+
else if (e.code !== core_1.ZWaveErrorCodes.Controller_MessageDropped
|
|
6189
|
+
&& e.code !== core_1.ZWaveErrorCodes.Controller_NodeTimeout) {
|
|
6190
|
+
errorMessage += `: ${e.message}`;
|
|
6191
|
+
}
|
|
6192
|
+
this.driver.controllerLog.logNode(bootstrappingNode.id, errorMessage, "warn");
|
|
6193
|
+
// Remember that we were NOT granted any S2 security classes
|
|
6194
|
+
unGrantSecurityClasses();
|
|
6195
|
+
bootstrappingNode.removeCC(core_1.CommandClasses["Security 2"]);
|
|
6196
|
+
return result;
|
|
6197
|
+
}
|
|
6198
|
+
finally {
|
|
6199
|
+
// Whatever happens, no further communication needs the temporary key
|
|
6200
|
+
deleteTempKey();
|
|
6201
|
+
}
|
|
6202
|
+
}
|
|
6203
|
+
async afterJoiningNetwork() {
|
|
6204
|
+
this.driver.driverLog.print("waiting for security bootstrapping...");
|
|
6205
|
+
const bootstrapInitStart = Date.now();
|
|
6206
|
+
const supportedCCs = (0, NodeInformationFrame_1.determineNIF)().supportedCCs;
|
|
6207
|
+
const supportsS0 = supportedCCs.includes(core_1.CommandClasses.Security);
|
|
6208
|
+
const supportsS2 = supportedCCs.includes(core_1.CommandClasses["Security 2"]);
|
|
6209
|
+
let initTimeout;
|
|
6210
|
+
let initPredicate;
|
|
6211
|
+
// KEX Get must be received:
|
|
6212
|
+
// - no later than 10..30 seconds after the inclusion if S0 is supported
|
|
6213
|
+
// - no later than 30 seconds after the inclusion if only S2 is supported
|
|
6214
|
+
// For simplicity, we wait the full 30s.
|
|
6215
|
+
// SecurityCCSchemeGet must be received no later than 10 seconds
|
|
6216
|
+
// after the inclusion if S0 is supported
|
|
6217
|
+
if (supportsS0 && supportsS2) {
|
|
6218
|
+
initTimeout = cc_1.inclusionTimeouts.TB1;
|
|
6219
|
+
initPredicate = (cc) => cc instanceof cc_1.SecurityCCSchemeGet
|
|
6220
|
+
|| cc instanceof cc_1.Security2CCKEXGet;
|
|
6221
|
+
}
|
|
6222
|
+
else if (supportsS2) {
|
|
6223
|
+
initTimeout = cc_1.inclusionTimeouts.TB1;
|
|
6224
|
+
initPredicate = (cc) => cc instanceof cc_1.Security2CCKEXGet;
|
|
6225
|
+
}
|
|
6226
|
+
else if (supportsS0) {
|
|
6227
|
+
initTimeout = 10000;
|
|
6228
|
+
initPredicate = (cc) => cc instanceof cc_1.SecurityCCSchemeGet;
|
|
6229
|
+
}
|
|
6230
|
+
else {
|
|
6231
|
+
initTimeout = 0;
|
|
6232
|
+
initPredicate = () => false;
|
|
6233
|
+
}
|
|
6234
|
+
const bootstrapInitPromise = this.driver.waitForCommand(initPredicate, initTimeout).catch(() => "timeout");
|
|
6235
|
+
const identifySelf = async () => {
|
|
6236
|
+
// Update own node ID and other controller flags.
|
|
6237
|
+
await this.identify().catch(shared_1.noop);
|
|
6238
|
+
// Notify applications that we're now part of a new network
|
|
6239
|
+
// The driver will point the databases to the new home ID
|
|
6240
|
+
this.emit("network found", this._homeId, this._ownNodeId);
|
|
6241
|
+
// Figure out the controller's network role
|
|
6242
|
+
await this.getControllerCapabilities().catch(shared_1.noop);
|
|
6243
|
+
// Create new node instances
|
|
6244
|
+
const { nodeIds } = await this.getSerialApiInitData();
|
|
6245
|
+
await this.initNodes(nodeIds, [], () => Promise.resolve());
|
|
6246
|
+
};
|
|
6247
|
+
// Do the self-identification while waiting for the bootstrap init command
|
|
6248
|
+
const [bootstrapInit] = await Promise.all([
|
|
6249
|
+
bootstrapInitPromise,
|
|
6250
|
+
identifySelf(),
|
|
6251
|
+
]);
|
|
6252
|
+
if (bootstrapInit === "timeout") {
|
|
6253
|
+
this.driver.controllerLog.print("No security bootstrapping command received, continuing without encryption...");
|
|
6254
|
+
}
|
|
6255
|
+
else if (bootstrapInit instanceof cc_1.SecurityCCSchemeGet) {
|
|
6256
|
+
const nodeId = bootstrapInit.nodeId;
|
|
6257
|
+
const bootstrappingNode = this.nodes.get(nodeId);
|
|
6258
|
+
if (!bootstrappingNode) {
|
|
6259
|
+
this.driver.controllerLog.logNode(nodeId, {
|
|
6260
|
+
message: "Received S2 bootstrap initiation from unknown node, ignoring...",
|
|
6261
|
+
level: "warn",
|
|
6262
|
+
});
|
|
6263
|
+
}
|
|
6264
|
+
else if (Date.now() - bootstrapInitStart > 10000) {
|
|
6265
|
+
// Received too late, S0 bootstrapping must not continue
|
|
6266
|
+
this.driver.controllerLog.print("Security S0 bootstrapping command received too late, continuing without encryption...");
|
|
6267
|
+
}
|
|
6268
|
+
else {
|
|
6269
|
+
// We definitely know that the node supports S0
|
|
6270
|
+
bootstrappingNode.addCC(core_1.CommandClasses.Security, {
|
|
6271
|
+
secure: true,
|
|
6272
|
+
isSupported: true,
|
|
6273
|
+
});
|
|
6274
|
+
this.driver.controllerLog.logNode(nodeId, {
|
|
6275
|
+
message: `Received S0 bootstrap initiation`,
|
|
6276
|
+
});
|
|
6277
|
+
const bootstrapResult = await this.expectSecurityBootstrapS0(bootstrappingNode);
|
|
6278
|
+
if (bootstrapResult !== undefined) {
|
|
6279
|
+
// If there was a failure, mark S0 as not supported
|
|
6280
|
+
bootstrappingNode.removeCC(core_1.CommandClasses.Security);
|
|
6281
|
+
}
|
|
6282
|
+
}
|
|
6283
|
+
}
|
|
6284
|
+
else if (bootstrapInit instanceof cc_1.Security2CCKEXGet) {
|
|
6285
|
+
const nodeId = bootstrapInit.nodeId;
|
|
6286
|
+
const bootstrappingNode = this.nodes.get(nodeId);
|
|
6287
|
+
if (!bootstrappingNode) {
|
|
6288
|
+
this.driver.controllerLog.logNode(nodeId, {
|
|
6289
|
+
message: "Received S2 bootstrap initiation from unknown node, ignoring...",
|
|
6290
|
+
level: "warn",
|
|
6291
|
+
});
|
|
6292
|
+
}
|
|
6293
|
+
else {
|
|
6294
|
+
// We definitely know that the node supports S2
|
|
6295
|
+
bootstrappingNode.addCC(core_1.CommandClasses["Security 2"], {
|
|
6296
|
+
secure: true,
|
|
6297
|
+
isSupported: true,
|
|
6298
|
+
});
|
|
6299
|
+
let grant;
|
|
6300
|
+
switch (this._joinNetworkOptions?.strategy) {
|
|
6301
|
+
// case JoinNetworkStrategy.Security_S2: {
|
|
6302
|
+
// grant = this._joinNetworkOptions.requested;
|
|
6303
|
+
// break;
|
|
6304
|
+
// }
|
|
6305
|
+
// case JoinNetworkStrategy.SmartStart:
|
|
6306
|
+
case Inclusion_1.JoinNetworkStrategy.Default:
|
|
6307
|
+
default: {
|
|
6308
|
+
// No options given, just request all keys
|
|
6309
|
+
grant = {
|
|
6310
|
+
securityClasses: [...core_1.securityClassOrder],
|
|
6311
|
+
clientSideAuth: false,
|
|
6312
|
+
};
|
|
6313
|
+
break;
|
|
6314
|
+
}
|
|
6315
|
+
}
|
|
6316
|
+
if (grant) {
|
|
6317
|
+
this.driver.controllerLog.logNode(nodeId, {
|
|
6318
|
+
message: `Received S2 bootstrap initiation, requesting keys: ${grant.securityClasses.map((sc) => `\n· ${(0, shared_1.getEnumMemberName)(core_1.SecurityClass, sc)}\n`).join("")}
|
|
6319
|
+
client-side auth: ${grant.clientSideAuth}`,
|
|
6320
|
+
});
|
|
6321
|
+
const bootstrapResult = await this
|
|
6322
|
+
.expectSecurityBootstrapS2(bootstrappingNode, grant);
|
|
6323
|
+
if (bootstrapResult !== undefined) {
|
|
6324
|
+
// If there was a failure, mark S2 as not supported
|
|
6325
|
+
bootstrappingNode.removeCC(core_1.CommandClasses["Security 2"]);
|
|
6326
|
+
}
|
|
6327
|
+
}
|
|
6328
|
+
}
|
|
6329
|
+
}
|
|
6330
|
+
this._joinNetworkOptions = undefined;
|
|
6331
|
+
// Read protocol information of all nodes
|
|
6332
|
+
for (const node of this.nodes.values()) {
|
|
6333
|
+
if (node.isControllerNode)
|
|
6334
|
+
continue;
|
|
6335
|
+
await node["queryProtocolInfo"]();
|
|
6336
|
+
}
|
|
6337
|
+
// Notify applications that joining the network is complete
|
|
6338
|
+
this.emit("network joined");
|
|
6339
|
+
}
|
|
5234
6340
|
};
|
|
5235
6341
|
exports.ZWaveController = ZWaveController;
|
|
5236
6342
|
exports.ZWaveController = ZWaveController = __decorate([
|