zwave-js 11.0.0 → 11.2.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 (158) hide show
  1. package/build/lib/controller/Controller.d.ts +86 -3
  2. package/build/lib/controller/Controller.d.ts.map +1 -1
  3. package/build/lib/controller/Controller.js +391 -164
  4. package/build/lib/controller/Controller.js.map +1 -1
  5. package/build/lib/controller/Features.js +1 -1
  6. package/build/lib/controller/Features.js.map +1 -1
  7. package/build/lib/controller/Inclusion.js +6 -6
  8. package/build/lib/controller/Inclusion.js.map +1 -1
  9. package/build/lib/controller/MockControllerBehaviors.d.ts.map +1 -1
  10. package/build/lib/controller/MockControllerBehaviors.js +3 -2
  11. package/build/lib/controller/MockControllerBehaviors.js.map +1 -1
  12. package/build/lib/controller/MockControllerState.js +2 -2
  13. package/build/lib/controller/MockControllerState.js.map +1 -1
  14. package/build/lib/controller/NodeInformationFrame.d.ts.map +1 -1
  15. package/build/lib/controller/NodeInformationFrame.js +6 -1
  16. package/build/lib/controller/NodeInformationFrame.js.map +1 -1
  17. package/build/lib/controller/ZWaveSDKVersions.js +1 -1
  18. package/build/lib/controller/ZWaveSDKVersions.js.map +1 -1
  19. package/build/lib/controller/_Types.js +1 -1
  20. package/build/lib/controller/_Types.js.map +1 -1
  21. package/build/lib/driver/Bootloader.js +1 -1
  22. package/build/lib/driver/Bootloader.js.map +1 -1
  23. package/build/lib/driver/Driver.d.ts +44 -12
  24. package/build/lib/driver/Driver.d.ts.map +1 -1
  25. package/build/lib/driver/Driver.js +495 -207
  26. package/build/lib/driver/Driver.js.map +1 -1
  27. package/build/lib/driver/MessageGenerators.d.ts.map +1 -1
  28. package/build/lib/driver/MessageGenerators.js +4 -11
  29. package/build/lib/driver/MessageGenerators.js.map +1 -1
  30. package/build/lib/driver/NetworkCache.d.ts +5 -0
  31. package/build/lib/driver/NetworkCache.d.ts.map +1 -1
  32. package/build/lib/driver/NetworkCache.js +5 -0
  33. package/build/lib/driver/NetworkCache.js.map +1 -1
  34. package/build/lib/driver/SerialAPICommandMachine.d.ts +17 -11
  35. package/build/lib/driver/SerialAPICommandMachine.d.ts.map +1 -1
  36. package/build/lib/driver/SerialAPICommandMachine.js +16 -6
  37. package/build/lib/driver/SerialAPICommandMachine.js.map +1 -1
  38. package/build/lib/driver/StateMachineShared.d.ts +27 -52
  39. package/build/lib/driver/StateMachineShared.d.ts.map +1 -1
  40. package/build/lib/driver/StateMachineShared.js +16 -48
  41. package/build/lib/driver/StateMachineShared.js.map +1 -1
  42. package/build/lib/node/Node.d.ts +11 -0
  43. package/build/lib/node/Node.d.ts.map +1 -1
  44. package/build/lib/node/Node.js +223 -8
  45. package/build/lib/node/Node.js.map +1 -1
  46. package/build/lib/node/NodeReadyMachine.d.ts +2 -2
  47. package/build/lib/node/NodeReadyMachine.d.ts.map +1 -1
  48. package/build/lib/node/NodeReadyMachine.js.map +1 -1
  49. package/build/lib/node/NodeStatusMachine.d.ts +2 -2
  50. package/build/lib/node/NodeStatusMachine.d.ts.map +1 -1
  51. package/build/lib/node/NodeStatusMachine.js.map +1 -1
  52. package/build/lib/serialapi/_Types.js +1 -1
  53. package/build/lib/serialapi/_Types.js.map +1 -1
  54. package/build/lib/serialapi/application/ApplicationCommandRequest.js +3 -4
  55. package/build/lib/serialapi/application/ApplicationCommandRequest.js.map +1 -1
  56. package/build/lib/serialapi/application/ApplicationUpdateRequest.js +13 -21
  57. package/build/lib/serialapi/application/ApplicationUpdateRequest.js.map +1 -1
  58. package/build/lib/serialapi/application/BridgeApplicationCommandRequest.js +2 -3
  59. package/build/lib/serialapi/application/BridgeApplicationCommandRequest.js.map +1 -1
  60. package/build/lib/serialapi/application/SerialAPIStartedRequest.js +3 -4
  61. package/build/lib/serialapi/application/SerialAPIStartedRequest.js.map +1 -1
  62. package/build/lib/serialapi/application/ShutdownMessages.js +4 -6
  63. package/build/lib/serialapi/application/ShutdownMessages.js.map +1 -1
  64. package/build/lib/serialapi/capability/GetControllerCapabilitiesMessages.js +4 -6
  65. package/build/lib/serialapi/capability/GetControllerCapabilitiesMessages.js.map +1 -1
  66. package/build/lib/serialapi/capability/GetControllerVersionMessages.js +4 -6
  67. package/build/lib/serialapi/capability/GetControllerVersionMessages.js.map +1 -1
  68. package/build/lib/serialapi/capability/GetProtocolVersionMessages.js +4 -6
  69. package/build/lib/serialapi/capability/GetProtocolVersionMessages.js.map +1 -1
  70. package/build/lib/serialapi/capability/GetSerialApiCapabilitiesMessages.js +4 -6
  71. package/build/lib/serialapi/capability/GetSerialApiCapabilitiesMessages.js.map +1 -1
  72. package/build/lib/serialapi/capability/GetSerialApiInitDataMessages.js +4 -6
  73. package/build/lib/serialapi/capability/GetSerialApiInitDataMessages.js.map +1 -1
  74. package/build/lib/serialapi/capability/HardResetRequest.js +4 -6
  75. package/build/lib/serialapi/capability/HardResetRequest.js.map +1 -1
  76. package/build/lib/serialapi/capability/SerialAPISetupMessages.js +59 -88
  77. package/build/lib/serialapi/capability/SerialAPISetupMessages.js.map +1 -1
  78. package/build/lib/serialapi/capability/SetApplicationNodeInformationRequest.js +2 -3
  79. package/build/lib/serialapi/capability/SetApplicationNodeInformationRequest.js.map +1 -1
  80. package/build/lib/serialapi/memory/GetControllerIdMessages.js +4 -6
  81. package/build/lib/serialapi/memory/GetControllerIdMessages.js.map +1 -1
  82. package/build/lib/serialapi/misc/GetBackgroundRSSIMessages.js +4 -6
  83. package/build/lib/serialapi/misc/GetBackgroundRSSIMessages.js.map +1 -1
  84. package/build/lib/serialapi/misc/SetRFReceiveModeMessages.js +4 -6
  85. package/build/lib/serialapi/misc/SetRFReceiveModeMessages.js.map +1 -1
  86. package/build/lib/serialapi/misc/SetSerialApiTimeoutsMessages.js +4 -6
  87. package/build/lib/serialapi/misc/SetSerialApiTimeoutsMessages.js.map +1 -1
  88. package/build/lib/serialapi/misc/SoftResetRequest.js +2 -3
  89. package/build/lib/serialapi/misc/SoftResetRequest.js.map +1 -1
  90. package/build/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.js +6 -8
  91. package/build/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.js.map +1 -1
  92. package/build/lib/serialapi/network-mgmt/AssignPriorityReturnRouteMessages.js +6 -9
  93. package/build/lib/serialapi/network-mgmt/AssignPriorityReturnRouteMessages.js.map +1 -1
  94. package/build/lib/serialapi/network-mgmt/AssignPrioritySUCReturnRouteMessages.js +6 -9
  95. package/build/lib/serialapi/network-mgmt/AssignPrioritySUCReturnRouteMessages.js.map +1 -1
  96. package/build/lib/serialapi/network-mgmt/AssignReturnRouteMessages.js +6 -9
  97. package/build/lib/serialapi/network-mgmt/AssignReturnRouteMessages.js.map +1 -1
  98. package/build/lib/serialapi/network-mgmt/AssignSUCReturnRouteMessages.js +6 -9
  99. package/build/lib/serialapi/network-mgmt/AssignSUCReturnRouteMessages.js.map +1 -1
  100. package/build/lib/serialapi/network-mgmt/DeleteReturnRouteMessages.js +6 -9
  101. package/build/lib/serialapi/network-mgmt/DeleteReturnRouteMessages.js.map +1 -1
  102. package/build/lib/serialapi/network-mgmt/DeleteSUCReturnRouteMessages.js +6 -9
  103. package/build/lib/serialapi/network-mgmt/DeleteSUCReturnRouteMessages.js.map +1 -1
  104. package/build/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.js +4 -6
  105. package/build/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.js.map +1 -1
  106. package/build/lib/serialapi/network-mgmt/GetPriorityRouteMessages.js +4 -6
  107. package/build/lib/serialapi/network-mgmt/GetPriorityRouteMessages.js.map +1 -1
  108. package/build/lib/serialapi/network-mgmt/GetRoutingInfoMessages.js +4 -6
  109. package/build/lib/serialapi/network-mgmt/GetRoutingInfoMessages.js.map +1 -1
  110. package/build/lib/serialapi/network-mgmt/GetSUCNodeIdMessages.js +4 -6
  111. package/build/lib/serialapi/network-mgmt/GetSUCNodeIdMessages.js.map +1 -1
  112. package/build/lib/serialapi/network-mgmt/IsFailedNodeMessages.js +4 -6
  113. package/build/lib/serialapi/network-mgmt/IsFailedNodeMessages.js.map +1 -1
  114. package/build/lib/serialapi/network-mgmt/RemoveFailedNodeMessages.js +8 -11
  115. package/build/lib/serialapi/network-mgmt/RemoveFailedNodeMessages.js.map +1 -1
  116. package/build/lib/serialapi/network-mgmt/RemoveNodeFromNetworkRequest.js +6 -8
  117. package/build/lib/serialapi/network-mgmt/RemoveNodeFromNetworkRequest.js.map +1 -1
  118. package/build/lib/serialapi/network-mgmt/ReplaceFailedNodeRequest.js +8 -11
  119. package/build/lib/serialapi/network-mgmt/ReplaceFailedNodeRequest.js.map +1 -1
  120. package/build/lib/serialapi/network-mgmt/RequestNodeInfoMessages.js +4 -6
  121. package/build/lib/serialapi/network-mgmt/RequestNodeInfoMessages.js.map +1 -1
  122. package/build/lib/serialapi/network-mgmt/RequestNodeNeighborUpdateMessages.js +5 -7
  123. package/build/lib/serialapi/network-mgmt/RequestNodeNeighborUpdateMessages.js.map +1 -1
  124. package/build/lib/serialapi/network-mgmt/SetPriorityRouteMessages.js +4 -6
  125. package/build/lib/serialapi/network-mgmt/SetPriorityRouteMessages.js.map +1 -1
  126. package/build/lib/serialapi/network-mgmt/SetSUCNodeIDMessages.js +7 -10
  127. package/build/lib/serialapi/network-mgmt/SetSUCNodeIDMessages.js.map +1 -1
  128. package/build/lib/serialapi/nvm/ExtNVMReadLongBufferMessages.js +4 -6
  129. package/build/lib/serialapi/nvm/ExtNVMReadLongBufferMessages.js.map +1 -1
  130. package/build/lib/serialapi/nvm/ExtNVMReadLongByteMessages.js +4 -6
  131. package/build/lib/serialapi/nvm/ExtNVMReadLongByteMessages.js.map +1 -1
  132. package/build/lib/serialapi/nvm/ExtNVMWriteLongBufferMessages.js +4 -6
  133. package/build/lib/serialapi/nvm/ExtNVMWriteLongBufferMessages.js.map +1 -1
  134. package/build/lib/serialapi/nvm/ExtNVMWriteLongByteMessages.js +4 -6
  135. package/build/lib/serialapi/nvm/ExtNVMWriteLongByteMessages.js.map +1 -1
  136. package/build/lib/serialapi/nvm/FirmwareUpdateNVMMessages.js +29 -43
  137. package/build/lib/serialapi/nvm/FirmwareUpdateNVMMessages.js.map +1 -1
  138. package/build/lib/serialapi/nvm/GetNVMIdMessages.js +6 -8
  139. package/build/lib/serialapi/nvm/GetNVMIdMessages.js.map +1 -1
  140. package/build/lib/serialapi/nvm/NVMOperationsMessages.js +6 -8
  141. package/build/lib/serialapi/nvm/NVMOperationsMessages.js.map +1 -1
  142. package/build/lib/serialapi/transport/SendDataBridgeMessages.js +12 -18
  143. package/build/lib/serialapi/transport/SendDataBridgeMessages.js.map +1 -1
  144. package/build/lib/serialapi/transport/SendDataMessages.js +14 -21
  145. package/build/lib/serialapi/transport/SendDataMessages.js.map +1 -1
  146. package/package.json +17 -16
  147. package/build/lib/driver/CommandQueueMachine.d.ts +0 -60
  148. package/build/lib/driver/CommandQueueMachine.d.ts.map +0 -1
  149. package/build/lib/driver/CommandQueueMachine.js +0 -259
  150. package/build/lib/driver/CommandQueueMachine.js.map +0 -1
  151. package/build/lib/driver/SendThreadMachine.d.ts +0 -97
  152. package/build/lib/driver/SendThreadMachine.d.ts.map +0 -1
  153. package/build/lib/driver/SendThreadMachine.js +0 -286
  154. package/build/lib/driver/SendThreadMachine.js.map +0 -1
  155. package/build/lib/driver/TransactionMachine.d.ts +0 -30
  156. package/build/lib/driver/TransactionMachine.d.ts.map +0 -1
  157. package/build/lib/driver/TransactionMachine.js +0 -250
  158. package/build/lib/driver/TransactionMachine.js.map +0 -1
