zwave-js 8.6.1 → 8.7.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.
Files changed (42) hide show
  1. package/build/Utils.d.ts +2 -2
  2. package/build/Utils.d.ts.map +1 -1
  3. package/build/Utils.js +3 -1
  4. package/build/Utils.js.map +1 -1
  5. package/build/lib/commandclass/AssociationCC.d.ts.map +1 -1
  6. package/build/lib/commandclass/AssociationCC.js +2 -4
  7. package/build/lib/commandclass/AssociationCC.js.map +1 -1
  8. package/build/lib/commandclass/ManufacturerProprietaryCC.d.ts.map +1 -1
  9. package/build/lib/commandclass/ManufacturerProprietaryCC.js +15 -8
  10. package/build/lib/commandclass/ManufacturerProprietaryCC.js.map +1 -1
  11. package/build/lib/commandclass/MultiChannelAssociationCC.d.ts.map +1 -1
  12. package/build/lib/commandclass/MultiChannelAssociationCC.js +2 -4
  13. package/build/lib/commandclass/MultiChannelAssociationCC.js.map +1 -1
  14. package/build/lib/controller/AddNodeToNetworkRequest.d.ts +36 -8
  15. package/build/lib/controller/AddNodeToNetworkRequest.d.ts.map +1 -1
  16. package/build/lib/controller/AddNodeToNetworkRequest.js +111 -41
  17. package/build/lib/controller/AddNodeToNetworkRequest.js.map +1 -1
  18. package/build/lib/controller/ApplicationUpdateRequest.d.ts +16 -4
  19. package/build/lib/controller/ApplicationUpdateRequest.d.ts.map +1 -1
  20. package/build/lib/controller/ApplicationUpdateRequest.js +56 -15
  21. package/build/lib/controller/ApplicationUpdateRequest.js.map +1 -1
  22. package/build/lib/controller/Controller.d.ts +62 -20
  23. package/build/lib/controller/Controller.d.ts.map +1 -1
  24. package/build/lib/controller/Controller.js +629 -264
  25. package/build/lib/controller/Controller.js.map +1 -1
  26. package/build/lib/controller/Inclusion.d.ts +31 -4
  27. package/build/lib/controller/Inclusion.d.ts.map +1 -1
  28. package/build/lib/controller/Inclusion.js +15 -3
  29. package/build/lib/controller/Inclusion.js.map +1 -1
  30. package/build/lib/controller/RemoveNodeFromNetworkRequest.d.ts +15 -9
  31. package/build/lib/controller/RemoveNodeFromNetworkRequest.d.ts.map +1 -1
  32. package/build/lib/controller/RemoveNodeFromNetworkRequest.js +70 -41
  33. package/build/lib/controller/RemoveNodeFromNetworkRequest.js.map +1 -1
  34. package/build/lib/driver/Driver.d.ts +1 -1
  35. package/build/lib/driver/Driver.d.ts.map +1 -1
  36. package/build/lib/driver/Driver.js +154 -33
  37. package/build/lib/driver/Driver.js.map +1 -1
  38. package/build/lib/node/Node.d.ts +6 -0
  39. package/build/lib/node/Node.d.ts.map +1 -1
  40. package/build/lib/node/Node.js +20 -0
  41. package/build/lib/node/Node.js.map +1 -1
  42. package/package.json +10 -10
@@ -66,9 +66,11 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
66
66
  super();
67
67
  this.driver = driver;
68
68
  this._healNetworkActive = false;
69
- this._exclusionActive = false;
70
- this._inclusionActive = false;
69
+ this._provisioningList = [];
70
+ this._inclusionState = Inclusion_1.InclusionState.Idle;
71
+ this._smartStartEnabled = false;
71
72
  this._includeController = false;
73
+ this._unprovisionRemovedNode = false;
72
74
  this._healNetworkProgress = new Map();
73
75
  this._nodes = new Map();
74
76
  this._nodes.getOrThrow = function (nodeId) {
@@ -79,9 +81,9 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
79
81
  return node;
80
82
  }.bind(this._nodes);
81
83
  // register message handlers
82
- driver.registerRequestHandler(Constants_1.FunctionType.AddNodeToNetwork, this.handleAddNodeRequest.bind(this));
83
- driver.registerRequestHandler(Constants_1.FunctionType.RemoveNodeFromNetwork, this.handleRemoveNodeRequest.bind(this));
84
- driver.registerRequestHandler(Constants_1.FunctionType.ReplaceFailedNode, this.handleReplaceNodeRequest.bind(this));
84
+ driver.registerRequestHandler(Constants_1.FunctionType.AddNodeToNetwork, this.handleAddNodeStatusReport.bind(this));
85
+ driver.registerRequestHandler(Constants_1.FunctionType.RemoveNodeFromNetwork, this.handleRemoveNodeStatusReport.bind(this));
86
+ driver.registerRequestHandler(Constants_1.FunctionType.ReplaceFailedNode, this.handleReplaceNodeStatusReport.bind(this));
85
87
  }
