@willieee802/zigbee-herdsman 0.49.3 → 0.50.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 (211) hide show
  1. package/.github/dependabot.yml +0 -3
  2. package/.github/workflows/ci.yml +1 -2
  3. package/.github/workflows/release-please.yml +1 -1
  4. package/.github/workflows/typedoc.yaml +3 -3
  5. package/.release-please-manifest.json +1 -1
  6. package/CHANGELOG.md +143 -0
  7. package/biome.json +1 -1
  8. package/dist/adapter/adapter.d.ts +14 -1
  9. package/dist/adapter/adapter.d.ts.map +1 -1
  10. package/dist/adapter/adapter.js +17 -0
  11. package/dist/adapter/adapter.js.map +1 -1
  12. package/dist/adapter/adapterDiscovery.d.ts.map +1 -1
  13. package/dist/adapter/adapterDiscovery.js.map +1 -1
  14. package/dist/adapter/deconz/adapter/deconzAdapter.d.ts +1 -3
  15. package/dist/adapter/deconz/adapter/deconzAdapter.d.ts.map +1 -1
  16. package/dist/adapter/deconz/adapter/deconzAdapter.js +14 -29
  17. package/dist/adapter/deconz/adapter/deconzAdapter.js.map +1 -1
  18. package/dist/adapter/deconz/driver/constants.d.ts +1 -1
  19. package/dist/adapter/deconz/driver/constants.d.ts.map +1 -1
  20. package/dist/adapter/ember/adapter/emberAdapter.d.ts +1 -1
  21. package/dist/adapter/ember/adapter/emberAdapter.d.ts.map +1 -1
  22. package/dist/adapter/ember/adapter/emberAdapter.js +19 -10
  23. package/dist/adapter/ember/adapter/emberAdapter.js.map +1 -1
  24. package/dist/adapter/ember/adapter/oneWaitress.d.ts +2 -0
  25. package/dist/adapter/ember/adapter/oneWaitress.d.ts.map +1 -1
  26. package/dist/adapter/ember/adapter/oneWaitress.js +13 -5
  27. package/dist/adapter/ember/adapter/oneWaitress.js.map +1 -1
  28. package/dist/adapter/ezsp/adapter/ezspAdapter.d.ts +1 -3
  29. package/dist/adapter/ezsp/adapter/ezspAdapter.d.ts.map +1 -1
  30. package/dist/adapter/ezsp/adapter/ezspAdapter.js +17 -30
  31. package/dist/adapter/ezsp/adapter/ezspAdapter.js.map +1 -1
  32. package/dist/adapter/ezsp/driver/index.d.ts +1 -1
  33. package/dist/adapter/ezsp/driver/index.d.ts.map +1 -1
  34. package/dist/adapter/ezsp/driver/index.js +1 -1
  35. package/dist/adapter/ezsp/driver/index.js.map +1 -1
  36. package/dist/adapter/ezsp/driver/types/index.d.ts +1 -1
  37. package/dist/adapter/ezsp/driver/types/index.d.ts.map +1 -1
  38. package/dist/adapter/ezsp/driver/types/index.js +3 -3
  39. package/dist/adapter/ezsp/driver/types/index.js.map +1 -1
  40. package/dist/adapter/serialPort.d.ts.map +1 -1
  41. package/dist/adapter/serialPort.js +7 -0
  42. package/dist/adapter/serialPort.js.map +1 -1
  43. package/dist/adapter/z-stack/adapter/adapter-backup.js +1 -1
  44. package/dist/adapter/z-stack/adapter/adapter-backup.js.map +1 -1
  45. package/dist/adapter/z-stack/adapter/adapter-nv-memory.js +1 -1
  46. package/dist/adapter/z-stack/adapter/adapter-nv-memory.js.map +1 -1
  47. package/dist/adapter/z-stack/adapter/manager.d.ts.map +1 -1
  48. package/dist/adapter/z-stack/adapter/manager.js +12 -2
  49. package/dist/adapter/z-stack/adapter/manager.js.map +1 -1
  50. package/dist/adapter/z-stack/adapter/tstype.d.ts.map +1 -1
  51. package/dist/adapter/z-stack/adapter/zStackAdapter.d.ts +1 -3
  52. package/dist/adapter/z-stack/adapter/zStackAdapter.d.ts.map +1 -1
  53. package/dist/adapter/z-stack/adapter/zStackAdapter.js +20 -34
  54. package/dist/adapter/z-stack/adapter/zStackAdapter.js.map +1 -1
  55. package/dist/adapter/z-stack/constants/index.d.ts +1 -1
  56. package/dist/adapter/z-stack/constants/index.d.ts.map +1 -1
  57. package/dist/adapter/z-stack/constants/index.js +1 -1
  58. package/dist/adapter/z-stack/constants/index.js.map +1 -1
  59. package/dist/adapter/z-stack/unpi/constants.d.ts +1 -1
  60. package/dist/adapter/z-stack/unpi/constants.d.ts.map +1 -1
  61. package/dist/adapter/z-stack/unpi/constants.js +1 -1
  62. package/dist/adapter/z-stack/unpi/constants.js.map +1 -1
  63. package/dist/adapter/zboss/adapter/zbossAdapter.d.ts +7 -8
  64. package/dist/adapter/zboss/adapter/zbossAdapter.d.ts.map +1 -1
  65. package/dist/adapter/zboss/adapter/zbossAdapter.js +12 -30
  66. package/dist/adapter/zboss/adapter/zbossAdapter.js.map +1 -1
  67. package/dist/adapter/zboss/driver.d.ts.map +1 -1
  68. package/dist/adapter/zboss/driver.js +8 -1
  69. package/dist/adapter/zboss/driver.js.map +1 -1
  70. package/dist/adapter/zboss/uart.d.ts.map +1 -1
  71. package/dist/adapter/zboss/uart.js +14 -2
  72. package/dist/adapter/zboss/uart.js.map +1 -1
  73. package/dist/adapter/zigate/adapter/zigateAdapter.d.ts +1 -3
  74. package/dist/adapter/zigate/adapter/zigateAdapter.d.ts.map +1 -1
  75. package/dist/adapter/zigate/adapter/zigateAdapter.js +8 -29
  76. package/dist/adapter/zigate/adapter/zigateAdapter.js.map +1 -1
  77. package/dist/adapter/zoh/adapter/zohAdapter.d.ts +1 -3
  78. package/dist/adapter/zoh/adapter/zohAdapter.d.ts.map +1 -1
  79. package/dist/adapter/zoh/adapter/zohAdapter.js +18 -33
  80. package/dist/adapter/zoh/adapter/zohAdapter.js.map +1 -1
  81. package/dist/controller/controller.d.ts.map +1 -1
  82. package/dist/controller/controller.js +10 -2
  83. package/dist/controller/controller.js.map +1 -1
  84. package/dist/controller/greenPower.d.ts.map +1 -1
  85. package/dist/controller/greenPower.js +15 -9
  86. package/dist/controller/greenPower.js.map +1 -1
  87. package/dist/controller/helpers/ota.d.ts +4 -4
  88. package/dist/controller/helpers/ota.d.ts.map +1 -1
  89. package/dist/controller/helpers/ota.js +28 -9
  90. package/dist/controller/helpers/ota.js.map +1 -1
  91. package/dist/controller/helpers/zclFrameConverter.d.ts.map +1 -1
  92. package/dist/controller/helpers/zclFrameConverter.js +17 -16
  93. package/dist/controller/helpers/zclFrameConverter.js.map +1 -1
  94. package/dist/controller/model/device.d.ts +14 -4
  95. package/dist/controller/model/device.d.ts.map +1 -1
  96. package/dist/controller/model/device.js +167 -85
  97. package/dist/controller/model/device.js.map +1 -1
  98. package/dist/controller/model/endpoint.d.ts +7 -3
  99. package/dist/controller/model/endpoint.d.ts.map +1 -1
  100. package/dist/controller/model/endpoint.js +34 -21
  101. package/dist/controller/model/endpoint.js.map +1 -1
  102. package/dist/controller/model/group.d.ts +0 -1
  103. package/dist/controller/model/group.d.ts.map +1 -1
  104. package/dist/controller/model/group.js +14 -19
  105. package/dist/controller/model/group.js.map +1 -1
  106. package/dist/controller/touchlink.js +3 -3
  107. package/dist/controller/touchlink.js.map +1 -1
  108. package/dist/utils/timeService.js +2 -2
  109. package/dist/utils/timeService.js.map +1 -1
  110. package/dist/zspec/zcl/buffaloZcl.d.ts +3 -3
  111. package/dist/zspec/zcl/buffaloZcl.d.ts.map +1 -1
  112. package/dist/zspec/zcl/buffaloZcl.js +198 -96
  113. package/dist/zspec/zcl/buffaloZcl.js.map +1 -1
  114. package/dist/zspec/zcl/definition/cluster.d.ts +2 -2
  115. package/dist/zspec/zcl/definition/cluster.d.ts.map +1 -1
  116. package/dist/zspec/zcl/definition/cluster.js +2699 -2808
  117. package/dist/zspec/zcl/definition/cluster.js.map +1 -1
  118. package/dist/zspec/zcl/definition/clusters-types.d.ts +63 -1109
  119. package/dist/zspec/zcl/definition/clusters-types.d.ts.map +1 -1
  120. package/dist/zspec/zcl/definition/enums.d.ts +0 -1
  121. package/dist/zspec/zcl/definition/enums.d.ts.map +1 -1
  122. package/dist/zspec/zcl/definition/enums.js +0 -1
  123. package/dist/zspec/zcl/definition/enums.js.map +1 -1
  124. package/dist/zspec/zcl/definition/foundation.d.ts +306 -7
  125. package/dist/zspec/zcl/definition/foundation.d.ts.map +1 -1
  126. package/dist/zspec/zcl/definition/foundation.js +552 -207
  127. package/dist/zspec/zcl/definition/foundation.js.map +1 -1
  128. package/dist/zspec/zcl/definition/status.d.ts +21 -10
  129. package/dist/zspec/zcl/definition/status.d.ts.map +1 -1
  130. package/dist/zspec/zcl/definition/status.js +11 -0
  131. package/dist/zspec/zcl/definition/status.js.map +1 -1
  132. package/dist/zspec/zcl/definition/tstype.d.ts +57 -48
  133. package/dist/zspec/zcl/definition/tstype.d.ts.map +1 -1
  134. package/dist/zspec/zcl/utils.d.ts +7 -4
  135. package/dist/zspec/zcl/utils.d.ts.map +1 -1
  136. package/dist/zspec/zcl/utils.js +133 -240
  137. package/dist/zspec/zcl/utils.js.map +1 -1
  138. package/dist/zspec/zcl/zclFrame.d.ts +4 -4
  139. package/dist/zspec/zcl/zclFrame.d.ts.map +1 -1
  140. package/dist/zspec/zcl/zclFrame.js +19 -103
  141. package/dist/zspec/zcl/zclFrame.js.map +1 -1
  142. package/dist/zspec/zcl/zclStatusError.d.ts +1 -1
  143. package/dist/zspec/zcl/zclStatusError.d.ts.map +1 -1
  144. package/dist/zspec/zcl/zclStatusError.js +2 -2
  145. package/dist/zspec/zcl/zclStatusError.js.map +1 -1
  146. package/package.json +1 -1
  147. package/scripts/clusters-typegen.ts +44 -139
  148. package/src/adapter/adapter.ts +38 -3
  149. package/src/adapter/adapterDiscovery.ts +2 -1
  150. package/src/adapter/deconz/adapter/deconzAdapter.ts +24 -51
  151. package/src/adapter/deconz/driver/constants.ts +1 -1
  152. package/src/adapter/ember/adapter/emberAdapter.ts +23 -10
  153. package/src/adapter/ember/adapter/oneWaitress.ts +16 -6
  154. package/src/adapter/ezsp/adapter/ezspAdapter.ts +27 -48
  155. package/src/adapter/ezsp/driver/index.ts +1 -1
  156. package/src/adapter/ezsp/driver/types/index.ts +99 -99
  157. package/src/adapter/serialPort.ts +9 -0
  158. package/src/adapter/z-stack/adapter/adapter-backup.ts +1 -1
  159. package/src/adapter/z-stack/adapter/adapter-nv-memory.ts +1 -1
  160. package/src/adapter/z-stack/adapter/manager.ts +16 -2
  161. package/src/adapter/z-stack/adapter/tstype.ts +1 -0
  162. package/src/adapter/z-stack/adapter/zStackAdapter.ts +34 -81
  163. package/src/adapter/z-stack/constants/index.ts +1 -1
  164. package/src/adapter/z-stack/unpi/constants.ts +1 -1
  165. package/src/adapter/zboss/adapter/zbossAdapter.ts +23 -54
  166. package/src/adapter/zboss/driver.ts +8 -1
  167. package/src/adapter/zboss/uart.ts +14 -1
  168. package/src/adapter/zigate/adapter/zigateAdapter.ts +17 -48
  169. package/src/adapter/zoh/adapter/zohAdapter.ts +27 -50
  170. package/src/controller/controller.ts +12 -2
  171. package/src/controller/greenPower.ts +16 -9
  172. package/src/controller/helpers/ota.ts +37 -11
  173. package/src/controller/helpers/zclFrameConverter.ts +20 -17
  174. package/src/controller/model/device.ts +204 -97
  175. package/src/controller/model/endpoint.ts +36 -24
  176. package/src/controller/model/group.ts +14 -20
  177. package/src/controller/touchlink.ts +3 -3
  178. package/src/utils/timeService.ts +2 -2
  179. package/src/zspec/zcl/buffaloZcl.ts +226 -100
  180. package/src/zspec/zcl/definition/cluster.ts +2713 -2822
  181. package/src/zspec/zcl/definition/clusters-types.ts +80 -1135
  182. package/src/zspec/zcl/definition/enums.ts +0 -1
  183. package/src/zspec/zcl/definition/foundation.ts +703 -216
  184. package/src/zspec/zcl/definition/status.ts +22 -11
  185. package/src/zspec/zcl/definition/tstype.ts +59 -58
  186. package/src/zspec/zcl/utils.ts +137 -264
  187. package/src/zspec/zcl/zclFrame.ts +25 -130
  188. package/src/zspec/zcl/zclStatusError.ts +2 -2
  189. package/test/adapter/ember/emberAdapter.test.ts +191 -4
  190. package/test/adapter/ezsp/uart.test.ts +10 -10
  191. package/test/adapter/z-stack/adapter.test.ts +88 -32
  192. package/test/adapter/zoh/zohAdapter.test.ts +4 -4
  193. package/test/controller.test.ts +822 -248
  194. package/test/device-ota.test.ts +141 -16
  195. package/test/device.test.ts +731 -0
  196. package/test/requests.bench.ts +2 -0
  197. package/test/zcl.test.ts +70 -95
  198. package/test/zspec/zcl/buffalo.test.ts +251 -11
  199. package/test/zspec/zcl/foundation.test.ts +990 -0
  200. package/test/zspec/zcl/frame.test.ts +84 -69
  201. package/test/zspec/zcl/utils.test.ts +105 -81
  202. package/tsconfig.json +0 -1
  203. package/scripts/check-clusters-changes.ts +0 -328
  204. package/scripts/clusters-changes.log +0 -584
  205. package/scripts/utils.ts +0 -88
  206. package/scripts/zap-update-clusters-report.json +0 -303
  207. package/scripts/zap-update-clusters.ts +0 -1520
  208. package/scripts/zap-update-types.ts +0 -707
  209. package/scripts/zap-xml-clusters-overrides-data.ts +0 -52
  210. package/scripts/zap-xml-clusters-overrides.ts +0 -400
  211. package/scripts/zap-xml-types.ts +0 -146
