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