86
88
  get libraryVersion() {
87
89
  return this._libraryVersion;
@@ -181,10 +183,28 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
181
183
  get supportsTimers() {
182
184
  return this._supportsTimers;
183
185
  }
186
+ /** Whether the controller is known to support soft reset */
187
+ get supportsSoftReset() {
188
+ return this._supportsSoftReset;
189
+ }
190
+ /** @internal */
191
+ set supportsSoftReset(value) {
192
+ this._supportsSoftReset = value;
193
+ }
184
194
  /** A dictionary of the nodes connected to this controller */
185
195
  get nodes() {
186
196
  return this._nodes;
187
197
  }
198
+ /** Returns the node with the given DSK */
199
+ getNodeByDSK(dsk) {
200
+ var _a;
201
+ if (typeof dsk === "string")
202
+ dsk = (0, core_1.dskFromString)(dsk);
203
+ for (const node of this._nodes.values()) {
204
+ if ((_a = node.dsk) === null || _a === void 0 ? void 0 : _a.equals(dsk))
205
+ return node;
206
+ }
207
+ }
188
208
  /** Returns the controller node's value DB */
189
209
  get valueDB() {
190
210
  return this._nodes.get(this._ownNodeId).valueDB;
@@ -202,9 +222,95 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
202
222
  const nodes = nodeIDs.map((id) => this._nodes.getOrThrow(id));
203
223
  return new VirtualNode_1.VirtualNode(undefined, this.driver, nodes);
204
224
  }
225
+ /** @internal */
226
+ get provisioningList() {
227
+ return this._provisioningList;
228
+ }
229
+ /** Adds the given entry (DSK and security classes) to the controller's SmartStart provisioning list or replaces an existing entry */
230
+ provisionSmartStartNode(entry) {
231
+ const index = this._provisioningList.findIndex((e) => e.dsk === entry.dsk);
232
+ if (index === -1) {
233
+ this._provisioningList.push(entry);
234
+ }
235
+ else {
236
+ this._provisioningList[index] = entry;
237
+ }
238
+ this.autoProvisionSmartStart();
239
+ void this.driver.saveNetworkToCache();
240
+ }
241
+ /**
242
+ * Removes the given DSK or node ID from the controller's SmartStart provisioning list.
243
+ *
244
+ * **Note:** If this entry corresponds to an included node, it will **NOT** be excluded
245
+ */
246
+ unprovisionSmartStartNode(dskOrNodeId) {
247
+ const index = this._provisioningList.findIndex((e) => e.dsk === dskOrNodeId ||
248
+ (typeof dskOrNodeId === "number" &&
249
+ "nodeId" in e &&
250
+ e.nodeId === dskOrNodeId));
251
+ if (index >= 0) {
252
+ this._provisioningList.splice(index, 1);
253
+ this.autoProvisionSmartStart();
254
+ void this.driver.saveNetworkToCache();
255
+ }
256
+ }
257
+ /**
258
+ * Returns the entry for the given DSK from the controller's SmartStart provisioning list.
259
+ */
260
+ getProvisioningEntry(dsk) {
261
+ return this._provisioningList.find((e) => e.dsk === dsk);
262
+ }
263
+ /**
264
+ * Returns all entries from the controller's SmartStart provisioning list.
265
+ */
266
+ getProvisioningEntries() {
267
+ // Make copies so no one can modify the internal list (except for user info)
268
+ return this._provisioningList.map((e) => {
269
+ const { dsk, securityClasses, nodeId, ...rest } = e;
270
+ return {
271
+ dsk,
272
+ securityClasses: [...securityClasses],
273
+ ...(nodeId != undefined ? { nodeId } : {}),
274
+ ...rest,
275
+ };
276
+ });
277
+ }
278
+ /** Returns whether the SmartStart provisioning list contains entries that have not been included yet */
279
+ hasPlannedProvisioningEntries() {
280
+ return this._provisioningList.some((e) => !("nodeId" in e));
281
+ }
282
+ /**
283
+ * @internal
284
+ * Automatically starts smart start inclusion if there are nodes pending inclusion.
285
+ */
286
+ autoProvisionSmartStart() {
287
+ if (this.hasPlannedProvisioningEntries()) {
288
+ // SmartStart should be enabled
289
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
290
+ void this.enableSmartStart().catch(() => { });
291
+ }
292
+ else {
293
+ // SmartStart should be disabled
294
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
295
+ void this.disableSmartStart().catch(() => { });
296
+ }
297
+ }
298
+ markNodeOnProvisioningList(node) {
299
+ // If this node's DSK is on the provisioning list, remember the node ID
300
+ if (node.dsk) {
301
+ const entry = this._provisioningList.find((e) => e.dsk === (0, core_1.dskToString)(node.dsk));
302
+ if (entry)
303
+ entry.nodeId = node.id;
304
+ }
305
+ }
306
+ unmarkNodeOnProvisioningList(nodeId) {
307
+ const entry = this._provisioningList.find((e) => "nodeId" in e && e.nodeId === nodeId);
308
+ if (entry)
309
+ delete entry.nodeId;
310
+ }
205
311
  /**
206
312
  * @internal
207
- * Queries the controller IDs which are necessary for the value DBs to be opened
313
+ * Queries the controller IDs and its Serial API capabilities
208
314
  */
209
315
  async identify() {
210
316
  // get the home and node id of the controller
@@ -215,14 +321,24 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
215
321
  this.driver.controllerLog.print(`received controller IDs:
216
322
  home ID: ${(0, shared_1.num2hex)(this._homeId)}
217
323
  own node ID: ${this._ownNodeId}`);
218
- }
219
- /**
220
- * @internal
221
- * Queries the controller IDs which are necessary for the value DBs to be opened
222
- */
223
- initializeControllerNode() {
224
- const nodeId = this._ownNodeId;
225
- this._nodes.set(nodeId, new Node_1.ZWaveNode(nodeId, this.driver, undefined, undefined, undefined));
324
+ // Figure out what the serial API can do
325
+ this.driver.controllerLog.print(`querying API capabilities...`);
326
+ const apiCaps = await this.driver.sendMessage(new GetSerialApiCapabilitiesMessages_1.GetSerialApiCapabilitiesRequest(this.driver), {
327
+ supportCheck: false,
328
+ });
329
+ this._serialApiVersion = apiCaps.serialApiVersion;
330
+ this._manufacturerId = apiCaps.manufacturerId;
331
+ this._productType = apiCaps.productType;
332
+ this._productId = apiCaps.productId;
333
+ this._supportedFunctionTypes = apiCaps.supportedFunctionTypes;
334
+ this.driver.controllerLog.print(`received API capabilities:
335
+ serial API version: ${this._serialApiVersion}
336
+ manufacturer ID: ${(0, shared_1.num2hex)(this._manufacturerId)}
337
+ product type: ${(0, shared_1.num2hex)(this._productType)}
338
+ product ID: ${(0, shared_1.num2hex)(this._productId)}
339
+ supported functions: ${this._supportedFunctionTypes
340
+ .map((fn) => `\n · ${Constants_1.FunctionType[fn]} (${(0, shared_1.num2hex)(fn)})`)
341
+ .join("")}`);
226
342
  }
227
343
  /**
228
344
  * @internal
@@ -230,7 +346,6 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
230
346
  * @param restoreFromCache Asynchronous callback for the driver to restore the network from cache after nodes are created
231
347
  */
232
348
  async interview(restoreFromCache) {
233
- this.driver.controllerLog.print("beginning interview...");
234
349
  // get basic controller version info
235
350
  this.driver.controllerLog.print(`querying version info...`);
236
351
  const version = await this.driver.sendMessage(new GetControllerVersionMessages_1.GetControllerVersionRequest(this.driver), {
@@ -258,25 +373,6 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
258
373
  is SIS present: ${this._isSISPresent}
259
374
  was real primary: ${this._wasRealPrimary}
260
375
  is a SUC: ${this._isStaticUpdateController}`);
261
- // find out which part of the API is supported
262
- this.driver.controllerLog.print(`querying API capabilities...`);
263
- const apiCaps = await this.driver.sendMessage(new GetSerialApiCapabilitiesMessages_1.GetSerialApiCapabilitiesRequest(this.driver), {
264
- supportCheck: false,
265
- });
266
- this._serialApiVersion = apiCaps.serialApiVersion;
267
- this._manufacturerId = apiCaps.manufacturerId;
268
- this._productType = apiCaps.productType;
269
- this._productId = apiCaps.productId;
270
- this._supportedFunctionTypes = apiCaps.supportedFunctionTypes;
271
- this.driver.controllerLog.print(`received API capabilities:
272
- serial API version: ${this._serialApiVersion}
273
- manufacturer ID: ${(0, shared_1.num2hex)(this._manufacturerId)}
274
- product type: ${(0, shared_1.num2hex)(this._productType)}
275
- product ID: ${(0, shared_1.num2hex)(this._productId)}
276
- supported functions: ${this._supportedFunctionTypes
277
- .map((fn) => `\n · ${Constants_1.FunctionType[fn]} (${(0, shared_1.num2hex)(fn)})`)
278
- .join("")}`);
279
- // now we can check if a function is supported
280
376
  // Figure out which sub commands of SerialAPISetup are supported
281
377
  if (this.isFunctionSupported(Constants_1.FunctionType.SerialAPISetup)) {
282
378
  this.driver.controllerLog.print(`querying serial API setup capabilities...`);
@@ -353,9 +449,6 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
353
449
  ]);
354
450
  // create an empty entry in the nodes map so we can initialize them afterwards
355
451
  for (const nodeId of initData.nodeIds) {
356
- // The controller node is created separately
357
- if (nodeId === this._ownNodeId)
358
- continue;
359
452
  this._nodes.set(nodeId, new Node_1.ZWaveNode(nodeId, this.driver, undefined, undefined, undefined,
360
453
  // Use the previously created index to avoid doing extra work when creating the value DB
361
454
  this.createValueDBForNode(nodeId, valueDBIndexes.get(nodeId))));
@@ -465,9 +558,24 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
465
558
  }
466
559
  });
467
560
  }
561
+ get inclusionState() {
562
+ return this._inclusionState;
563
+ }
564
+ setInclusionState(state) {
565
+ if (this._inclusionState === state)
566
+ return;
567
+ this._inclusionState = state;
568
+ if (state === Inclusion_1.InclusionState.Idle && this._smartStartEnabled) {
569
+ // If Smart Start was enabled before the inclusion/exclusion,
570
+ // enable it again and ignore errors
571
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
572
+ this.enableSmartStart().catch(() => { });
573
+ }
574
+ }
468
575
  async beginInclusion(options) {
469
- // don't start it twice
470
- if (this._inclusionActive || this._exclusionActive) {
576
+ if (this._inclusionState === Inclusion_1.InclusionState.Including ||
577
+ this._inclusionState === Inclusion_1.InclusionState.Excluding ||
578
+ this._inclusionState === Inclusion_1.InclusionState.Busy) {
471
579
  return false;
472
580
  }
473
581
  if (options == undefined) {
@@ -483,97 +591,280 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
483
591
  };
484
592
  }
485
593
  // Protect against invalid inclusion options
486
- if (!(options.strategy in Inclusion_1.InclusionStrategy)) {
594
+ if (!(options.strategy in Inclusion_1.InclusionStrategy) ||
595
+ // @ts-expect-error We're checking for user errors
596
+ options.strategy === Inclusion_1.InclusionStrategy.SmartStart) {
487
597
  throw new core_1.ZWaveError(`Invalid inclusion strategy: ${options.strategy}`, core_1.ZWaveErrorCodes.Argument_Invalid);
488
598
  }
489
- if (options.strategy === Inclusion_1.InclusionStrategy.SmartStart) {
490
- throw new core_1.ZWaveError(`SmartStart is not supported yet!`, core_1.ZWaveErrorCodes.Driver_NotSupported);
599
+ if (this._inclusionState === Inclusion_1.InclusionState.SmartStart) {
600
+ // Disable listening mode so we can switch to inclusion mode
601
+ await this.stopInclusion();
491
602
  }
492
- this._inclusionActive = true;
603
+ this.setInclusionState(Inclusion_1.InclusionState.Including);
493
604
  this._inclusionOptions = options;
494
- this.driver.controllerLog.print(`Starting inclusion process with strategy ${(0, shared_1.getEnumMemberName)(Inclusion_1.InclusionStrategy, options.strategy)}...`);
495
- // create the promise we're going to return
496
- this._beginInclusionPromise = (0, deferred_promise_1.createDeferredPromise)();
497
- // kick off the inclusion process
605
+ try {
606
+ this.driver.controllerLog.print(`Starting inclusion process with strategy ${(0, shared_1.getEnumMemberName)(Inclusion_1.InclusionStrategy, options.strategy)}...`);
607
+ // kick off the inclusion process
608
+ await this.driver.sendMessage(new AddNodeToNetworkRequest_1.AddNodeToNetworkRequest(this.driver, {
609
+ addNodeType: AddNodeToNetworkRequest_1.AddNodeType.Any,
610
+ highPower: true,
611
+ networkWide: true,
612
+ }));
613
+ this.driver.controllerLog.print(`The controller is now ready to add nodes`);
614
+ this.emit("inclusion started",
615
+ // TODO: Remove first parameter in next major version
616
+ options.strategy !== Inclusion_1.InclusionStrategy.Insecure, options.strategy);
617
+ }
618
+ catch (e) {
619
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
620
+ if ((0, core_1.isZWaveError)(e) &&
621
+ e.code === core_1.ZWaveErrorCodes.Controller_CallbackNOK) {
622
+ this.driver.controllerLog.print(`Starting the inclusion failed`, "error");
623
+ throw new core_1.ZWaveError("The inclusion could not be started.", core_1.ZWaveErrorCodes.Controller_InclusionFailed);
624
+ }
625
+ throw e;
626
+ }
627
+ return true;
628
+ }
629
+ /** @internal */
630
+ async beginInclusionSmartStart(provisioningEntry) {
631
+ if (this._inclusionState === Inclusion_1.InclusionState.Including ||
632
+ this._inclusionState === Inclusion_1.InclusionState.Excluding ||
633
+ this._inclusionState === Inclusion_1.InclusionState.Busy) {
634
+ return false;
635
+ }
636
+ // Disable listening mode so we can switch to inclusion mode
637
+ await this.stopInclusion();
638
+ this.setInclusionState(Inclusion_1.InclusionState.Including);
639
+ this._inclusionOptions = {
640
+ strategy: Inclusion_1.InclusionStrategy.SmartStart,
641
+ provisioning: provisioningEntry,
642
+ };
643
+ try {
644
+ this.driver.controllerLog.print(`Including SmartStart node with DSK ${provisioningEntry.dsk}`);
645
+ // kick off the inclusion process
646
+ const dskBuffer = (0, core_1.dskFromString)(provisioningEntry.dsk);
647
+ await this.driver.sendMessage(new AddNodeToNetworkRequest_1.AddNodeDSKToNetworkRequest(this.driver, {
648
+ nwiHomeId: (0, core_1.nwiHomeIdFromDSK)(dskBuffer),
649
+ authHomeId: (0, core_1.authHomeIdFromDSK)(dskBuffer),
650
+ highPower: true,
651
+ networkWide: true,
652
+ }));
653
+ this.emit("inclusion started",
654
+ // TODO: Remove first parameter in next major version
655
+ true, Inclusion_1.InclusionStrategy.SmartStart);
656
+ }
657
+ catch (e) {
658
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
659
+ // Error handling for this happens at the call site
660
+ throw e;
661
+ }
662
+ return true;
663
+ }
664
+ /**
665
+ * Is used internally to stop an active inclusion process without waiting for a confirmation
666
+ * @internal
667
+ */
668
+ async stopInclusionNoCallback() {
498
669
  await this.driver.sendMessage(new AddNodeToNetworkRequest_1.AddNodeToNetworkRequest(this.driver, {
499
- addNodeType: AddNodeToNetworkRequest_1.AddNodeType.Any,
670
+ callbackId: 0,
671
+ addNodeType: AddNodeToNetworkRequest_1.AddNodeType.Stop,
500
672
  highPower: true,
501
673
  networkWide: true,
502
674
  }));
503
- return this._beginInclusionPromise;
675
+ this.driver.controllerLog.print(`The inclusion process was stopped`);
676
+ this.emit("inclusion stopped");
504
677
  }
505
- /** Is used internally to stop an active inclusion process without creating deadlocks */
506
- async stopInclusionInternal() {
507
- // don't stop it twice
508
- if (!this._inclusionActive)
509
- return;
510
- this._inclusionActive = false;
511
- this.driver.controllerLog.print(`stopping inclusion process...`);
512
- // create the promise we're going to return
513
- this._stopInclusionPromise = (0, deferred_promise_1.createDeferredPromise)();
514
- // kick off the inclusion process
515
- await this.driver.sendMessage(new AddNodeToNetworkRequest_1.AddNodeToNetworkRequest(this.driver, {
678
+ /**
679
+ * Finishes an inclusion process. This must only be called after the ProtocolDone status is received.
680
+ * Returns the ID of the newly added node.
681
+ */
682
+ async finishInclusion() {
683
+ this.driver.controllerLog.print(`finishing inclusion process...`);
684
+ const response = await this.driver.sendMessage(new AddNodeToNetworkRequest_1.AddNodeToNetworkRequest(this.driver, {
516
685
  addNodeType: AddNodeToNetworkRequest_1.AddNodeType.Stop,
517
686
  highPower: true,
518
687
  networkWide: true,
519
688
  }));
520
- // Don't await the promise or we create a deadlock
521
- void this._stopInclusionPromise.then(() => {
522
- this.driver.controllerLog.print(`the inclusion process was stopped`);
523
- this.emit("inclusion stopped");
524
- });
689
+ if (response.status === AddNodeToNetworkRequest_1.AddNodeStatus.Done) {
690
+ return response.statusContext.nodeId;
691
+ }
692
+ this.driver.controllerLog.print(`Finishing the inclusion failed`, "error");
693
+ throw new core_1.ZWaveError("Finishing the inclusion failed", core_1.ZWaveErrorCodes.Controller_InclusionFailed);
525
694
  }
526
695
  /**
527
696
  * Stops an active inclusion process. Resolves to true when the controller leaves inclusion mode,
528
697
  * and false if the inclusion was not active.
529
698
  */
530
699
  async stopInclusion() {
531
- // don't stop it twice
532
- if (!this._inclusionActive)
700
+ if (this._inclusionState !== Inclusion_1.InclusionState.Including) {
533
701
  return false;
534
- await this.stopInclusionInternal();
535
- return this._stopInclusionPromise;
702
+ }
703
+ this.driver.controllerLog.print(`stopping inclusion process...`);
704
+ try {
705
+ // stop the inclusion process
706
+ await this.driver.sendMessage(new AddNodeToNetworkRequest_1.AddNodeToNetworkRequest(this.driver, {
707
+ addNodeType: AddNodeToNetworkRequest_1.AddNodeType.Stop,
708
+ highPower: true,
709
+ networkWide: true,
710
+ }));
711
+ this.driver.controllerLog.print(`The inclusion process was stopped`);
712
+ this.emit("inclusion stopped");
713
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
714
+ return true;
715
+ }
716
+ catch (e) {
717
+ if ((0, core_1.isZWaveError)(e) &&
718
+ e.code === core_1.ZWaveErrorCodes.Controller_CallbackNOK) {
719
+ this.driver.controllerLog.print(`Stopping the inclusion failed`, "error");
720
+ throw new core_1.ZWaveError("The inclusion could not be stopped.", core_1.ZWaveErrorCodes.Controller_InclusionFailed);
721
+ }
722
+ throw e;
723
+ }
724
+ }
725
+ /**
726
+ * Puts the controller into listening mode for Smart Start inclusion.
727
+ * Whenever a node on the provisioning list announces itself, it will automatically be added.
728
+ *
729
+ * Resolves to `true` when the listening mode is started or was active, and `false` if it is scheduled for later activation.
730
+ */
731
+ async enableSmartStart() {
732
+ if (this._smartStartEnabled)
733
+ return true;
734
+ this._smartStartEnabled = true;
735
+ if (this._inclusionState === Inclusion_1.InclusionState.Idle) {
736
+ this.setInclusionState(Inclusion_1.InclusionState.SmartStart);
737
+ this.driver.controllerLog.print(`Enabling Smart Start listening mode...`);
738
+ try {
739
+ await this.driver.sendMessage(new AddNodeToNetworkRequest_1.EnableSmartStartListenRequest(this.driver, {}));
740
+ this.driver.controllerLog.print(`Smart Start listening mode enabled`);
741
+ return true;
742
+ }
743
+ catch (e) {
744
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
745
+ this.driver.controllerLog.print(`Smart Start listening mode could not be enabled: ${(0, shared_1.getErrorMessage)(e)}`, "error");
746
+ throw e;
747
+ }
748
+ }
749
+ else if (this._inclusionState === Inclusion_1.InclusionState.SmartStart) {
750
+ return true;
751
+ }
752
+ else {
753
+ this.driver.controllerLog.print(`Smart Start listening mode scheduled for later activation...`);
754
+ return false;
755
+ }
756
+ }
757
+ /**
758
+ * Disables the listening mode for Smart Start inclusion.
759
+ *
760
+ * Resolves to `true` when the listening mode is stopped, and `false` if was not active.
761
+ */
762
+ async disableSmartStart() {
763
+ if (!this._smartStartEnabled)
764
+ return false;
765
+ this._smartStartEnabled = false;
766
+ if (this._inclusionState === Inclusion_1.InclusionState.SmartStart) {
767
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
768
+ this.driver.controllerLog.print(`disabling Smart Start listening mode...`);
769
+ try {
770
+ await this.stopInclusion();
771
+ this.driver.controllerLog.print(`Smart Start listening mode disabled`);
772
+ return true;
773
+ }
774
+ catch (e) {
775
+ this.setInclusionState(Inclusion_1.InclusionState.SmartStart);
776
+ this.driver.controllerLog.print(`Smart Start listening mode could not be disabled: ${(0, shared_1.getErrorMessage)(e)}`, "error");
777
+ throw e;
778
+ }
779
+ }
780
+ else {
781
+ this.driver.controllerLog.print(`Smart Start listening mode disabled`);
782
+ return true;
783
+ }
536
784
  }
537
785
  /**
538
786
  * Starts the exclusion process of new nodes.
539
- * Resolves to true when the process was started,
540
- * and false if an inclusion or exclusion process was already active
787
+ * Resolves to true when the process was started, and false if an inclusion or exclusion process was already active.
788
+ *
789
+ * @param unprovision Whether the removed node should also be removed from the Smart Start provisioning list.
541
790
  */
542
- async beginExclusion() {
543
- // don't start it twice
544
- if (this._inclusionActive || this._exclusionActive)
791
+ async beginExclusion(unprovision = false) {
792
+ if (this._inclusionState === Inclusion_1.InclusionState.Including ||
793
+ this._inclusionState === Inclusion_1.InclusionState.Excluding ||
794
+ this._inclusionState === Inclusion_1.InclusionState.Busy) {
545
795
  return false;
546
- this._exclusionActive = true;
796
+ }
797
+ if (this._inclusionState === Inclusion_1.InclusionState.SmartStart) {
798
+ // Disable listening mode so we can switch to exclusion mode
799
+ await this.stopInclusion();
800
+ }
801
+ this.setInclusionState(Inclusion_1.InclusionState.Excluding);
547
802
  this.driver.controllerLog.print(`starting exclusion process...`);
548
- // create the promise we're going to return
549
- this._beginInclusionPromise = (0, deferred_promise_1.createDeferredPromise)();
550
- // kick off the inclusion process
551
- await this.driver.sendMessage(new RemoveNodeFromNetworkRequest_1.RemoveNodeFromNetworkRequest(this.driver, {
552
- removeNodeType: RemoveNodeFromNetworkRequest_1.RemoveNodeType.Any,
553
- highPower: true,
554
- networkWide: true,
555
- }));
556
- return this._beginInclusionPromise;
803
+ try {
804
+ // kick off the inclusion process
805
+ await this.driver.sendMessage(new RemoveNodeFromNetworkRequest_1.RemoveNodeFromNetworkRequest(this.driver, {
806
+ removeNodeType: RemoveNodeFromNetworkRequest_1.RemoveNodeType.Any,
807
+ highPower: true,
808
+ networkWide: true,
809
+ }));
810
+ this.driver.controllerLog.print(`The controller is now ready to remove nodes`);
811
+ this._unprovisionRemovedNode = unprovision;
812
+ this.emit("exclusion started");
813
+ return true;
814
+ }
815
+ catch (e) {
816
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
817
+ if ((0, core_1.isZWaveError)(e) &&
818
+ e.code === core_1.ZWaveErrorCodes.Controller_CallbackNOK) {
819
+ this.driver.controllerLog.print(`Starting the exclusion failed`, "error");
820
+ throw new core_1.ZWaveError("The exclusion could not be started.", core_1.ZWaveErrorCodes.Controller_ExclusionFailed);
821
+ }
822
+ throw e;
823
+ }
557
824
  }
558
- /** Is used internally to stop an active inclusion process without creating deadlocks */
559
- async stopExclusionInternal() {
560
- // don't stop it twice
561
- if (!this._exclusionActive)
562
- return;
563
- this._exclusionActive = false;
564
- this.driver.controllerLog.print(`stopping exclusion process...`);
565
- // create the promise we're going to return
566
- this._stopInclusionPromise = (0, deferred_promise_1.createDeferredPromise)();
567
- // kick off the inclusion process
825
+ /**
826
+ * Is used internally to stop an active exclusion process without waiting for confirmation
827
+ * @internal
828
+ */
829
+ async stopExclusionNoCallback() {
568
830
  await this.driver.sendMessage(new RemoveNodeFromNetworkRequest_1.RemoveNodeFromNetworkRequest(this.driver, {
831
+ callbackId: 0,
569
832
  removeNodeType: RemoveNodeFromNetworkRequest_1.RemoveNodeType.Stop,
570
833
  highPower: true,
571
834
  networkWide: true,
572
835
  }));
573
- void this._stopInclusionPromise.then(() => {
836
+ this.driver.controllerLog.print(`the exclusion process was stopped`);
837
+ this.emit("exclusion stopped");
838
+ }
839
+ /**
840
+ * Stops an active exclusion process. Resolves to true when the controller leaves exclusion mode,
841
+ * and false if the inclusion was not active.
842
+ */
843
+ async stopExclusion() {
844
+ if (this._inclusionState !== Inclusion_1.InclusionState.Excluding) {
845
+ return false;
846
+ }
847
+ this.driver.controllerLog.print(`stopping exclusion process...`);
848
+ try {
849
+ // kick off the inclusion process
850
+ await this.driver.sendMessage(new RemoveNodeFromNetworkRequest_1.RemoveNodeFromNetworkRequest(this.driver, {
851
+ removeNodeType: RemoveNodeFromNetworkRequest_1.RemoveNodeType.Stop,
852
+ highPower: true,
853
+ networkWide: true,
854
+ }));
574
855
  this.driver.controllerLog.print(`the exclusion process was stopped`);
575
856
  this.emit("exclusion stopped");
576
- });
857
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
858
+ return true;
859
+ }
860
+ catch (e) {
861
+ if ((0, core_1.isZWaveError)(e) &&
862
+ e.code === core_1.ZWaveErrorCodes.Controller_CallbackNOK) {
863
+ this.driver.controllerLog.print(`Stopping the exclusion failed`, "error");
864
+ throw new core_1.ZWaveError("The exclusion could not be stopped.", core_1.ZWaveErrorCodes.Controller_ExclusionFailed);
865
+ }
866
+ throw e;
867
+ }
577
868
  }
578
869
  async secureBootstrapS0(node, assumeSupported = false) {
579
870
  if (!this.driver.securityManager) {
@@ -665,7 +956,34 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
665
956
  version: 1,
666
957
  });
667
958
  }
668
- const { userCallbacks } = this._inclusionOptions;
959
+ let userCallbacks;
960
+ const inclusionOptions = this
961
+ ._inclusionOptions;
962
+ if ("provisioning" in inclusionOptions) {
963
+ // SmartStart and S2 with QR code are pre-provisioned, so we don't need to ask the user for anything
964
+ userCallbacks = {
965
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
966
+ abort() { },
967
+ grantSecurityClasses: (requested) => {
968
+ return Promise.resolve({
969
+ clientSideAuth: false,
970
+ securityClasses: requested.securityClasses.filter((r) => inclusionOptions.provisioning.securityClasses.includes(r)),
971
+ });
972
+ },
973
+ validateDSKAndEnterPIN: (dsk) => {
974
+ const fullDSK = inclusionOptions.provisioning.dsk;
975
+ const pin = fullDSK.slice(0, 5);
976
+ // Make sure the DSK matches
977
+ if (pin + dsk !== fullDSK)
978
+ return Promise.resolve(false);
979
+ return Promise.resolve(pin);
980
+ },
981
+ };
982
+ }
983
+ else {
984
+ // Use the provided callbacks
985
+ userCallbacks = inclusionOptions.userCallbacks;
986
+ }
669
987
  const deleteTempKey = () => {
670
988
  var _a, _b;
671
989
  // Whatever happens, no further communication needs the temporary key
@@ -787,16 +1105,27 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
787
1105
  return abort();
788
1106
  }
789
1107
  const nodePublicKey = pubKeyResponse.publicKey;
790
- // This is the starting point of the timer TAI2. Depending on how long the user takes to confirm,
791
- // the node has less time to send its KEXSet
1108
+ // This is the starting point of the timer TAI2.
792
1109
  const timerStartTAI2 = Date.now();
1110
+ // Generate ECDH key pair. We need to immediately send the other node our public key,
1111
+ // so it won't abort bootstrapping
1112
+ const keyPair = await util_1.default.promisify(crypto_1.default.generateKeyPair)("x25519");
1113
+ const publicKey = (0, core_1.decodeX25519KeyDER)(keyPair.publicKey.export({
1114
+ type: "spki",
1115
+ format: "der",
1116
+ }));
1117
+ await api.sendPublicKey(publicKey);
1118
+ // After this, the node will start sending us a KEX SET every 10 seconds.
1119
+ // We won't be able to decode it until the DSK was verified
793
1120
  if (grantedKeys.includes(core_1.SecurityClass.S2_AccessControl) ||
794
1121
  grantedKeys.includes(core_1.SecurityClass.S2_Authenticated)) {
795
1122
  // For authenticated encryption, the DSK (first 16 bytes of the public key) is obfuscated (missing the first 2 bytes)
796
1123
  // Request the user to enter the missing part as a 5-digit PIN
797
1124
  const dsk = (0, core_1.dskToString)(nodePublicKey.slice(0, 16)).slice(5);
1125
+ // The time the user has to enter the PIN is limited by the timeout TAI2
1126
+ const tai2RemainingMs = shared_2.inclusionTimeouts.TAI2 - (Date.now() - timerStartTAI2);
798
1127
  const pinResult = await Promise.race([
799
- (0, async_1.wait)(shared_2.inclusionTimeouts.TAI2, true).then(() => false),
1128
+ (0, async_1.wait)(tai2RemainingMs, true).then(() => false),
800
1129
  userCallbacks
801
1130
  .validateDSKAndEnterPIN(dsk)
802
1131
  // ignore errors in application callbacks
@@ -814,12 +1143,8 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
814
1143
  // Fill in the missing two bytes of the public key
815
1144
  nodePublicKey.writeUInt16BE(parseInt(pinResult, 10), 0);
816
1145
  }
817
- // Generate ECDH key pair. Z-Wave works with the "raw" keys, so this is a tad complicated
818
- const keyPair = await util_1.default.promisify(crypto_1.default.generateKeyPair)("x25519");
819
- const publicKey = (0, core_1.decodeX25519KeyDER)(keyPair.publicKey.export({
820
- type: "spki",
821
- format: "der",
822
- }));
1146
+ // After the user has verified the DSK, we can derive the shared secret
1147
+ // Z-Wave works with the "raw" keys, so this is a tad complicated
823
1148
  const sharedSecret = crypto_1.default.diffieHellman({
824
1149
  publicKey: crypto_1.default.createPublicKey({
825
1150
  key: (0, core_1.encodeX25519KeyDERSPKI)(nodePublicKey),
@@ -828,15 +1153,14 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
828
1153
  }),
829
1154
  privateKey: keyPair.privateKey,
830
1155
  });
831
- // Derive temporary key from ECDH key pair
1156
+ // Derive temporary key from ECDH key pair - this will allow us to receive the node's KEX SET commands
832
1157
  const tempKeys = (0, core_1.deriveTempKeys)((0, core_1.computePRK)(sharedSecret, publicKey, nodePublicKey));
1158
+ this.driver.securityManager2.deleteNonce(node.id);
833
1159
  this.driver.securityManager2.tempKeys.set(node.id, {
834
1160
  keyCCM: tempKeys.tempKeyCCM,
835
1161
  personalizationString: tempKeys.tempPersonalizationString,
836
1162
  });
837
- await api.sendPublicKey(publicKey);
838
- // Wait until the encrypted KEXSet from the node was received
839
- // (if there is even time left)
1163
+ // Now wait for the next KEXSet from the node (if there is even time left)
840
1164
  const tai2RemainingMs = shared_2.inclusionTimeouts.TAI2 - (Date.now() - timerStartTAI2);
841
1165
  if (tai2RemainingMs < 1) {
842
1166
  this.driver.controllerLog.logNode(node.id, {
@@ -953,6 +1277,8 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
953
1277
  for (const securityClass of core_1.securityClassOrder) {
954
1278
  node.securityClasses.set(securityClass, grantedKeys.includes(securityClass));
955
1279
  }
1280
+ // Remember the DSK (first 16 bytes of the public key)
1281
+ node.dsk = nodePublicKey.slice(0, 16);
956
1282
  this.driver.controllerLog.logNode(node.id, {
957
1283
  message: `Security S2 bootstrapping successful with these security classes:${[
958
1284
  ...node.securityClasses.entries(),
@@ -1079,60 +1405,32 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
1079
1405
  }
1080
1406
  }
1081
1407
  }
1082
- /**
1083
- * Stops an active exclusion process. Resolves to true when the controller leaves exclusion mode,
1084
- * and false if the inclusion was not active.
1085
- */
1086
- async stopExclusion() {
1087
- // don't stop it twice
1088
- if (!this._exclusionActive)
1089
- return false;
1090
- await this.stopExclusionInternal();
1091
- return this._stopInclusionPromise;
1092
- }
1093
1408
  /**
1094
1409
  * Is called when an AddNode request is received from the controller.
1095
1410
  * Handles and controls the inclusion process.
1096
1411
  */
1097
- async handleAddNodeRequest(msg) {
1412
+ async handleAddNodeStatusReport(msg) {
1098
1413
  var _a, _b, _c, _d, _e, _f, _g;
1099
1414
  this.driver.controllerLog.print(`handling add node request (status = ${AddNodeToNetworkRequest_1.AddNodeStatus[msg.status]})`);
1100
- if ((!this._inclusionActive && msg.status !== AddNodeToNetworkRequest_1.AddNodeStatus.Done) ||
1415
+ if (this._inclusionState !== Inclusion_1.InclusionState.Including ||
1101
1416
  this._inclusionOptions == undefined) {
1102
1417
  this.driver.controllerLog.print(` inclusion is NOT active, ignoring it...`);
1103
1418
  return true; // Don't invoke any more handlers
1104
1419
  }
1105
1420
  switch (msg.status) {
1106
- case AddNodeToNetworkRequest_1.AddNodeStatus.Ready:
1107
- // this is called when inclusion was started successfully
1108
- this.driver.controllerLog.print(` the controller is now ready to add nodes`);
1109
- if (this._beginInclusionPromise != null) {
1110
- this._beginInclusionPromise.resolve(true);
1111
- this.emit("inclusion started",
1112
- // TODO: Remove first parameter in next major version
1113
- this._inclusionOptions.strategy !==
1114
- Inclusion_1.InclusionStrategy.Insecure, this._inclusionOptions.strategy);
1115
- }
1116
- break;
1117
1421
  case AddNodeToNetworkRequest_1.AddNodeStatus.Failed:
1118
- // this is called when inclusion could not be started...
1119
- if (this._beginInclusionPromise != null) {
1120
- this.driver.controllerLog.print(` starting the inclusion failed`, "error");
1121
- this._beginInclusionPromise.reject(new core_1.ZWaveError("The inclusion could not be started.", core_1.ZWaveErrorCodes.Controller_InclusionFailed));
1122
- }
1123
- else {
1124
- // ...or adding a node failed
1125
- this.driver.controllerLog.print(` adding the node failed`, "error");
1126
- this.emit("inclusion failed");
1127
- }
1422
+ // This code is handled elsewhere for starting the inclusion, so this means
1423
+ // that adding a node failed
1424
+ this.driver.controllerLog.print(`Adding the node failed`, "error");
1425
+ this.emit("inclusion failed");
1128
1426
  // in any case, stop the inclusion process so we don't accidentally add another node
1129
1427
  try {
1130
- await this.stopInclusionInternal();
1428
+ await this.stopInclusion();
1131
1429
  }
1132
1430
  catch {
1133
1431
  /* ok */
1134
1432
  }
1135
- break;
1433
+ return true; // Don't invoke any more handlers
1136
1434
  case AddNodeToNetworkRequest_1.AddNodeStatus.AddingController:
1137
1435
  this._includeController = true;
1138
1436
  // fall through!
@@ -1149,103 +1447,113 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
1149
1447
  case AddNodeToNetworkRequest_1.AddNodeStatus.ProtocolDone: {
1150
1448
  // this is called after a new node is added
1151
1449
  // stop the inclusion process so we don't accidentally add another node
1450
+ let nodeId;
1152
1451
  try {
1153
- await this.stopInclusionInternal();
1452
+ nodeId = await this.finishInclusion();
1154
1453
  }
1155
1454
  catch {
1156
- /* ok */
1455
+ // ignore the error
1157
1456
  }
1158
- break;
1159
- }
1160
- case AddNodeToNetworkRequest_1.AddNodeStatus.Done: {
1161
- // this is called when the inclusion was completed
1162
- this.driver.controllerLog.print(`done called for ${msg.statusContext.nodeId}`);
1163
- // stopping the inclusion was acknowledged by the controller
1164
- if (this._stopInclusionPromise != null)
1165
- this._stopInclusionPromise.resolve(true);
1166
- if (msg.statusContext.nodeId === core_1.NODE_ID_BROADCAST) {
1457
+ // It is recommended to send another STOP command to the controller
1458
+ try {
1459
+ await this.stopInclusionNoCallback();
1460
+ }
1461
+ catch {
1462
+ // ignore the error
1463
+ }
1464
+ if (!nodeId || !this._nodePendingInclusion) {
1465
+ // The inclusion did not succeed
1466
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
1467
+ this._nodePendingInclusion = undefined;
1468
+ return true;
1469
+ }
1470
+ else if (nodeId === core_1.NODE_ID_BROADCAST) {
1167
1471
  // No idea how this can happen but it dit at least once
1168
1472
  this.driver.controllerLog.print(`Cannot add a node with the Node ID ${core_1.NODE_ID_BROADCAST}, aborting...`, "warn");
1473
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
1169
1474
  this._nodePendingInclusion = undefined;
1475
+ return true;
1170
1476
  }
1171
- else if (this._nodePendingInclusion != null) {
1172
- const newNode = this._nodePendingInclusion;
1173
- const supportedCommandClasses = [
1174
- ...newNode.implementedCommandClasses.entries(),
1175
- ]
1176
- .filter(([, info]) => info.isSupported)
1177
- .map(([cc]) => cc);
1178
- const controlledCommandClasses = [
1179
- ...newNode.implementedCommandClasses.entries(),
1180
- ]
1181
- .filter(([, info]) => info.isControlled)
1182
- .map(([cc]) => cc);
1183
- this.driver.controllerLog.print(`finished adding node ${newNode.id}:
1477
+ // We're technically done with the inclusion but should not include
1478
+ // anything else until the node has been bootstrapped
1479
+ this.setInclusionState(Inclusion_1.InclusionState.Busy);
1480
+ // Inclusion is now completed, bootstrap the node
1481
+ const newNode = this._nodePendingInclusion;
1482
+ const supportedCommandClasses = [
1483
+ ...newNode.implementedCommandClasses.entries(),
1484
+ ]
1485
+ .filter(([, info]) => info.isSupported)
1486
+ .map(([cc]) => cc);
1487
+ const controlledCommandClasses = [
1488
+ ...newNode.implementedCommandClasses.entries(),
1489
+ ]
1490
+ .filter(([, info]) => info.isControlled)
1491
+ .map(([cc]) => cc);
1492
+ this.driver.controllerLog.print(`finished adding node ${newNode.id}:
1184
1493
  basic device class: ${(_a = newNode.deviceClass) === null || _a === void 0 ? void 0 : _a.basic.label}
1185
1494
  generic device class: ${(_b = newNode.deviceClass) === null || _b === void 0 ? void 0 : _b.generic.label}
1186
1495
  specific device class: ${(_c = newNode.deviceClass) === null || _c === void 0 ? void 0 : _c.specific.label}
1187
1496
  supported CCs: ${supportedCommandClasses
1188
- .map((cc) => `\n · ${core_1.CommandClasses[cc]} (${(0, shared_1.num2hex)(cc)})`)
1189
- .join("")}
1497
+ .map((cc) => `\n · ${core_1.CommandClasses[cc]} (${(0, shared_1.num2hex)(cc)})`)
1498
+ .join("")}
1190
1499
  controlled CCs: ${controlledCommandClasses
1191
- .map((cc) => `\n · ${core_1.CommandClasses[cc]} (${(0, shared_1.num2hex)(cc)})`)
1192
- .join("")}`);
1193
- // remember the node
1194
- this._nodes.set(newNode.id, newNode);
1195
- this._nodePendingInclusion = undefined;
1196
- // We're communicating with the device, so assume it is alive
1197
- // If it is actually a sleeping device, it will be marked as such later
1198
- newNode.markAsAlive();
1199
- // Assign SUC return route to make sure the node knows where to get its routes from
1200
- newNode.hasSUCReturnRoute = await this.assignSUCReturnRoute(newNode.id);
1201
- const opts = this._inclusionOptions;
1202
- // The default inclusion strategy is: Use S2 if possible, only use S0 if necessary, use no encryption otherwise
1203
- let lowSecurity = false;
1204
- if (newNode.supportsCC(core_1.CommandClasses["Security 2"]) &&
1205
- (opts.strategy === Inclusion_1.InclusionStrategy.Default ||
1206
- opts.strategy === Inclusion_1.InclusionStrategy.Security_S2)) {
1207
- await this.secureBootstrapS2(newNode);
1208
- const actualSecurityClass = newNode.getHighestSecurityClass();
1209
- if (actualSecurityClass == undefined ||
1210
- actualSecurityClass <
1211
- core_1.SecurityClass.S2_Unauthenticated) {
1212
- lowSecurity = true;
1213
- }
1500
+ .map((cc) => `\n · ${core_1.CommandClasses[cc]} (${(0, shared_1.num2hex)(cc)})`)
1501
+ .join("")}`);
1502
+ // remember the node
1503
+ this._nodes.set(newNode.id, newNode);
1504
+ this._nodePendingInclusion = undefined;
1505
+ // We're communicating with the device, so assume it is alive
1506
+ // If it is actually a sleeping device, it will be marked as such later
1507
+ newNode.markAsAlive();
1508
+ // Assign SUC return route to make sure the node knows where to get its routes from
1509
+ newNode.hasSUCReturnRoute = await this.assignSUCReturnRoute(newNode.id);
1510
+ const opts = this._inclusionOptions;
1511
+ // The default inclusion strategy is: Use S2 if possible, only use S0 if necessary, use no encryption otherwise
1512
+ let lowSecurity = false;
1513
+ if (newNode.supportsCC(core_1.CommandClasses["Security 2"]) &&
1514
+ (opts.strategy === Inclusion_1.InclusionStrategy.Default ||
1515
+ opts.strategy === Inclusion_1.InclusionStrategy.Security_S2 ||
1516
+ opts.strategy === Inclusion_1.InclusionStrategy.SmartStart)) {
1517
+ await this.secureBootstrapS2(newNode);
1518
+ const actualSecurityClass = newNode.getHighestSecurityClass();
1519
+ if (actualSecurityClass == undefined ||
1520
+ actualSecurityClass < core_1.SecurityClass.S2_Unauthenticated) {
1521
+ lowSecurity = true;
1214
1522
  }
1215
- else if (newNode.supportsCC(core_1.CommandClasses.Security) &&
1216
- (opts.strategy === Inclusion_1.InclusionStrategy.Security_S0 ||
1217
- (opts.strategy === Inclusion_1.InclusionStrategy.Default &&
1218
- (opts.forceSecurity ||
1219
- ((_g = ((_e = (_d = newNode.deviceClass) === null || _d === void 0 ? void 0 : _d.specific) !== null && _e !== void 0 ? _e : (_f = newNode.deviceClass) === null || _f === void 0 ? void 0 : _f.generic)) === null || _g === void 0 ? void 0 : _g.requiresSecurity))))) {
1220
- await this.secureBootstrapS0(newNode);
1221
- const actualSecurityClass = newNode.getHighestSecurityClass();
1222
- if (actualSecurityClass == undefined ||
1223
- actualSecurityClass < core_1.SecurityClass.S0_Legacy) {
1224
- lowSecurity = true;
1225
- }
1523
+ }
1524
+ else if (newNode.supportsCC(core_1.CommandClasses.Security) &&
1525
+ (opts.strategy === Inclusion_1.InclusionStrategy.Security_S0 ||
1526
+ (opts.strategy === Inclusion_1.InclusionStrategy.Default &&
1527
+ (opts.forceSecurity ||
1528
+ ((_g = ((_e = (_d = newNode.deviceClass) === null || _d === void 0 ? void 0 : _d.specific) !== null && _e !== void 0 ? _e : (_f = newNode.deviceClass) === null || _f === void 0 ? void 0 : _f.generic)) === null || _g === void 0 ? void 0 : _g.requiresSecurity))))) {
1529
+ await this.secureBootstrapS0(newNode);
1530
+ const actualSecurityClass = newNode.getHighestSecurityClass();
1531
+ if (actualSecurityClass == undefined ||
1532
+ actualSecurityClass < core_1.SecurityClass.S0_Legacy) {
1533
+ lowSecurity = true;
1226
1534
  }
1227
- this._includeController = false;
1228
- // Bootstrap the node's lifelines, so it knows where the controller is
1229
- await this.bootstrapLifelineAndWakeup(newNode);
1230
- // We're done adding this node, notify listeners
1231
- const result = {};
1232
- if (lowSecurity)
1233
- result.lowSecurity = true;
1234
- this.emit("node added", newNode, result);
1235
1535
  }
1236
- break;
1536
+ this._includeController = false;
1537
+ // Bootstrap the node's lifelines, so it knows where the controller is
1538
+ await this.bootstrapLifelineAndWakeup(newNode);
1539
+ // We're done adding this node, notify listeners
1540
+ const result = {};
1541
+ if (lowSecurity)
1542
+ result.lowSecurity = true;
1543
+ this.markNodeOnProvisioningList(newNode);
1544
+ this.emit("node added", newNode, result);
1545
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
1546
+ return true; // Don't invoke any more handlers
1237
1547
  }
1238
- default:
1239
- // not sure what to do with this message
1240
- return false;
1241
1548
  }
1242
- return true; // Don't invoke any more handlers
1549
+ // not sure what to do with this message
1550
+ return false;
1243
1551
  }
1244
1552
  /**
1245
1553
  * Is called when an ReplaceFailed request is received from the controller.
1246
1554
  * Handles and controls the replace process.
1247
1555
  */
1248
- async handleReplaceNodeRequest(msg) {
1556
+ async handleReplaceNodeStatusReport(msg) {
1249
1557
  var _a, _b, _c;
1250
1558
  this.driver.controllerLog.print(`handling replace node request (status = ${ReplaceFailedNodeRequest_1.ReplaceFailedNodeStatus[msg.replaceStatus]})`);
1251
1559
  if (this._inclusionOptions == undefined) {
@@ -1254,9 +1562,11 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
1254
1562
  }
1255
1563
  switch (msg.replaceStatus) {
1256
1564
  case ReplaceFailedNodeRequest_1.ReplaceFailedNodeStatus.NodeOK:
1565
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
1257
1566
  (_a = this._replaceFailedPromise) === null || _a === void 0 ? void 0 : _a.reject(new core_1.ZWaveError(`The node could not be replaced because it has responded`, core_1.ZWaveErrorCodes.ReplaceFailedNode_NodeOK));
1258
1567
  break;
1259
1568
  case ReplaceFailedNodeRequest_1.ReplaceFailedNodeStatus.FailedNodeReplaceFailed:
1569
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
1260
1570
  (_b = this._replaceFailedPromise) === null || _b === void 0 ? void 0 : _b.reject(new core_1.ZWaveError(`The failed node has not been replaced`, core_1.ZWaveErrorCodes.ReplaceFailedNode_Failed));
1261
1571
  break;
1262
1572
  case ReplaceFailedNodeRequest_1.ReplaceFailedNodeStatus.FailedNodeReplace:
@@ -1267,7 +1577,7 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
1267
1577
  // TODO: Remove first parameter in next major version
1268
1578
  this._inclusionOptions.strategy !==
1269
1579
  Inclusion_1.InclusionStrategy.Insecure, this._inclusionOptions.strategy);
1270
- this._inclusionActive = true;
1580
+ this.setInclusionState(Inclusion_1.InclusionState.Including);
1271
1581
  (_c = this._replaceFailedPromise) === null || _c === void 0 ? void 0 : _c.resolve(true);
1272
1582
  // stop here, don't emit inclusion failed
1273
1583
  return true;
@@ -1276,7 +1586,11 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
1276
1586
  this.emit("inclusion stopped");
1277
1587
  if (this._nodePendingReplace) {
1278
1588
  this.emit("node removed", this._nodePendingReplace, true);
1589
+ this.unmarkNodeOnProvisioningList(this._nodePendingReplace.id);
1279
1590
  this._nodes.delete(this._nodePendingReplace.id);
1591
+ // We're technically done with the replacing but should not include
1592
+ // anything else until the node has been bootstrapped
1593
+ this.setInclusionState(Inclusion_1.InclusionState.Busy);
1280
1594
  // Create a fresh node instance and forget the old one
1281
1595
  const newNode = new Node_1.ZWaveNode(this._nodePendingReplace.id, this.driver, undefined, undefined, undefined,
1282
1596
  // Create an empty value DB and specify that it contains no values
@@ -1316,6 +1630,8 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
1316
1630
  const result = {};
1317
1631
  if (lowSecurity)
1318
1632
  result.lowSecurity = true;
1633
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
1634
+ this.markNodeOnProvisioningList(newNode);
1319
1635
  this.emit("node added", newNode, result);
1320
1636
  }
1321
1637
  // stop here, don't emit inclusion failed
@@ -1328,40 +1644,26 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
1328
1644
  * Is called when a RemoveNode request is received from the controller.
1329
1645
  * Handles and controls the exclusion process.
1330
1646
  */
1331
- async handleRemoveNodeRequest(msg) {
1647
+ async handleRemoveNodeStatusReport(msg) {
1332
1648
  this.driver.controllerLog.print(`handling remove node request (status = ${RemoveNodeFromNetworkRequest_1.RemoveNodeStatus[msg.status]})`);
1333
- if (!this._exclusionActive && msg.status !== RemoveNodeFromNetworkRequest_1.RemoveNodeStatus.Done) {
1649
+ if (this._inclusionState !== Inclusion_1.InclusionState.Excluding) {
1334
1650
  this.driver.controllerLog.print(` exclusion is NOT active, ignoring it...`);
1335
1651
  return true; // Don't invoke any more handlers
1336
1652
  }
1337
1653
  switch (msg.status) {
1338
- case RemoveNodeFromNetworkRequest_1.RemoveNodeStatus.Ready:
1339
- // this is called when inclusion was started successfully
1340
- this.driver.controllerLog.print(` the controller is now ready to remove nodes`);
1341
- if (this._beginInclusionPromise != null) {
1342
- this._beginInclusionPromise.resolve(true);
1343
- this.emit("exclusion started");
1344
- }
1345
- break;
1346
1654
  case RemoveNodeFromNetworkRequest_1.RemoveNodeStatus.Failed:
1347
- // this is called when inclusion could not be started...
1348
- if (this._beginInclusionPromise != null) {
1349
- this.driver.controllerLog.print(` starting the exclusion failed`, "error");
1350
- this._beginInclusionPromise.reject(new core_1.ZWaveError("The exclusion could not be started.", core_1.ZWaveErrorCodes.Controller_ExclusionFailed));
1351
- }
1352
- else {
1353
- // ...or removing a node failed
1354
- this.driver.controllerLog.print(` removing the node failed`, "error");
1355
- this.emit("exclusion failed");
1356
- }
1655
+ // This code is handled elsewhere for starting the exclusion, so this means
1656
+ // that removing a node failed
1657
+ this.driver.controllerLog.print(`Removing the node failed`, "error");
1658
+ this.emit("exclusion failed");
1357
1659
  // in any case, stop the exclusion process so we don't accidentally remove another node
1358
1660
  try {
1359
- await this.stopExclusionInternal();
1661
+ await this.stopExclusion();
1360
1662
  }
1361
1663
  catch {
1362
1664
  /* ok */
1363
1665
  }
1364
- break;
1666
+ return true; // Don't invoke any more handlers
1365
1667
  case RemoveNodeFromNetworkRequest_1.RemoveNodeStatus.RemovingSlave:
1366
1668
  case RemoveNodeFromNetworkRequest_1.RemoveNodeStatus.RemovingController: {
1367
1669
  // this is called when a node is removed
@@ -1372,29 +1674,33 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
1372
1674
  // this is called when the exclusion was completed
1373
1675
  // stop the exclusion process so we don't accidentally remove another node
1374
1676
  try {
1375
- await this.stopExclusionInternal();
1677
+ await this.stopExclusionNoCallback();
1376
1678
  }
1377
1679
  catch {
1378
1680
  /* ok */
1379
1681
  }
1380
- // stopping the inclusion was acknowledged by the controller
1381
- if (this._stopInclusionPromise != null)
1382
- this._stopInclusionPromise.resolve(true);
1383
- if (this._nodePendingExclusion != null) {
1384
- this.driver.controllerLog.print(`Node ${this._nodePendingExclusion.id} was removed`);
1385
- // notify listeners
1386
- this.emit("node removed", this._nodePendingExclusion, false);
1387
- // and forget the node
1388
- this._nodes.delete(this._nodePendingExclusion.id);
1389
- this._nodePendingExclusion = undefined;
1682
+ if (!this._nodePendingExclusion) {
1683
+ // The exclusion did not succeed
1684
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
1685
+ return true;
1390
1686
  }
1391
- break;
1687
+ const nodeId = this._nodePendingExclusion.id;
1688
+ this.driver.controllerLog.print(`Node ${nodeId} was removed`);
1689
+ // Avoid automatic re-inclusion using SmartStart if desired
1690
+ if (this._unprovisionRemovedNode)
1691
+ this.unprovisionSmartStartNode(nodeId);
1692
+ // notify listeners
1693
+ this.emit("node removed", this._nodePendingExclusion, false);
1694
+ // and forget the node
1695
+ this.unmarkNodeOnProvisioningList(nodeId);
1696
+ this._nodes.delete(nodeId);
1697
+ this._nodePendingExclusion = undefined;
1698
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
1699
+ return true; // Don't invoke any more handlers
1392
1700
  }
1393
- default:
1394
- // not sure what to do with this message
1395
- return false;
1396
1701
  }
1397
- return true; // Don't invoke any more handlers
1702
+ // not sure what to do with this message
1703
+ return false;
1398
1704
  }
1399
1705
  /**
1400
1706
  * Performs a healing process for all alive nodes in the network,
@@ -2171,15 +2477,23 @@ ${associatedNodes.join(", ")}`,
2171
2477
  // Emit the removed event so the driver and applications can react
2172
2478
  this.emit("node removed", this.nodes.get(nodeId), false);
2173
2479
  // and forget the node
2480
+ this.unmarkNodeOnProvisioningList(nodeId);
2174
2481
  this._nodes.delete(nodeId);
2175
2482
  return;
2176
2483
  }
2177
2484
  }
2178
2485
  }
2179
2486
  async replaceFailedNode(nodeId, options) {
2180
- // don't start it twice
2181
- if (this._inclusionActive || this._exclusionActive)
2487
+ if (this._inclusionState === Inclusion_1.InclusionState.Including ||
2488
+ this._inclusionState === Inclusion_1.InclusionState.Excluding ||
2489
+ this._inclusionState === Inclusion_1.InclusionState.Busy) {
2182
2490
  return false;
2491
+ }
2492
+ if (this._inclusionState === Inclusion_1.InclusionState.SmartStart) {
2493
+ // Disable listening mode so we can switch to exclusion mode
2494
+ await this.stopInclusion();
2495
+ }
2496
+ this.setInclusionState(Inclusion_1.InclusionState.Busy);
2183
2497
  if (options == undefined) {
2184
2498
  options = {
2185
2499
  strategy: Inclusion_1.InclusionStrategy.Security_S0,
@@ -2195,6 +2509,7 @@ ${associatedNodes.join(", ")}`,
2195
2509
  this.driver.controllerLog.print(`starting replace failed node process...`);
2196
2510
  const node = this.nodes.getOrThrow(nodeId);
2197
2511
  if (await node.ping()) {
2512
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
2198
2513
  throw new core_1.ZWaveError(`The node replace process could not be started because the node responded to a ping.`, core_1.ZWaveErrorCodes.ReplaceFailedNode_Failed);
2199
2514
  }
2200
2515
  this._inclusionOptions = options;
@@ -2220,6 +2535,7 @@ ${associatedNodes.join(", ")}`,
2220
2535
  ReplaceFailedNodeRequest_1.ReplaceFailedNodeStartFlags.ReplaceFailed)) {
2221
2536
  message += `\n· The controller is busy or the node has responded`;
2222
2537
  }
2538
+ this.setInclusionState(Inclusion_1.InclusionState.Idle);
2223
2539
  throw new core_1.ZWaveError(message, core_1.ZWaveErrorCodes.ReplaceFailedNode_Failed);
2224
2540
  }
2225
2541
  else {
@@ -2334,6 +2650,18 @@ ${associatedNodes.join(", ")}`,
2334
2650
  */
2335
2651
  serialize() {
2336
2652
  return {
2653
+ controller: {
2654
+ supportsSoftReset: this.supportsSoftReset,
2655
+ provisioningList: this.provisioningList.map((e) => {
2656
+ const { dsk, securityClasses, ...rest } = e;
2657
+ return {
2658
+ dsk,
2659
+ securityClasses: securityClasses.map((s) => core_1.SecurityClass[s]),
2660
+ // The user-defined properties are saved as-is
2661
+ ...rest,
2662
+ };
2663
+ }),
2664
+ },
2337
2665
  nodes: (0, objects_1.composeObject)([...this.nodes.entries()].map(([id, node]) => [id.toString(), node.serialize()])),
2338
2666
  };
2339
2667
  }
@@ -2342,6 +2670,43 @@ ${associatedNodes.join(", ")}`,
2342
2670
  * Deserializes the controller information and all nodes from the cache.
2343
2671
  */
2344
2672
  async deserialize(serialized) {
2673
+ if ((0, typeguards_1.isObject)(serialized.controller)) {
2674
+ // Parse whether the controller supports soft reset
2675
+ if (typeof serialized.controller.supportsSoftReset === "boolean") {
2676
+ this.supportsSoftReset =
2677
+ serialized.controller.supportsSoftReset;
2678
+ }
2679
+ // Parse the controller's Smart Start provisioning list
2680
+ if ((0, typeguards_1.isArray)(serialized.controller.provisioningList)) {
2681
+ entries: for (const entry of serialized.controller
2682
+ .provisioningList) {
2683
+ if (!(0, typeguards_1.isObject)(entry))
2684
+ continue;
2685
+ const { dsk, securityClasses: secClasses, ...rest } = entry;
2686
+ if (typeof entry.dsk !== "string")
2687
+ continue;
2688
+ if (!(0, typeguards_1.isArray)(entry.securityClasses))
2689
+ continue;
2690
+ if (!(0, core_1.isValidDSK)(entry.dsk))
2691
+ continue;
2692
+ const securityClasses = [];
2693
+ for (const s of secClasses) {
2694
+ if (typeof s !== "string")
2695
+ continue entries;
2696
+ const secClass = core_1.SecurityClass[s];
2697
+ if (typeof secClass !== "number")
2698
+ continue entries;
2699
+ securityClasses.push(secClass);
2700
+ }
2701
+ this._provisioningList.push({
2702
+ dsk: entry.dsk,
2703
+ securityClasses,
2704
+ // The user-defined properties are not validated further
2705
+ ...rest,
2706
+ });
2707
+ }
2708
+ }
2709
+ }
2345
2710
  if ((0, typeguards_1.isObject)(serialized.nodes)) {
2346
2711
  for (const nodeId of Object.keys(serialized.nodes)) {
2347
2712
  const serializedNode = serialized.nodes[nodeId];
@@ -2351,9 +2716,9 @@ ${associatedNodes.join(", ")}`,
2351
2716
  throw new core_1.ZWaveError("The cache file is invalid", core_1.ZWaveErrorCodes.Driver_InvalidCache);
2352
2717
  }
2353
2718
  if (this.nodes.has(serializedNode.id)) {
2354
- await this.nodes
2355
- .get(serializedNode.id)
2356
- .deserialize(serializedNode);
2719
+ const node = this.nodes.getOrThrow(serializedNode.id);
2720
+ await node.deserialize(serializedNode);
2721
+ this.markNodeOnProvisioningList(node);
2357
2722
  }
2358
2723
  }
2359
2724
  }