@@ -74,7 +74,7 @@ const NodeInformationFrame_1 = require("./NodeInformationFrame");
74
74
  const ZWaveSDKVersions_1 = require("./ZWaveSDKVersions");
75
75
  const _Types_3 = require("./_Types");
76
76
  const utils_1 = require("./utils");
77
- let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
77
+ let ZWaveController = exports.ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
78
78
  /** @internal */
79
79
  constructor(driver, bootloaderOnly = false) {
80
80
  super();
@@ -644,6 +644,18 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
644
644
  async hardReset() {
645
645
  // begin the reset process
646
646
  try {
647
+ const associations = this.nodes.get(this._ownNodeId)?.associations;
648
+ if (associations?.length) {
649
+ this.driver.controllerLog.print("Notifying associated nodes about reset...");
650
+ for (const nodeId of associations) {
651
+ const node = this.nodes.get(nodeId);
652
+ if (!node)
653
+ continue;
654
+ void node.sendResetLocallyNotification().catch(() => {
655
+ // ignore
656
+ });
657
+ }
658
+ }
647
659
  this.driver.controllerLog.print("performing hard reset...");
648
660
  await this.driver.sendMessage(new HardResetRequest_1.HardResetRequest(this.driver), {
649
661
  supportCheck: false,
@@ -1031,11 +1043,8 @@ let ZWaveController = class ZWaveController extends shared_1.TypedEventEmitter {
1031
1043
  direction: "inbound",
1032
1044
  });
1033
1045
  node.updateNodeInfo(msg.nodeInformation);
1034
- // Tell the send thread that we received a NIF from the node
1035
- this.driver["sendThread"].send({
1036
- type: "NIF",
1037
- nodeId: node.id,
1038
- });
1046
+ // Resolve active pings that would fail otherwise
1047
+ this.driver.resolvePendingPings(node.id);
1039
1048
  if (node.canSleep &&
1040
1049
  node.supportsCC(core_1.CommandClasses["Wake Up"])) {
1041
1050
  // In case this is a sleeping node and there are no messages in the queue, the node may go back to sleep very soon
@@ -1147,7 +1156,8 @@ supported CCs: ${nodeInfo.supportedCCs
1147
1156
  // No command received, bootstrap node by ourselves
1148
1157
  this.driver.controllerLog.logNode(nodeId, "no initiate command received, bootstrapping node...");
1149
1158
  // Assign SUC return route to make sure the node knows where to get its routes from
1150
- newNode.hasSUCReturnRoute = await this.assignSUCReturnRoute(newNode.id);
1159
+ newNode.hasSUCReturnRoute =
1160
+ await this.assignSUCReturnRoutes(newNode.id);
1151
1161
  // Include using the default inclusion strategy:
1152
1162
  // * Use S2 if possible,
1153
1163
  // * only use S0 if necessary,
@@ -1184,8 +1194,6 @@ supported CCs: ${nodeInfo.supportedCCs
1184
1194
  }
1185
1195
  }
1186
1196
  }
1187
- // Bootstrap the node's lifelines, so it knows where the controller is
1188
- await this.bootstrapLifelineAndWakeup(newNode);
1189
1197
  // We're done adding this node, notify listeners
1190
1198
  const result = bootstrapFailure != undefined
1191
1199
  ? {
@@ -1200,7 +1208,9 @@ supported CCs: ${nodeInfo.supportedCCs
1200
1208
  const step = initiate.step;
1201
1209
  newNode.once("ready", () => {
1202
1210
  this.driver.controllerLog.logNode(nodeId, `Notifying node ${inclCtrlrId} of finished inclusion`);
1203
- void inclCtrlr.commandClasses["Inclusion Controller"]
1211
+ // Create API without checking for support
1212
+ const api = inclCtrlr.createAPI(core_1.CommandClasses["Inclusion Controller"], false);
1213
+ void api
1204
1214
  .completeStep(step, cc_1.InclusionControllerStatus.OK)
1205
1215
  // eslint-disable-next-line @typescript-eslint/no-empty-function
1206
1216
  .catch(() => { });
@@ -1253,8 +1263,6 @@ supported CCs: ${nodeInfo.supportedCCs
1253
1263
  }
1254
1264
  // Perform S0/S2 bootstrapping
1255
1265
  const bootstrapFailure = await this.proxyBootstrap(newNode, inclCtrlr);
1256
- // Bootstrap the node's lifelines, so it knows where the controller is
1257
- await this.bootstrapLifelineAndWakeup(newNode);
1258
1266
  // We're done adding this node, notify listeners
1259
1267
  const result = bootstrapFailure != undefined
1260
1268
  ? {
@@ -1267,7 +1275,9 @@ supported CCs: ${nodeInfo.supportedCCs
1267
1275
  // And notify the inclusion controller after we're done interviewing
1268
1276
  newNode.once("ready", () => {
1269
1277
  this.driver.controllerLog.logNode(inclCtrlr.nodeId, `Notifying inclusion controller of finished inclusion`);
1270
- void inclCtrlr.commandClasses["Inclusion Controller"]
1278
+ // Create API without checking for support
1279
+ const api = inclCtrlr.createAPI(core_1.CommandClasses["Inclusion Controller"], false);
1280
+ void api
1271
1281
  .completeStep(initiate.step, cc_1.InclusionControllerStatus.OK)
1272
1282
  // eslint-disable-next-line @typescript-eslint/no-empty-function
1273
1283
  .catch(() => { });
@@ -1869,97 +1879,6 @@ supported CCs: ${nodeInfo.supportedCCs
1869
1879
  this.cancelBootstrapS2Promise = undefined;
1870
1880
  }
1871
1881
  }
1872
- /** Ensures that the node knows where to reach the controller */
1873
- async bootstrapLifelineAndWakeup(node) {
1874
- // If the node was bootstrapped with S2, all these requests must happen securely
1875
- if ((0, core_1.securityClassIsS2)(node.getHighestSecurityClass())) {
1876
- for (const cc of [
1877
- core_1.CommandClasses["Wake Up"],
1878
- core_1.CommandClasses.Association,
1879
- core_1.CommandClasses["Multi Channel Association"],
1880
- core_1.CommandClasses.Version,
1881
- ]) {
1882
- if (node.supportsCC(cc)) {
1883
- node.addCC(cc, { secure: true });
1884
- }
1885
- }
1886
- }
1887
- if (node.supportsCC(core_1.CommandClasses["Z-Wave Plus Info"])) {
1888
- // SDS11846: The Z-Wave+ lifeline must be assigned to a node as the very first thing
1889
- if (node.supportsCC(core_1.CommandClasses.Association) ||
1890
- node.supportsCC(core_1.CommandClasses["Multi Channel Association"])) {
1891
- this.driver.controllerLog.logNode(node.id, {
1892
- message: `Configuring Z-Wave+ Lifeline association...`,
1893
- direction: "none",
1894
- });
1895
- const ownNodeId = this.driver.controller.ownNodeId;
1896
- try {
1897
- if (node.supportsCC(core_1.CommandClasses.Association)) {
1898
- await node.commandClasses.Association.addNodeIds(1, ownNodeId);
1899
- }
1900
- else {
1901
- await node.commandClasses["Multi Channel Association"].addDestinations({
1902
- groupId: 1,
1903
- endpoints: [{ nodeId: ownNodeId, endpoint: 0 }],
1904
- });
1905
- }
1906
- // After setting the association, make sure the node knows how to reach us
1907
- await this.assignReturnRoute(node.id, ownNodeId);
1908
- }
1909
- catch (e) {
1910
- if ((0, core_1.isTransmissionError)(e) || (0, core_1.isRecoverableZWaveError)(e)) {
1911
- this.driver.controllerLog.logNode(node.id, {
1912
- message: `Failed to configure Z-Wave+ Lifeline association: ${e.message}`,
1913
- direction: "none",
1914
- level: "warn",
1915
- });
1916
- }
1917
- else {
1918
- throw e;
1919
- }
1920
- }
1921
- }
1922
- else {
1923
- this.driver.controllerLog.logNode(node.id, {
1924
- message: `Cannot configure Z-Wave+ Lifeline association: Node does not support associations...`,
1925
- direction: "none",
1926
- level: "warn",
1927
- });
1928
- }
1929
- }
1930
- if (node.supportsCC(core_1.CommandClasses["Wake Up"])) {
1931
- try {
1932
- // Query the version, so we can setup the wakeup destination correctly.
1933
- let supportedVersion;
1934
- if (node.supportsCC(core_1.CommandClasses.Version)) {
1935
- supportedVersion =
1936
- await node.commandClasses.Version.getCCVersion(core_1.CommandClasses["Wake Up"]);
1937
- }
1938
- // If querying the version can't be done, we should at least assume that it supports V1
1939
- supportedVersion ?? (supportedVersion = 1);
1940
- if (supportedVersion > 0) {
1941
- node.addCC(core_1.CommandClasses["Wake Up"], {
1942
- version: supportedVersion,
1943
- });
1944
- const instance = node.createCCInstance(core_1.CommandClasses["Wake Up"]);
1945
- await instance.interview(this.driver);
1946
- }
1947
- }
1948
- catch (e) {
1949
- if ((0, core_1.isTransmissionError)(e) || (0, core_1.isRecoverableZWaveError)(e)) {
1950
- this.driver.controllerLog.logNode(node.id, {
1951
- message: `Cannot configure wakeup destination: ${e.message}`,
1952
- direction: "none",
1953
- level: "warn",
1954
- });
1955
- }
1956
- else {
1957
- // we want to pass all other errors through
1958
- throw e;
1959
- }
1960
- }
1961
- }
1962
- }
1963
1882
  /**
1964
1883
  * Is called when an AddNode request is received from the controller.
1965
1884
  * Handles and controls the inclusion process.
@@ -2066,7 +1985,7 @@ supported CCs: ${nodeInfo.supportedCCs
2066
1985
  // If it is actually a sleeping device, it will be marked as such later
2067
1986
  newNode.markAsAlive();
2068
1987
  // Assign SUC return route to make sure the node knows where to get its routes from
2069
- newNode.hasSUCReturnRoute = await this.assignSUCReturnRoute(newNode.id);
1988
+ newNode.hasSUCReturnRoute = await this.assignSUCReturnRoutes(newNode.id);
2070
1989
  const opts = this._inclusionOptions;
2071
1990
  let bootstrapFailure;
2072
1991
  let smartStartFailed = false;
@@ -2180,10 +2099,6 @@ supported CCs: ${nodeInfo.supportedCCs
2180
2099
  });
2181
2100
  }
2182
2101
  }
2183
- else {
2184
- // Bootstrap the node's lifelines, so it knows where the controller is
2185
- await this.bootstrapLifelineAndWakeup(newNode);
2186
- }
2187
2102
  this.setInclusionState(Inclusion_1.InclusionState.Idle);
2188
2103
  // We're done adding this node, notify listeners
2189
2104
  const result = bootstrapFailure != undefined
@@ -2253,7 +2168,8 @@ supported CCs: ${nodeInfo.supportedCCs
2253
2168
  // If it is actually a sleeping device, it will be marked as such later
2254
2169
  newNode.markAsAlive();
2255
2170
  // Assign SUC return route to make sure the node knows where to get its routes from
2256
- newNode.hasSUCReturnRoute = await this.assignSUCReturnRoute(newNode.id);
2171
+ newNode.hasSUCReturnRoute =
2172
+ await this.assignSUCReturnRoutes(newNode.id);
2257
2173
  // Try perform the security bootstrap process. When replacing a node, we don't know any supported CCs
2258
2174
  // yet, so we need to trust the chosen inclusion strategy.
2259
2175
  const strategy = this._inclusionOptions.strategy;
@@ -2287,8 +2203,6 @@ supported CCs: ${nodeInfo.supportedCCs
2287
2203
  newNode.securityClasses.set(secClass, false);
2288
2204
  }
2289
2205
  }
2290
- // Bootstrap the node's lifelines, so it knows where the controller is
2291
- await this.bootstrapLifelineAndWakeup(newNode);
2292
2206
  // We're done adding this node, notify listeners. This also kicks off the node interview
2293
2207
  const result = bootstrapFailure != undefined
2294
2208
  ? {
@@ -2575,18 +2489,9 @@ supported CCs: ${nodeInfo.supportedCCs
2575
2489
  message: `refreshing neighbor list (attempt ${attempt})...`,
2576
2490
  direction: "outbound",
2577
2491
  });
2578
- // During inclusion, the timeout is mainly required for the node to detect all neighbors
2579
- // We do the same here, so we just reuse the timeout
2580
- const discoveryTimeout = (0, AddNodeToNetworkRequest_1.computeNeighborDiscoveryTimeout)(this.driver,
2581
- // Controllers take longer, just assume the worst case here
2582
- core_1.NodeType.Controller);
2583
2492
  try {
2584
- const resp = await this.driver.sendMessage(new RequestNodeNeighborUpdateMessages_1.RequestNodeNeighborUpdateRequest(this.driver, {
2585
- nodeId,
2586
- discoveryTimeout,
2587
- }));
2588
- if (resp.updateStatus ===
2589
- RequestNodeNeighborUpdateMessages_1.NodeNeighborUpdateStatus.UpdateDone) {
2493
+ const result = await this.discoverNodeNeighbors(nodeId);
2494
+ if (result) {
2590
2495
  this.driver.controllerLog.logNode(nodeId, {
2591
2496
  message: "neighbor list refreshed...",
2592
2497
  direction: "inbound",
@@ -2595,7 +2500,6 @@ supported CCs: ${nodeInfo.supportedCCs
2595
2500
  break;
2596
2501
  }
2597
2502
  else {
2598
- // UpdateFailed
2599
2503
  this.driver.controllerLog.logNode(nodeId, {
2600
2504
  message: "refreshing neighbor list failed...",
2601
2505
  direction: "inbound",
@@ -2616,24 +2520,16 @@ supported CCs: ${nodeInfo.supportedCCs
2616
2520
  }
2617
2521
  }
2618
2522
  // 2. re-create the SUC return route, just in case
2619
- if (await this.deleteSUCReturnRoute(nodeId)) {
2620
- node.hasSUCReturnRoute = false;
2621
- }
2622
- node.hasSUCReturnRoute = await this.assignSUCReturnRoute(nodeId);
2623
- // 3. delete all return routes so we can assign new ones
2523
+ node.hasSUCReturnRoute || (node.hasSUCReturnRoute = await this.assignSUCReturnRoutes(nodeId));
2524
+ // 3. delete all return routes to get rid of potential priority return routes
2624
2525
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2625
2526
  this.driver.controllerLog.logNode(nodeId, {
2626
2527
  message: `deleting return routes (attempt ${attempt})...`,
2627
2528
  direction: "outbound",
2628
2529
  });
2629
- try {
2630
- await this.driver.sendMessage(new DeleteReturnRouteMessages_1.DeleteReturnRouteRequest(this.driver, { nodeId }));
2631
- // this step was successful, continue with the next
2530
+ if (await this.deleteReturnRoutes(nodeId)) {
2632
2531
  break;
2633
2532
  }
2634
- catch (e) {
2635
- this.driver.controllerLog.logNode(nodeId, `deleting return routes failed: ${(0, shared_1.getErrorMessage)(e)}`, "warn");
2636
- }
2637
2533
  if (attempt === maxAttempts) {
2638
2534
  this.driver.controllerLog.logNode(nodeId, {
2639
2535
  message: `failed to delete return routes after ${maxAttempts} attempts, healing failed`,
@@ -2643,22 +2539,18 @@ supported CCs: ${nodeInfo.supportedCCs
2643
2539
  return false;
2644
2540
  }
2645
2541
  }
2646
- // 4. Assign up to 4 return routes for associations, one of which should be the controller
2542
+ // 4. Assign return routes to all association destinations.
2647
2543
  let associatedNodes = [];
2648
- const maxReturnRoutes = 4;
2649
2544
  try {
2650
2545
  associatedNodes = (0, arrays_1.distinct)((0, shared_1.flatMap)([...this.getAssociations({ nodeId }).values()], (assocs) => assocs.map((a) => a.nodeId))).sort();
2651
2546
  }
2652
2547
  catch {
2653
2548
  /* ignore */
2654
2549
  }
2655
- // Always include ourselves first
2550
+ // One of those should probably be the controller. Not sure if the SUC return route is enough.
2656
2551
  if (!associatedNodes.includes(this._ownNodeId)) {
2657
2552
  associatedNodes.unshift(this._ownNodeId);
2658
2553
  }
2659
- if (associatedNodes.length > maxReturnRoutes) {
2660
- associatedNodes = associatedNodes.slice(0, maxReturnRoutes);
2661
- }
2662
2554
  this.driver.controllerLog.logNode(nodeId, {
2663
2555
  message: `assigning return routes to the following nodes:
2664
2556
  ${associatedNodes.join(", ")}`,
@@ -2670,17 +2562,10 @@ ${associatedNodes.join(", ")}`,
2670
2562
  message: `assigning return route to node ${destinationNodeId} (attempt ${attempt})...`,
2671
2563
  direction: "outbound",
2672
2564
  });
2673
- try {
2674
- await this.driver.sendMessage(new AssignReturnRouteMessages_1.AssignReturnRouteRequest(this.driver, {
2675
- nodeId,
2676
- destinationNodeId,
2677
- }));
2565
+ if (await this.assignReturnRoutes(nodeId, destinationNodeId)) {
2678
2566
  // this step was successful, continue with the next
2679
2567
  break;
2680
2568
  }
2681
- catch (e) {
2682
- this.driver.controllerLog.logNode(nodeId, `assigning return route failed: ${(0, shared_1.getErrorMessage)(e)}`, "warn");
2683
- }
2684
2569
  if (attempt === maxAttempts) {
2685
2570
  this.driver.controllerLog.logNode(nodeId, {
2686
2571
  message: `failed to assign return route after ${maxAttempts} attempts, healing failed`,
@@ -2715,23 +2600,145 @@ ${associatedNodes.join(", ")}`,
2715
2600
  }));
2716
2601
  return result.isOK();
2717
2602
  }
2718
- async assignSUCReturnRoute(nodeId) {
2603
+ // After a lot of experimenting, it seems to make sense to document how assigning return routes works in the controller.
2604
+ // Each node has a list of 4 return routes per destination (and probably a separate list for the SUC):
2605
+ // - #0, repeaters..., speed, wakeup
2606
+ // - #1, repeaters..., speed, wakeup
2607
+ // - #2, repeaters..., speed, wakeup
2608
+ // - #3, repeaters..., speed, wakeup
2609
+ //
2610
+ // Empty slots are filled with 0 repeaters, 9.6kbit/s, no wakeup
2611
+ //
2612
+ // Calling assignReturnRoute will assign all 4 slots, some of which may be empty.
2613
+ // Calling deleteReturnRoute will assign an empty route to all 4 slots.
2614
+ //
2615
+ // Priority return routes are indicated by a separate "pointer" byte which tells the node which route is the priority.
2616
+ // Calling assignPriorityReturnRoute will first assign 4 routes, one of which is then marked as priority.
2617
+ // This is not fully understood yet, but it seems that the priority route is actually the last non-empty route.
2618
+ // If the priority byte points to an empty route, it is ignored.
2619
+ //
2620
+ // Calling assignReturnRoute after having assigned a priority return route will not clear that pointer byte. This
2621
+ // means that a previously-assigned priority route can randomly change if assignReturnRoute assigns enough routes.
2622
+ // deleteReturnRoute does also clear the priority byte.
2623
+ /** @deprecated Use {@link assignSUCReturnRoutes} instead */
2624
+ assignSUCReturnRoute(nodeId) {
2625
+ return this.assignSUCReturnRoutes(nodeId);
2626
+ }
2627
+ /**
2628
+ * Instructs the controller to assign static routes from the given end node to the SUC.
2629
+ * This will assign up to 4 routes, depending on the network topology (that the controller knows about).
2630
+ */
2631
+ async assignSUCReturnRoutes(nodeId) {
2719
2632
  this.driver.controllerLog.logNode(nodeId, {
2720
2633
  message: `Assigning SUC return route...`,
2721
2634
  direction: "outbound",
2722
2635
  });
2636
+ // Since there is only one SUC, we can do the right thing here and delete all routes first, which clears any dangling priority return routes.
2637
+ // Afterwards, we'll set up all routes again anyways.
2638
+ await this.deleteSUCReturnRoutes(nodeId);
2723
2639
  try {
2724
2640
  const result = await this.driver.sendMessage(new AssignSUCReturnRouteMessages_1.AssignSUCReturnRouteRequest(this.driver, {
2725
2641
  nodeId,
2726
2642
  }));
2727
- return this.handleRouteAssignmentTransmitReport(result, nodeId);
2643
+ const success = this.handleRouteAssignmentTransmitReport(result, nodeId);
2644
+ if (success) {
2645
+ // Custom assigned are no longer valid
2646
+ this.setCustomSUCReturnRoutesCached(nodeId, undefined);
2647
+ }
2648
+ return success;
2728
2649
  }
2729
2650
  catch (e) {
2730
2651
  this.driver.controllerLog.logNode(nodeId, `Assigning SUC return route failed: ${(0, shared_1.getErrorMessage)(e)}`, "error");
2731
2652
  return false;
2732
2653
  }
2733
2654
  }
2734
- async deleteSUCReturnRoute(nodeId) {
2655
+ /**
2656
+ * Returns which custom static routes are currently assigned from the given end node to the SUC.
2657
+ *
2658
+ * **Note:** This only considers routes that were assigned using {@link assignCustomSUCReturnRoutes}.
2659
+ * If another controller has assigned routes in the meantime, this information may be out of date.
2660
+ */
2661
+ getCustomSUCReturnRoutesCached(nodeId) {
2662
+ return (this.driver.cacheGet(NetworkCache_1.cacheKeys.node(nodeId).customSUCReturnRoutes) ?? []);
2663
+ }
2664
+ setCustomSUCReturnRoutesCached(nodeId, routes) {
2665
+ this.driver.cacheSet(NetworkCache_1.cacheKeys.node(nodeId).customSUCReturnRoutes, routes);
2666
+ }
2667
+ /**
2668
+ * Assigns static routes from the given end node to the SUC. Unlike {@link assignSUCReturnRoutes}, this method assigns
2669
+ * the given routes instead of having the controller calculate them. At most 4 routes can be assigned. If less are
2670
+ * specified, the remaining routes are cleared.
2671
+ *
2672
+ * **Note:** Calling {@link assignSUCReturnRoutes} or {@link deleteSUCReturnRoutes} will override the custom routes.
2673
+ */
2674
+ async assignCustomSUCReturnRoutes(nodeId, routes) {
2675
+ this.driver.controllerLog.logNode(nodeId, {
2676
+ message: `Assigning custom SUC return routes...`,
2677
+ direction: "outbound",
2678
+ });
2679
+ // Since there is only one SUC, we can do the right thing here and delete all routes first, which clears the priority return routes.
2680
+ await this.deleteSUCReturnRoutes(nodeId);
2681
+ let result = true;
2682
+ const MAX_ROUTES = 4;
2683
+ // Keep track of which routes have been assigned
2684
+ const assignedRoutes = this.getCustomSUCReturnRoutesCached(nodeId);
2685
+ while (assignedRoutes.length < MAX_ROUTES) {
2686
+ assignedRoutes.push(core_1.EMPTY_ROUTE);
2687
+ }
2688
+ for (let i = 0; i < MAX_ROUTES; i++) {
2689
+ const route = routes[i] ?? core_1.EMPTY_ROUTE;
2690
+ const isEmpty = (0, core_1.isEmptyRoute)(route);
2691
+ // We are always listening
2692
+ const targetWakeup = false;
2693
+ const cc = new cc_1.ZWaveProtocolCCAssignSUCReturnRoute(this.driver, {
2694
+ nodeId,
2695
+ // Empty routes are marked with a nodeId of 0
2696
+ destinationNodeId: isEmpty ? 0 : this.ownNodeId ?? 1,
2697
+ routeIndex: i,
2698
+ repeaters: route.repeaters,
2699
+ destinationSpeed: route.routeSpeed,
2700
+ destinationWakeUp: (0, cc_1.FLiRS2WakeUpTime)(targetWakeup ?? false),
2701
+ });
2702
+ try {
2703
+ // TODO: add a better method to send ZWaveProtocolCC
2704
+ await this.driver.sendCommand(cc, {
2705
+ priority: core_1.MessagePriority.MultistepController,
2706
+ autoEncapsulate: false,
2707
+ changeNodeStatusOnMissingACK: false,
2708
+ maxSendAttempts: 1,
2709
+ useSupervision: false,
2710
+ transmitOptions: core_1.TransmitOptions.AutoRoute | core_1.TransmitOptions.ACK,
2711
+ });
2712
+ // Remember that this route has been assigned
2713
+ assignedRoutes[i] = route;
2714
+ }
2715
+ catch (e) {
2716
+ this.driver.controllerLog.logNode(nodeId, {
2717
+ message: `Assigning custom SUC return route #${i} failed`,
2718
+ direction: "outbound",
2719
+ level: "warn",
2720
+ });
2721
+ result = false;
2722
+ }
2723
+ }
2724
+ // Trim empty routes off the end. We may end up with empty routes in the middle
2725
+ // if an assignment fails.
2726
+ while (assignedRoutes.length > 0 &&
2727
+ (0, core_1.isEmptyRoute)(assignedRoutes[assignedRoutes.length - 1])) {
2728
+ assignedRoutes.pop();
2729
+ }
2730
+ this.setCustomSUCReturnRoutesCached(nodeId, assignedRoutes);
2731
+ return result;
2732
+ }
2733
+ /** @deprecated use {@link deleteSUCReturnRoutes} instead */
2734
+ deleteSUCReturnRoute(nodeId) {
2735
+ return this.deleteReturnRoutes(nodeId);
2736
+ }
2737
+ /**
2738
+ * Instructs the controller to assign static routes from the given end node to the SUC.
2739
+ * This will assign up to 4 routes, depending on the network topology (that the controller knows about).
2740
+ */
2741
+ async deleteSUCReturnRoutes(nodeId) {
2735
2742
  this.driver.controllerLog.logNode(nodeId, {
2736
2743
  message: `Deleting SUC return route...`,
2737
2744
  direction: "outbound",
@@ -2740,16 +2747,52 @@ ${associatedNodes.join(", ")}`,
2740
2747
  const result = await this.driver.sendMessage(new DeleteSUCReturnRouteMessages_1.DeleteSUCReturnRouteRequest(this.driver, {
2741
2748
  nodeId,
2742
2749
  }));
2743
- return this.handleRouteAssignmentTransmitReport(result, nodeId);
2750
+ const success = this.handleRouteAssignmentTransmitReport(result, nodeId);
2751
+ if (success) {
2752
+ // Custom assigned and priority return routes are no longer valid
2753
+ this.setPrioritySUCReturnRouteCached(nodeId, undefined);
2754
+ this.setCustomSUCReturnRoutesCached(nodeId, undefined);
2755
+ }
2756
+ return success;
2744
2757
  }
2745
2758
  catch (e) {
2746
2759
  this.driver.controllerLog.logNode(nodeId, `Deleting SUC return route failed: ${(0, shared_1.getErrorMessage)(e)}`, "error");
2747
2760
  return false;
2748
2761
  }
2749
2762
  }
2750
- async assignReturnRoute(nodeId, destinationNodeId) {
2763
+ /**
2764
+ * Returns which custom static routes are currently assigned between the given end nodes.
2765
+ *
2766
+ * **Note:** This only considers routes that were assigned using {@link assignCustomReturnRoutes}.
2767
+ * If another controller has assigned routes in the meantime, this information may be out of date.
2768
+ */
2769
+ getCustomReturnRoutesCached(nodeId, destinationNodeId) {
2770
+ return (this.driver.cacheGet(NetworkCache_1.cacheKeys.node(nodeId).customReturnRoutes(destinationNodeId)) ?? []);
2771
+ }
2772
+ setCustomReturnRoutesCached(nodeId, destinationNodeId, routes) {
2773
+ this.driver.cacheSet(NetworkCache_1.cacheKeys.node(nodeId).customReturnRoutes(destinationNodeId), routes);
2774
+ }
2775
+ clearCustomReturnRoutesCached(nodeId) {
2776
+ // This is a bit ugly, but the best we can do right now.
2777
+ for (let dest = 1; dest <= core_1.MAX_NODES; dest++) {
2778
+ this.setCustomReturnRoutesCached(nodeId, dest, undefined);
2779
+ }
2780
+ }
2781
+ /** @deprecated use {@link assignReturnRoutes} instead */
2782
+ assignReturnRoute(nodeId, destinationNodeId) {
2783
+ return this.assignReturnRoutes(nodeId, destinationNodeId);
2784
+ }
2785
+ /**
2786
+ * Instructs the controller to assign static routes between the two given end nodes.
2787
+ * This will assign up to 4 routes, depending on the network topology (that the controller knows about).
2788
+ */
2789
+ async assignReturnRoutes(nodeId, destinationNodeId) {
2790
+ // Make sure this is not misused by passing the controller's node ID
2791
+ if (destinationNodeId === this.ownNodeId) {
2792
+ return this.assignSUCReturnRoutes(nodeId);
2793
+ }
2751
2794
  this.driver.controllerLog.logNode(nodeId, {
2752
- message: `Assigning return route to node ${destinationNodeId}...`,
2795
+ message: `Assigning return routes to node ${destinationNodeId}...`,
2753
2796
  direction: "outbound",
2754
2797
  });
2755
2798
  try {
@@ -2757,14 +2800,105 @@ ${associatedNodes.join(", ")}`,
2757
2800
  nodeId,
2758
2801
  destinationNodeId,
2759
2802
  }));
2760
- return this.handleRouteAssignmentTransmitReport(result, nodeId);
2803
+ const success = this.handleRouteAssignmentTransmitReport(result, nodeId);
2804
+ if (success) {
2805
+ // Custom assigned are no longer valid
2806
+ this.setCustomReturnRoutesCached(nodeId, destinationNodeId, undefined);
2807
+ // The priority route probably is invalid too now, but it may also point to a random route
2808
+ if (this.hasPriorityReturnRouteCached(nodeId, destinationNodeId) !== false) {
2809
+ this.setPriorityReturnRouteCached(nodeId, destinationNodeId, core_1.UNKNOWN_STATE);
2810
+ }
2811
+ }
2812
+ return success;
2761
2813
  }
2762
2814
  catch (e) {
2763
- this.driver.controllerLog.logNode(nodeId, `Assigning return route failed: ${(0, shared_1.getErrorMessage)(e)}`, "error");
2815
+ this.driver.controllerLog.logNode(nodeId, `Assigning return routes failed: ${(0, shared_1.getErrorMessage)(e)}`, "error");
2764
2816
  return false;
2765
2817
  }
2766
2818
  }
2767
- async deleteReturnRoute(nodeId) {
2819
+ /**
2820
+ * Assigns static routes between the two given end nodes. Unlike {@link assignReturnRoutes}, this method assigns
2821
+ * the given routes instead of having the controller calculate them. At most 4 routes can be assigned. If less are
2822
+ * specified, the remaining routes are cleared.
2823
+ *
2824
+ * **Note:** Calling {@link assignReturnRoutes} or {@link deleteReturnRoutes} will override the custom routes.
2825
+ */
2826
+ async assignCustomReturnRoutes(nodeId, destinationNodeId, routes) {
2827
+ // Make sure this is not misused by passing the controller's node ID
2828
+ if (destinationNodeId === this.ownNodeId) {
2829
+ return this.assignCustomSUCReturnRoutes(nodeId, routes);
2830
+ }
2831
+ this.driver.controllerLog.logNode(nodeId, {
2832
+ message: `Assigning custom return routes to node ${destinationNodeId}...`,
2833
+ direction: "outbound",
2834
+ });
2835
+ let result = true;
2836
+ const MAX_ROUTES = 4;
2837
+ // Keep track of which routes have been assigned
2838
+ const assignedRoutes = this.getCustomReturnRoutesCached(nodeId, destinationNodeId);
2839
+ while (assignedRoutes.length < MAX_ROUTES) {
2840
+ assignedRoutes.push(core_1.EMPTY_ROUTE);
2841
+ }
2842
+ for (let i = 0; i < MAX_ROUTES; i++) {
2843
+ const route = routes[i] ?? core_1.EMPTY_ROUTE;
2844
+ const isEmpty = (0, core_1.isEmptyRoute)(route);
2845
+ const targetWakeup = !isEmpty
2846
+ ? this.nodes.get(destinationNodeId)?.isFrequentListening
2847
+ : undefined;
2848
+ const cc = new cc_1.ZWaveProtocolCCAssignReturnRoute(this.driver, {
2849
+ nodeId,
2850
+ // Empty routes are marked with a nodeId of 0
2851
+ destinationNodeId: isEmpty ? 0 : destinationNodeId,
2852
+ routeIndex: i,
2853
+ repeaters: route.repeaters,
2854
+ destinationSpeed: route.routeSpeed,
2855
+ destinationWakeUp: (0, cc_1.FLiRS2WakeUpTime)(targetWakeup ?? false),
2856
+ });
2857
+ try {
2858
+ // TODO: add a better method to send ZWaveProtocolCC
2859
+ await this.driver.sendCommand(cc, {
2860
+ priority: core_1.MessagePriority.MultistepController,
2861
+ autoEncapsulate: false,
2862
+ changeNodeStatusOnMissingACK: false,
2863
+ maxSendAttempts: 1,
2864
+ useSupervision: false,
2865
+ transmitOptions: core_1.TransmitOptions.AutoRoute | core_1.TransmitOptions.ACK,
2866
+ });
2867
+ // Remember that this route has been assigned
2868
+ assignedRoutes[i] = route;
2869
+ }
2870
+ catch (e) {
2871
+ this.driver.controllerLog.logNode(nodeId, {
2872
+ message: `Assigning custom return route #${i} failed`,
2873
+ direction: "outbound",
2874
+ level: "warn",
2875
+ });
2876
+ result = false;
2877
+ }
2878
+ }
2879
+ // Trim empty routes off the end. We may end up with empty routes in the middle
2880
+ // if an assignment fails.
2881
+ while (assignedRoutes.length > 0 &&
2882
+ (0, core_1.isEmptyRoute)(assignedRoutes[assignedRoutes.length - 1])) {
2883
+ assignedRoutes.pop();
2884
+ }
2885
+ this.setCustomReturnRoutesCached(nodeId, destinationNodeId, assignedRoutes);
2886
+ // The priority route is probably invalid now, but it may also point to a random route
2887
+ if (this.hasPriorityReturnRouteCached(nodeId, destinationNodeId) !==
2888
+ false) {
2889
+ this.setPriorityReturnRouteCached(nodeId, destinationNodeId, core_1.UNKNOWN_STATE);
2890
+ }
2891
+ return result;
2892
+ }
2893
+ /** @deprecated use {@link deleteReturnRoutes} instead */
2894
+ deleteReturnRoute(nodeId) {
2895
+ return this.deleteReturnRoutes(nodeId);
2896
+ }
2897
+ /**
2898
+ * Instructs the controller to delete all static routes between the given node and all
2899
+ * other end nodes, including the priority return routes.
2900
+ */
2901
+ async deleteReturnRoutes(nodeId) {
2768
2902
  this.driver.controllerLog.logNode(nodeId, {
2769
2903
  message: `Deleting all return routes...`,
2770
2904
  direction: "outbound",
@@ -2773,7 +2907,13 @@ ${associatedNodes.join(", ")}`,
2773
2907
  const result = await this.driver.sendMessage(new DeleteReturnRouteMessages_1.DeleteReturnRouteRequest(this.driver, {
2774
2908
  nodeId,
2775
2909
  }));
2776
- return this.handleRouteAssignmentTransmitReport(result, nodeId);
2910
+ const success = this.handleRouteAssignmentTransmitReport(result, nodeId);
2911
+ if (success) {
2912
+ // All custom assigned routes are no longer valid
2913
+ this.clearPriorityReturnRoutesCached(nodeId);
2914
+ this.clearCustomReturnRoutesCached(nodeId);
2915
+ }
2916
+ return success;
2777
2917
  }
2778
2918
  catch (e) {
2779
2919
  this.driver.controllerLog.logNode(nodeId, `Deleting return routes failed: ${(0, shared_1.getErrorMessage)(e)}`, "error");
@@ -2788,6 +2928,10 @@ ${associatedNodes.join(", ")}`,
2788
2928
  * @param routeSpeed The transmission speed to use for the route
2789
2929
  */
2790
2930
  async assignPriorityReturnRoute(nodeId, destinationNodeId, repeaters, routeSpeed) {
2931
+ // Make sure this is not misused by passing the controller's node ID
2932
+ if (destinationNodeId === this.ownNodeId) {
2933
+ return this.assignPrioritySUCReturnRoute(nodeId, repeaters, routeSpeed);
2934
+ }
2791
2935
  this.driver.controllerLog.logNode(nodeId, {
2792
2936
  message: `Assigning priority return route to node ${destinationNodeId}...`,
2793
2937
  direction: "outbound",
@@ -2799,13 +2943,45 @@ ${associatedNodes.join(", ")}`,
2799
2943
  repeaters,
2800
2944
  routeSpeed,
2801
2945
  }));
2802
- return this.handleRouteAssignmentTransmitReport(result, nodeId);
2946
+ const success = this.handleRouteAssignmentTransmitReport(result, nodeId);
2947
+ if (success) {
2948
+ // Update the cached priority route
2949
+ this.setPriorityReturnRouteCached(nodeId, destinationNodeId, {
2950
+ repeaters,
2951
+ routeSpeed,
2952
+ });
2953
+ }
2954
+ return success;
2803
2955
  }
2804
2956
  catch (e) {
2805
2957
  this.driver.controllerLog.logNode(nodeId, `Assigning priority return route failed: ${(0, shared_1.getErrorMessage)(e)}`, "error");
2806
2958
  return false;
2807
2959
  }
2808
2960
  }
2961
+ hasPriorityReturnRouteCached(nodeId, destinationNodeId) {
2962
+ const ret = this.driver.cacheGet(NetworkCache_1.cacheKeys.node(nodeId).priorityReturnRoute(destinationNodeId));
2963
+ if (ret === core_1.UNKNOWN_STATE)
2964
+ return core_1.UNKNOWN_STATE;
2965
+ return ret !== undefined;
2966
+ }
2967
+ setPriorityReturnRouteCached(nodeId, destinationNodeId, route) {
2968
+ this.driver.cacheSet(NetworkCache_1.cacheKeys.node(nodeId).priorityReturnRoute(destinationNodeId), route);
2969
+ }
2970
+ clearPriorityReturnRoutesCached(nodeId) {
2971
+ // This is a bit ugly, but the best we can do right now.
2972
+ for (let dest = 1; dest <= core_1.MAX_NODES; dest++) {
2973
+ this.setPriorityReturnRouteCached(nodeId, dest, undefined);
2974
+ }
2975
+ }
2976
+ /**
2977
+ * Returns which priority route is currently assigned between the given end nodes.
2978
+ *
2979
+ * **Note:** This is using cached information, since there's no way to query priority routes from a node.
2980
+ * If another controller has assigned routes in the meantime, this information may be out of date.
2981
+ */
2982
+ getPriorityReturnRouteCached(nodeId, destinationNodeId) {
2983
+ return this.driver.cacheGet(NetworkCache_1.cacheKeys.node(nodeId).priorityReturnRoute(destinationNodeId));
2984
+ }
2809
2985
  /**
2810
2986
  * Assigns a priority route from an end node to the SUC. This route will always be used for the first transmission attempt.
2811
2987
  * @param nodeId The ID of the end node for which to assign the route
@@ -2823,13 +2999,36 @@ ${associatedNodes.join(", ")}`,
2823
2999
  repeaters,
2824
3000
  routeSpeed,
2825
3001
  }));
2826
- return this.handleRouteAssignmentTransmitReport(result, nodeId);
3002
+ const success = this.handleRouteAssignmentTransmitReport(result, nodeId);
3003
+ if (success) {
3004
+ // Update the cached priority route
3005
+ this.setPrioritySUCReturnRouteCached(nodeId, {
3006
+ repeaters,
3007
+ routeSpeed,
3008
+ });
3009
+ // The command above assigns a full set of new routes, so
3010
+ // custom SUC return routes are no longer valid
3011
+ this.setCustomSUCReturnRoutesCached(nodeId, undefined);
3012
+ }
3013
+ return success;
2827
3014
  }
2828
3015
  catch (e) {
2829
3016
  this.driver.controllerLog.logNode(nodeId, `Assigning priority SUC return route failed: ${(0, shared_1.getErrorMessage)(e)}`, "error");
2830
3017
  return false;
2831
3018
  }
2832
3019
  }
3020
+ setPrioritySUCReturnRouteCached(nodeId, route) {
3021
+ this.driver.cacheSet(NetworkCache_1.cacheKeys.node(nodeId).prioritySUCReturnRoute, route);
3022
+ }
3023
+ /**
3024
+ * Returns which priority route is currently assigned from the given end node to the SUC.
3025
+ *
3026
+ * **Note:** This is using cached information, since there's no way to query priority routes from a node.
3027
+ * If another controller has assigned routes in the meantime, this information may be out of date.
3028
+ */
3029
+ getPrioritySUCReturnRouteCached(nodeId) {
3030
+ return this.driver.cacheGet(NetworkCache_1.cacheKeys.node(nodeId).prioritySUCReturnRoute);
3031
+ }
2833
3032
  handleRouteAssignmentTransmitReport(msg, nodeId) {
2834
3033
  switch (msg.transmitStatus) {
2835
3034
  case core_1.TransmitStatus.OK:
@@ -2980,7 +3179,7 @@ ${associatedNodes.join(", ")}`,
2980
3179
  // Nodes need a return route to be able to send commands to other nodes
2981
3180
  const destinationNodeIDs = (0, arrays_1.distinct)(destinations.map((d) => d.nodeId)).filter((id) => id !== this.ownNodeId);
2982
3181
  for (const id of destinationNodeIDs) {
2983
- await this.assignReturnRoute(source.nodeId, id);
3182
+ await this.assignReturnRoutes(source.nodeId, id);
2984
3183
  }
2985
3184
  }
2986
3185
  /**
@@ -3243,6 +3442,35 @@ ${associatedNodes.join(", ")}`,
3243
3442
  }
3244
3443
  return result.maxPayloadSize;
3245
3444
  }
3445
+ /**
3446
+ * Instructs a node to (re-)discover its neighbors.
3447
+ *
3448
+ * **WARNING:** On some controllers, this can cause new SUC return routes to be assigned.
3449
+ *
3450
+ * @returns `true` if the update was successful and the new neighbors can be retrieved using
3451
+ * {@link getKnownNodeNeighbors}. `false` if the update failed.
3452
+ */
3453
+ async discoverNodeNeighbors(nodeId) {
3454
+ // TODO: Consider making this not block the send queue.
3455
+ // However, I haven't actually seen a UpdateStarted callback in the wild,
3456
+ // so we don't know if that would even work.
3457
+ // During inclusion, the timeout is mainly required for the node to detect all neighbors
3458
+ // We do the same here, so we just reuse the timeout
3459
+ const discoveryTimeout = (0, AddNodeToNetworkRequest_1.computeNeighborDiscoveryTimeout)(this.driver,
3460
+ // Controllers take longer, just assume the worst case here
3461
+ core_1.NodeType.Controller);
3462
+ const resp = await this.driver.sendMessage(new RequestNodeNeighborUpdateMessages_1.RequestNodeNeighborUpdateRequest(this.driver, {
3463
+ nodeId,
3464
+ discoveryTimeout,
3465
+ }));
3466
+ const success = resp.updateStatus === RequestNodeNeighborUpdateMessages_1.NodeNeighborUpdateStatus.UpdateDone;
3467
+ if (success) {
3468
+ // Not sure why, but Zniffer traces show that a node neighbor update can cause the controller to
3469
+ // also do AssignSUCReturnRoute. As a result, we need to invalidate our route cache.
3470
+ this.setCustomSUCReturnRoutesCached(nodeId, undefined);
3471
+ }
3472
+ return success;
3473
+ }
3246
3474
  /**
3247
3475
  * Returns the known list of neighbors for a node
3248
3476
  */
@@ -4199,8 +4427,7 @@ ${associatedNodes.join(", ")}`,
4199
4427
  }
4200
4428
  }
4201
4429
  };
4202
- ZWaveController = __decorate([
4430
+ exports.ZWaveController = ZWaveController = __decorate([
4203
4431
  (0, shared_1.Mixin)([ControllerStatistics_1.ControllerStatisticsHost])
4204
4432
  ], ZWaveController);
4205
- exports.ZWaveController = ZWaveController;
4206
4433
  //# sourceMappingURL=Controller.js.map