zwave-js 8.6.0 → 8.7.2

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