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.
Files changed (48) hide show
  1. package/bin/mock-server.js +2 -0
  2. package/build/lib/controller/Controller.d.ts +119 -9
  3. package/build/lib/controller/Controller.d.ts.map +1 -1
  4. package/build/lib/controller/Controller.js +1187 -81
  5. package/build/lib/controller/Controller.js.map +1 -1
  6. package/build/lib/controller/Inclusion.d.ts +44 -0
  7. package/build/lib/controller/Inclusion.d.ts.map +1 -1
  8. package/build/lib/controller/Inclusion.js +42 -1
  9. package/build/lib/controller/Inclusion.js.map +1 -1
  10. package/build/lib/driver/Driver.d.ts +16 -1
  11. package/build/lib/driver/Driver.d.ts.map +1 -1
  12. package/build/lib/driver/Driver.js +290 -97
  13. package/build/lib/driver/Driver.js.map +1 -1
  14. package/build/lib/driver/NetworkCache.d.ts +3 -0
  15. package/build/lib/driver/NetworkCache.d.ts.map +1 -1
  16. package/build/lib/driver/NetworkCache.js +37 -9
  17. package/build/lib/driver/NetworkCache.js.map +1 -1
  18. package/build/lib/driver/ZWaveOptions.d.ts +10 -3
  19. package/build/lib/driver/ZWaveOptions.d.ts.map +1 -1
  20. package/build/lib/driver/ZWaveOptions.js.map +1 -1
  21. package/build/lib/node/Node.d.ts +4 -3
  22. package/build/lib/node/Node.d.ts.map +1 -1
  23. package/build/lib/node/Node.js +130 -34
  24. package/build/lib/node/Node.js.map +1 -1
  25. package/build/lib/serialapi/application/ApplicationUpdateRequest.d.ts +5 -0
  26. package/build/lib/serialapi/application/ApplicationUpdateRequest.d.ts.map +1 -1
  27. package/build/lib/serialapi/application/ApplicationUpdateRequest.js +24 -1
  28. package/build/lib/serialapi/application/ApplicationUpdateRequest.js.map +1 -1
  29. package/build/lib/serialapi/capability/GetControllerCapabilitiesMessages.d.ts +2 -0
  30. package/build/lib/serialapi/capability/GetControllerCapabilitiesMessages.d.ts.map +1 -1
  31. package/build/lib/serialapi/capability/GetControllerCapabilitiesMessages.js +6 -0
  32. package/build/lib/serialapi/capability/GetControllerCapabilitiesMessages.js.map +1 -1
  33. package/build/lib/serialapi/capability/GetSerialApiInitDataMessages.d.ts +2 -9
  34. package/build/lib/serialapi/capability/GetSerialApiInitDataMessages.d.ts.map +1 -1
  35. package/build/lib/serialapi/capability/GetSerialApiInitDataMessages.js.map +1 -1
  36. package/build/lib/serialapi/capability/SerialAPISetupMessages.d.ts +27 -1
  37. package/build/lib/serialapi/capability/SerialAPISetupMessages.d.ts.map +1 -1
  38. package/build/lib/serialapi/capability/SerialAPISetupMessages.js +96 -1
  39. package/build/lib/serialapi/capability/SerialAPISetupMessages.js.map +1 -1
  40. package/build/lib/serialapi/network-mgmt/SetLearnModeMessages.d.ts +47 -0
  41. package/build/lib/serialapi/network-mgmt/SetLearnModeMessages.d.ts.map +1 -0
  42. package/build/lib/serialapi/network-mgmt/SetLearnModeMessages.js +137 -0
  43. package/build/lib/serialapi/network-mgmt/SetLearnModeMessages.js.map +1 -0
  44. package/build/lib/serialapi/nvm/ExtendedNVMOperationsMessages.d.ts +60 -0
  45. package/build/lib/serialapi/nvm/ExtendedNVMOperationsMessages.d.ts.map +1 -0
  46. package/build/lib/serialapi/nvm/ExtendedNVMOperationsMessages.js +201 -0
  47. package/build/lib/serialapi/nvm/ExtendedNVMOperationsMessages.js.map +1 -0
  48. 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
- _isPrimary;
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
- return this._isPrimary;
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.driver.controllerLog.print(`querying additional controller information...`);
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.driver.controllerLog.print(`querying controller capabilities...`);
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
- // There is no official API to query whether a given region is supported,
683
- // but there are ways to figure out if LR regions are.
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._isPrimary
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 = await node_util_1.default.promisify(node_crypto_1.default.generateKeyPair)("x25519");
2094
- const publicKey = (0, core_1.decodeX25519KeyDER)(keyPair.publicKey.export({
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: node_crypto_1.default.createPublicKey({
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.confirmGrantedKeys({
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
- // FIXME: Once supported in firmware, query the controller for supported regions instead of hardcoding
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 this.externalNVMOpen();
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 this
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 this.externalNVMClose();
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 this.externalNVMOpen();
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 this.externalNVMReadBuffer700(0, 0xff)).buffer.length || 48;
5071
+ const chunkSize = (await read(0, 0xff)).buffer.length || 48;
4748
5072
  // Close NVM and re-open again for writing
4749
- await this.externalNVMClose();
4750
- await this.externalNVMOpen();
5073
+ await close();
5074
+ await open();
4751
5075
  for (let offset = 0; offset < nvmData.length; offset += chunkSize) {
4752
- const { endOfFile } = await this.externalNVMWriteBuffer700(offset, nvmData.subarray(offset, offset + chunkSize));
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 this.externalNVMClose();
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([