@@ -7,9 +7,9 @@ import * as ZSpec from "../../zspec";
7
7
  import {BroadcastAddress} from "../../zspec/enums";
8
8
  import type {Eui64} from "../../zspec/tstypes";
9
9
  import * as Zcl from "../../zspec/zcl";
10
- import type {TClusterCommandPayload, TClusterPayload, TPartialClusterAttributes} from "../../zspec/zcl/definition/clusters-types";
11
- import type {ClusterDefinition, CustomClusters} from "../../zspec/zcl/definition/tstype";
12
- import type {TZclFrame} from "../../zspec/zcl/zclFrame";
10
+ import type {TClusterCommandPayload, TPartialClusterAttributes} from "../../zspec/zcl/definition/clusters-types";
11
+ import type {Cluster, CustomClusters} from "../../zspec/zcl/definition/tstype";
12
+ import type {TFoundationZclFrame, TZclFrame} from "../../zspec/zcl/zclFrame";
13
13
  import * as Zdo from "../../zspec/zdo";
14
14
  import type {BindingTableEntry, LQITableEntry, RoutingTableEntry} from "../../zspec/zdo/definition/tstypes";
15
15
  import type {ControllerEventMap} from "../controller";
@@ -43,6 +43,11 @@ const INTERVIEW_GENBASIC_ATTRIBUTES = [
43
43
  "swBuildId",
44
44
  ] as const;
45
45
 
46
+ const GEN_BASIC_CLUSTER_ID = Zcl.Clusters.genBasic.ID;
47
+ const GEN_TIME_CLUSTER_ID = Zcl.Clusters.genTime.ID;
48
+ const GEN_POLL_CTRL_CLUSTER_ID = Zcl.Clusters.genPollCtrl.ID;
49
+ const GEN_OTA_CLUSTER_ID = Zcl.Clusters.genOta.ID;
50
+
46
51
  type CustomReadResponse = (frame: Zcl.Frame, endpoint: Endpoint) => boolean;
47
52
 
48
53
  export enum InterviewState {
@@ -74,6 +79,7 @@ export class Device extends Entity<ControllerEventMap> {
74
79
  private _gpSecurityKey?: number[];
75
80
  #scheduledOta: OtaSource | undefined;
76
81
  #otaInProgress = false;
82
+ #otaAbortController: AbortController | undefined;
77
83
 
78
84
  // Getters/setters
79
85
  get ieeeAddr(): string {
@@ -197,6 +203,7 @@ export class Device extends Entity<ControllerEventMap> {
197
203
  get customReadResponse(): CustomReadResponse | undefined {
198
204
  return this._customReadResponse;
199
205
  }
206
+ /** If the set function returns true, the default read response behavior is skipped */
200
207
  set customReadResponse(customReadResponse: CustomReadResponse | undefined) {
201
208
  this._customReadResponse = customReadResponse;
202
209
  }
@@ -235,7 +242,6 @@ export class Device extends Entity<ControllerEventMap> {
235
242
  // This lookup contains all devices that are queried from the database, this is to ensure that always
236
243
  // the same instance is returned.
237
244
  private static readonly devices: Map<number, Map<string, Device>> = new Map<number, Map<string, Device>>();
238
- private static loadedFromDatabase = false;
239
245
  private static readonly deletedDevices: Map<number /* databaseID */, Map<string /* IEEE */, Device>> = new Map();
240
246
  private static readonly nwkToIeeeCache: Map<number /* databaseID */, Map<number /* nwk addr */, string /* IEEE */>> = new Map();
241
247
 
@@ -291,6 +297,21 @@ export class Device extends Entity<ControllerEventMap> {
291
297
  this.#scheduledOta = scheduledOta;
292
298
  }
293
299
 
300
+ /**
301
+ * Reset transient data about the device.
302
+ * @param cache If true, reset some previously cached data.
303
+ * Should be set to true when device potentially changed its internal data to prevent mismatching state/config.
304
+ */
305
+ resetTransient(cache: boolean): void {
306
+ this._lastDefaultResponseSequenceNumber = undefined;
307
+
308
+ if (cache) {
309
+ // force retrieving this data again
310
+ this._checkinInterval = undefined;
311
+ this._pendingRequestTimeout = 0;
312
+ }
313
+ }
314
+
294
315
  public createEndpoint(id: number): Endpoint {
295
316
  if (this.getEndpoint(id)) {
296
317
  throw new Error(`Device '${this.ieeeAddr}' already has an endpoint '${id}'`);
@@ -349,72 +370,141 @@ export class Device extends Entity<ControllerEventMap> {
349
370
  return this.endpoints.find((e) => e.hasPendingRequests()) !== undefined;
350
371
  }
351
372
 
352
- public async onZclData(dataPayload: AdapterEvents.ZclPayload, frame: Zcl.Frame, endpoint: Endpoint): Promise<void> {
373
+ public async onZclData(
374
+ dataPayload: AdapterEvents.ZclPayload,
375
+ frame: Zcl.Frame,
376
+ endpoint: Endpoint,
377
+ defaultResponse: Zcl.Status | undefined,
378
+ ): Promise<void> {
353
379
  if (!Device.devices.get(this.databaseID)?.has(this.ieeeAddr)) {
354
380
  // prevent race conditions where device gets deleted during processing
355
381
  return;
356
382
  }
357
383
 
358
- if (frame.header.isGlobal) {
359
- // Response to read requests
360
- if (frame.command.name === "read" && !this._customReadResponse?.(frame, endpoint)) {
361
- const attributes: {[s: string]: KeyValue} = {
362
- ...endpoint.clusters,
363
- };
384
+ if (this.type === "GreenPower") {
385
+ // nothing below applies
386
+ return;
387
+ }
364
388
 
365
- const isTimeReadRequest = dataPayload.clusterID === Zcl.Clusters.genTime.ID;
366
- if (isTimeReadRequest) {
367
- attributes.genTime = {
368
- attributes: timeService.getTimeClusterAttributes(),
369
- };
370
- }
389
+ const {header, command, cluster} = frame;
390
+ let sendDefaultResponse = !dataPayload.wasBroadcast && command.response === undefined;
391
+ let defaultResponseStatus = defaultResponse ?? Zcl.Status.SUCCESS;
392
+
393
+ if (header.isGlobal) {
394
+ // Response to read requests from device to coordinator
395
+ switch (command.name) {
396
+ case "read": {
397
+ // NOTE: `sendDefaultResponse` always false from `command.response === 0x01`
398
+
399
+ if (this._customReadResponse?.(frame, endpoint)) {
400
+ break;
401
+ }
371
402
 
372
- if (frame.cluster.name in attributes) {
373
403
  const response: KeyValue = {};
374
404
 
375
- for (const entry of frame.payload) {
376
- const name = frame.cluster.getAttribute(entry.attrId)?.name;
405
+ switch (dataPayload.clusterID) {
406
+ case GEN_TIME_CLUSTER_ID: {
407
+ // relax type to index by attr name, undefined results in non-success attr record
408
+ const timeAttrs = timeService.getTimeClusterAttributes() as Record<string, unknown>;
409
+
410
+ for (const entry of frame.payload) {
411
+ // TODO: this.manufacturerID or frame.header.manufacturerCode
412
+ const name = Zcl.Utils.getClusterAttribute(cluster, entry.attrId, this.manufacturerID)?.name;
377
413
 
378
- if (name && name in attributes[frame.cluster.name].attributes) {
379
- response[name] = attributes[frame.cluster.name].attributes[name];
414
+ if (name === undefined) {
415
+ // UNSUPPORTED_ATTRIBUTE
416
+ response[entry.attrId] = {value: undefined, type: Zcl.DataType.NO_DATA};
417
+ } else {
418
+ response[name] = timeAttrs[name];
419
+ }
420
+ }
421
+ break;
422
+ }
423
+ // NOTE: can add more clusters here to use defaults from spec as needed
424
+ case GEN_BASIC_CLUSTER_ID: {
425
+ for (const entry of frame.payload) {
426
+ // TODO: this.manufacturerID or frame.header.manufacturerCode
427
+ const attr = Zcl.Utils.getClusterAttribute(cluster, entry.attrId, this.manufacturerID);
428
+
429
+ if (attr?.default === undefined) {
430
+ // UNSUPPORTED_ATTRIBUTE
431
+ response[entry.attrId] = {value: undefined, type: Zcl.DataType.NO_DATA};
432
+ } else {
433
+ response[attr.name] = attr.default;
434
+ }
435
+ }
436
+
437
+ break;
438
+ }
439
+ default: {
440
+ for (const entry of frame.payload) {
441
+ // UNSUPPORTED_ATTRIBUTE
442
+ response[entry.attrId] = {value: undefined, type: Zcl.DataType.NO_DATA};
443
+ }
444
+
445
+ break;
380
446
  }
381
447
  }
382
448
 
383
449
  try {
384
- await endpoint.readResponse(frame.cluster.ID, frame.header.transactionSequenceNumber, response, {
450
+ await endpoint.readResponse(cluster.ID, header.transactionSequenceNumber, response, {
385
451
  srcEndpoint: dataPayload.destinationEndpoint,
386
452
  });
387
453
  } catch (error) {
388
454
  logger.error(`Read response to ${this.ieeeAddr} failed (${(error as Error).message})`, NS);
455
+ // XXX: technically, if `readResponse` fails before reaching the network (internal to ZH), we should send a default response
456
+ // currently not possible due to implementation (no distinction as to "where" it failed)
389
457
  }
458
+
459
+ break;
460
+ }
461
+ case "defaultRsp": {
462
+ sendDefaultResponse = false; // per spec
463
+ break;
390
464
  }
391
465
  }
392
- } else if (frame.header.isSpecific) {
393
- switch (frame.cluster.name) {
466
+ } else if (header.isSpecific) {
467
+ switch (cluster.name) {
394
468
  case "ssIasZone": {
395
- if (frame.command.name === "enrollReq") {
469
+ if (command.name === "enrollReq") {
396
470
  // Respond to enroll requests
397
471
  logger.debug(`IAS - '${this.ieeeAddr}' responding to enroll response`, NS);
398
472
 
399
- await endpoint.command("ssIasZone", "enrollRsp", {enrollrspcode: 0, zoneid: 23}, {disableDefaultResponse: true});
473
+ try {
474
+ await endpoint.command(
475
+ "ssIasZone",
476
+ "enrollRsp",
477
+ {enrollrspcode: 0, zoneid: 23},
478
+ {transactionSequenceNumber: header.transactionSequenceNumber, disableDefaultResponse: true},
479
+ );
480
+
481
+ sendDefaultResponse = false; // per spec, sending a specific response TODO: no "Effect on receipt" in spec, is this correct?
482
+ } catch (error) {
483
+ logger.error(`Handling of IAS zone enroll for ${this.ieeeAddr} failed (${(error as Error).message})`, NS);
484
+ defaultResponseStatus = Zcl.Status.FAILURE;
485
+ }
400
486
  }
401
487
  break;
402
488
  }
403
489
  case "genPollCtrl": {
404
- if (frame.command.name === "checkin") {
490
+ if (command.name === "checkin") {
491
+ let startedFastPolling = false;
492
+
405
493
  // Handle check-in from sleeping end devices
406
494
  try {
407
495
  if (this.hasPendingRequests() || this._checkinInterval === undefined) {
408
496
  logger.debug(`check-in from ${this.ieeeAddr}: accepting fast-poll`, NS);
409
497
  await endpoint.command(
410
- frame.cluster.name as "genPollCtrl",
498
+ cluster.name as "genPollCtrl",
411
499
  "checkinRsp",
500
+ {startFastPolling: 1, fastPollTimeout: 0},
412
501
  {
413
- startFastPolling: 1,
414
- fastPollTimeout: 0,
502
+ transactionSequenceNumber: header.transactionSequenceNumber,
503
+ disableDefaultResponse: true,
504
+ sendPolicy: "immediate",
415
505
  },
416
- {sendPolicy: "immediate"},
417
506
  );
507
+ startedFastPolling = true;
418
508
 
419
509
  // This is a good time to read the checkin interval if we haven't stored it previously
420
510
  if (this._checkinInterval === undefined) {
@@ -428,24 +518,35 @@ export class Device extends Entity<ControllerEventMap> {
428
518
  }
429
519
 
430
520
  await Promise.all(this.endpoints.map(async (e) => await e.sendPendingRequests(true)));
431
- // We *must* end fast-poll when we're done sending things. Otherwise
432
- // we cause undue power-drain.
433
- logger.debug(`check-in from ${this.ieeeAddr}: stopping fast-poll`, NS);
434
- await endpoint.command(frame.cluster.name as "genPollCtrl", "fastPollStop", {}, {sendPolicy: "immediate"});
435
521
  } else {
436
522
  logger.debug(`check-in from ${this.ieeeAddr}: declining fast-poll`, NS);
437
523
  await endpoint.command(
438
- frame.cluster.name as "genPollCtrl",
524
+ cluster.name as "genPollCtrl",
439
525
  "checkinRsp",
526
+ {startFastPolling: 0, fastPollTimeout: 0},
440
527
  {
441
- startFastPolling: 0,
442
- fastPollTimeout: 0,
528
+ transactionSequenceNumber: header.transactionSequenceNumber,
529
+ disableDefaultResponse: true,
530
+ sendPolicy: "immediate",
443
531
  },
444
- {sendPolicy: "immediate"},
445
532
  );
446
533
  }
534
+
535
+ sendDefaultResponse = false; // per spec, sending a specific response
447
536
  } catch (error) {
448
537
  logger.error(`Handling of poll check-in from ${this.ieeeAddr} failed (${(error as Error).message})`, NS);
538
+ defaultResponseStatus = Zcl.Status.FAILURE;
539
+ } finally {
540
+ if (startedFastPolling) {
541
+ // We *must* end fast-poll when we're done sending things. Otherwise we cause undue power-drain.
542
+ logger.debug(`check-in from ${this.ieeeAddr}: stopping fast-poll`, NS);
543
+
544
+ try {
545
+ await endpoint.command(cluster.name as "genPollCtrl", "fastPollStop", {}, {sendPolicy: "immediate"});
546
+ } catch (error) {
547
+ logger.error(`Failed to stop fast poll for ${this.ieeeAddr} (${(error as Error).message})`, NS);
548
+ }
549
+ }
449
550
  }
450
551
  }
451
552
  break;
@@ -454,38 +555,28 @@ export class Device extends Entity<ControllerEventMap> {
454
555
  }
455
556
 
456
557
  // Send a default response if necessary.
457
- const isDefaultResponse = frame.header.isGlobal && frame.command.name === "defaultRsp";
458
- const commandHasResponse = frame.command.response !== undefined;
459
- const disableDefaultResponse = frame.header.frameControl.disableDefaultResponse;
460
558
  /* v8 ignore next */
461
559
  const disableTuyaDefaultResponse = this.manufacturerName?.startsWith("_TZ") && process.env.DISABLE_TUYA_DEFAULT_RESPONSE;
462
560
  // Sometimes messages are received twice, prevent responding twice
463
- const alreadyResponded = this._lastDefaultResponseSequenceNumber === frame.header.transactionSequenceNumber;
561
+ const alreadyResponded = this._lastDefaultResponseSequenceNumber === header.transactionSequenceNumber;
464
562
 
465
563
  if (
466
- this.type !== "GreenPower" &&
467
- !dataPayload.wasBroadcast &&
468
- !disableDefaultResponse &&
469
- !isDefaultResponse &&
470
- !commandHasResponse &&
471
564
  !this._skipDefaultResponse &&
565
+ sendDefaultResponse &&
566
+ (!header.frameControl.disableDefaultResponse || defaultResponseStatus !== Zcl.Status.SUCCESS) &&
472
567
  !alreadyResponded &&
473
568
  !disableTuyaDefaultResponse
474
569
  ) {
475
570
  try {
476
- this._lastDefaultResponseSequenceNumber = frame.header.transactionSequenceNumber;
477
- // In the ZCL it is not documented what the direction of the default response should be
478
- // In https://github.com/Koenkk/zigbee2mqtt/issues/18096 a commandResponse (SERVER_TO_CLIENT)
479
- // is send and the device expects a CLIENT_TO_SERVER back.
480
- // Previously SERVER_TO_CLIENT was always used.
481
- // Therefore for non-global commands we inverse the direction.
482
- const direction = frame.header.isGlobal
483
- ? Zcl.Direction.SERVER_TO_CLIENT
484
- : frame.header.frameControl.direction === Zcl.Direction.CLIENT_TO_SERVER
485
- ? Zcl.Direction.SERVER_TO_CLIENT
486
- : Zcl.Direction.CLIENT_TO_SERVER;
487
-
488
- await endpoint.defaultResponse(frame.command.ID, 0, frame.cluster.ID, frame.header.transactionSequenceNumber, {direction});
571
+ this._lastDefaultResponseSequenceNumber = header.transactionSequenceNumber;
572
+ const direction =
573
+ header.frameControl.direction === Zcl.Direction.CLIENT_TO_SERVER
574
+ ? Zcl.Direction.SERVER_TO_CLIENT
575
+ : Zcl.Direction.CLIENT_TO_SERVER;
576
+
577
+ await endpoint.defaultResponse(command.ID, defaultResponseStatus, cluster.ID, header.transactionSequenceNumber, {
578
+ direction,
579
+ });
489
580
  } catch (error) {
490
581
  logger.debug(`Default response to ${this.ieeeAddr} failed (${error})`, NS);
491
582
  }
@@ -501,7 +592,6 @@ export class Device extends Entity<ControllerEventMap> {
501
592
  */
502
593
  public static resetCache(): void {
503
594
  Device.devices.clear();
504
- Device.loadedFromDatabase = false;
505
595
  Device.deletedDevices.clear();
506
596
  Device.nwkToIeeeCache.clear();
507
597
  }
@@ -523,7 +613,7 @@ export class Device extends Entity<ControllerEventMap> {
523
613
 
524
614
  // default: no timeout (messages expire immediately after first send attempt)
525
615
  let pendingRequestTimeout = 0;
526
- if (endpoints.filter((e): boolean => e.inputClusters.includes(Zcl.Clusters.genPollCtrl.ID)).length > 0) {
616
+ if (endpoints.filter((e): boolean => e.inputClusters.includes(GEN_POLL_CTRL_CLUSTER_ID)).length > 0) {
527
617
  // default for devices that support genPollCtrl cluster (RX off when idle): 1 day
528
618
  pendingRequestTimeout = 86400000;
529
619
  }
@@ -608,23 +698,19 @@ export class Device extends Entity<ControllerEventMap> {
608
698
  }
609
699
 
610
700
  private static loadFromDatabaseIfNecessary(): void {
611
- if (!Device.loadedFromDatabase) {
612
- Entity.databases.forEach(database => {
613
- if (!Device.devices.has(database.id)) {
614
- Device.devices.set(database.id, new Map<string, Device>());
615
- Device.deletedDevices.set(database.id, new Map<string, Device>());
616
- Device.nwkToIeeeCache.set(database.id, new Map<number, string>());
617
- const entries = database.getEntriesIterator(['Coordinator', 'EndDevice', 'Router', 'GreenPower', 'Unknown']);
618
- for (const entry of entries) {
619
- const device = Device.fromDatabaseEntry(entry, database.id);
620
- Device.devices.get(database.id)!.set(device.ieeeAddr, device);
621
- Device.nwkToIeeeCache.get(database.id)!.set(device.networkAddress, device.ieeeAddr);
622
- }
701
+ Entity.databases.forEach(database => {
702
+ if (!Device.devices.has(database.id)) {
703
+ Device.devices.set(database.id, new Map<string, Device>());
704
+ Device.deletedDevices.set(database.id, new Map<string, Device>());
705
+ Device.nwkToIeeeCache.set(database.id, new Map<number, string>());
706
+ const entries = database.getEntriesIterator(['Coordinator', 'EndDevice', 'Router', 'GreenPower', 'Unknown']);
707
+ for (const entry of entries) {
708
+ const device = Device.fromDatabaseEntry(entry, database.id);
709
+ Device.devices.get(database.id)!.set(device.ieeeAddr, device);
710
+ Device.nwkToIeeeCache.get(database.id)!.set(device.networkAddress, device.ieeeAddr);
623
711
  }
624
- });
625
-
626
- Device.loadedFromDatabase = true;
627
- }
712
+ }
713
+ });
628
714
  }
629
715
 
630
716
  public static find(databaseID: number, ieeeOrNwkAddress: string | number, includeDeleted = false): Device | undefined {
@@ -1367,37 +1453,42 @@ export class Device extends Entity<ControllerEventMap> {
1367
1453
  // Zigbee does not have an official pinging mechanism. Use a read request
1368
1454
  // of a mandatory basic cluster attribute to keep it as lightweight as
1369
1455
  // possible.
1370
- const endpoint = this.endpoints.find((ep) => ep.inputClusters.includes(0)) ?? this.endpoints[0];
1456
+ const endpoint = this.endpoints.find((ep) => ep.inputClusters.includes(GEN_BASIC_CLUSTER_ID)) ?? this.endpoints[0];
1371
1457
  await endpoint.read("genBasic", ["zclVersion"], {disableRecovery, sendPolicy: "immediate"});
1372
1458
  }
1373
1459
 
1374
- public addCustomCluster(name: string, cluster: ClusterDefinition): void {
1460
+ public addCustomCluster(name: string, cluster: Cluster): void {
1375
1461
  assert(
1376
- ![Zcl.Clusters.touchlink.ID, Zcl.Clusters.greenPower.ID].includes(cluster.ID),
1462
+ cluster.ID !== Zcl.Clusters.touchlink.ID && cluster.ID !== Zcl.Clusters.greenPower.ID,
1377
1463
  "Overriding of greenPower or touchlink cluster is not supported",
1378
1464
  );
1379
- if (Zcl.Utils.isClusterName(name)) {
1380
- const existingCluster = this._customClusters[name] ?? Zcl.Clusters[name];
1381
1465
 
1466
+ if (Zcl.Utils.isClusterName(name)) {
1382
1467
  // Extend existing cluster
1468
+ const existingCluster = this._customClusters[name] ?? Zcl.Clusters[name];
1383
1469
  assert(existingCluster.ID === cluster.ID, `Custom cluster ID (${cluster.ID}) should match existing cluster ID (${existingCluster.ID})`);
1384
- cluster = {
1470
+
1471
+ const extendedCluster: Cluster = {
1472
+ name: cluster.name,
1385
1473
  ID: cluster.ID,
1386
1474
  manufacturerCode: cluster.manufacturerCode,
1387
1475
  attributes: {...existingCluster.attributes, ...cluster.attributes},
1388
1476
  commands: {...existingCluster.commands, ...cluster.commands},
1389
1477
  commandsResponse: {...existingCluster.commandsResponse, ...cluster.commandsResponse},
1390
1478
  };
1479
+
1480
+ this._customClusters[name] = extendedCluster;
1481
+ } else {
1482
+ this._customClusters[name] = cluster;
1391
1483
  }
1392
- this._customClusters[name] = cluster;
1393
1484
  }
1394
1485
 
1395
1486
  #waitForOtaCommand<Co extends string>(
1396
1487
  endpointId: number,
1397
1488
  commandId: number,
1398
- transactionSequenceNumber: number | undefined,
1489
+ defaultRspCommandId: number | undefined,
1399
1490
  timeout: number,
1400
- ): {promise: Promise<TZclFrame<"genOta", Co>>; cancel: () => void} {
1491
+ ): {promise: Promise<TZclFrame<"genOta", Co> | TFoundationZclFrame<"defaultRsp">>; cancel: () => void} {
1401
1492
  const adapter = Entity.getAdapterByID(this.databaseID);
1402
1493
  if (!adapter) {
1403
1494
  throw new Error(`No adapter found for database ID ${this.databaseID}`);
@@ -1407,18 +1498,19 @@ export class Device extends Entity<ControllerEventMap> {
1407
1498
  endpointId,
1408
1499
  Zcl.FrameType.SPECIFIC,
1409
1500
  Zcl.Direction.CLIENT_TO_SERVER,
1410
- transactionSequenceNumber,
1411
- Zcl.Clusters.genOta.ID,
1501
+ undefined,
1502
+ GEN_OTA_CLUSTER_ID,
1412
1503
  commandId,
1504
+ defaultRspCommandId,
1413
1505
  timeout,
1414
1506
  );
1415
- const promise = new Promise<Zcl.Frame & {payload: TClusterPayload<"genOta", Co>}>((resolve, reject) => {
1507
+ const promise = new Promise<TZclFrame<"genOta", Co> | TFoundationZclFrame<"defaultRsp">>((resolve, reject) => {
1416
1508
  waiter.promise.then(
1417
1509
  (payload) => {
1418
1510
  try {
1419
1511
  const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, this.customClusters);
1420
1512
 
1421
- resolve(frame);
1513
+ resolve(frame as TZclFrame<"genOta", Co> | TFoundationZclFrame<"defaultRsp">);
1422
1514
  } catch (error) {
1423
1515
  reject(error);
1424
1516
  }
@@ -1470,7 +1562,7 @@ export class Device extends Entity<ControllerEventMap> {
1470
1562
  const queryNextImageRequest = this.#waitForOtaCommand<"queryNextImageRequest">(
1471
1563
  endpoint.ID,
1472
1564
  Zcl.Clusters.genOta.commands.queryNextImageRequest.ID,
1473
- undefined,
1565
+ Zcl.Clusters.genOta.commandsResponse.imageNotify.ID,
1474
1566
  60000,
1475
1567
  );
1476
1568
 
@@ -1479,7 +1571,9 @@ export class Device extends Entity<ControllerEventMap> {
1479
1571
 
1480
1572
  const response = await queryNextImageRequest.promise;
1481
1573
 
1482
- return [response.payload, response.header.transactionSequenceNumber];
1574
+ assert(response.header.isSpecific);
1575
+
1576
+ return [(response as TZclFrame<"genOta", "queryNextImageRequest">).payload, response.header.transactionSequenceNumber];
1483
1577
  } catch {
1484
1578
  queryNextImageRequest.cancel();
1485
1579
 
@@ -1672,9 +1766,15 @@ export class Device extends Entity<ControllerEventMap> {
1672
1766
  let endResult: TZclFrame<"genOta", "upgradeEndRequest">;
1673
1767
 
1674
1768
  try {
1675
- endResult = await session.run();
1769
+ this.#otaAbortController = new AbortController();
1770
+ const runEnd = await session.run(this.#otaAbortController.signal);
1771
+
1772
+ assert(runEnd.header.isSpecific);
1773
+
1774
+ endResult = runEnd as TZclFrame<"genOta", "upgradeEndRequest">;
1676
1775
  } finally {
1677
1776
  this.#otaInProgress = false;
1777
+ this.#otaAbortController = undefined;
1678
1778
  }
1679
1779
 
1680
1780
  logger.debug(() => `Received upgrade end request for ${this.ieeeAddr}: ${JSON.stringify(endResult.payload)}`, NS);
@@ -1751,7 +1851,7 @@ export class Device extends Entity<ControllerEventMap> {
1751
1851
  await endpoint.defaultResponse(
1752
1852
  Zcl.Clusters.genOta.commands.upgradeEndRequest.ID,
1753
1853
  Zcl.Status.SUCCESS,
1754
- Zcl.Clusters.genOta.ID,
1854
+ GEN_OTA_CLUSTER_ID,
1755
1855
  endResult.header.transactionSequenceNumber,
1756
1856
  );
1757
1857
  } catch (error) {
@@ -1764,6 +1864,13 @@ export class Device extends Entity<ControllerEventMap> {
1764
1864
  }
1765
1865
  }
1766
1866
 
1867
+ /**
1868
+ * Abort running OTA if any. Send `ABORT` with next block response to device.
1869
+ */
1870
+ abortOta(): void {
1871
+ this.#otaAbortController?.abort();
1872
+ }
1873
+
1767
1874
  scheduleOta(source: OtaSource): void {
1768
1875
  assert(
1769
1876
  this.endpoints.some((e) => e.supportsOutputCluster("genOta")),