@willieee802/zigbee-herdsman 0.49.4 → 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 (209) 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 -3
  95. package/dist/controller/model/device.d.ts.map +1 -1
  96. package/dist/controller/model/device.js +155 -68
  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.js +4 -4
  103. package/dist/controller/model/group.js.map +1 -1
  104. package/dist/controller/touchlink.js +3 -3
  105. package/dist/controller/touchlink.js.map +1 -1
  106. package/dist/utils/timeService.js +2 -2
  107. package/dist/utils/timeService.js.map +1 -1
  108. package/dist/zspec/zcl/buffaloZcl.d.ts +3 -3
  109. package/dist/zspec/zcl/buffaloZcl.d.ts.map +1 -1
  110. package/dist/zspec/zcl/buffaloZcl.js +198 -96
  111. package/dist/zspec/zcl/buffaloZcl.js.map +1 -1
  112. package/dist/zspec/zcl/definition/cluster.d.ts +2 -2
  113. package/dist/zspec/zcl/definition/cluster.d.ts.map +1 -1
  114. package/dist/zspec/zcl/definition/cluster.js +2699 -2808
  115. package/dist/zspec/zcl/definition/cluster.js.map +1 -1
  116. package/dist/zspec/zcl/definition/clusters-types.d.ts +63 -1109
  117. package/dist/zspec/zcl/definition/clusters-types.d.ts.map +1 -1
  118. package/dist/zspec/zcl/definition/enums.d.ts +0 -1
  119. package/dist/zspec/zcl/definition/enums.d.ts.map +1 -1
  120. package/dist/zspec/zcl/definition/enums.js +0 -1
  121. package/dist/zspec/zcl/definition/enums.js.map +1 -1
  122. package/dist/zspec/zcl/definition/foundation.d.ts +306 -7
  123. package/dist/zspec/zcl/definition/foundation.d.ts.map +1 -1
  124. package/dist/zspec/zcl/definition/foundation.js +552 -207
  125. package/dist/zspec/zcl/definition/foundation.js.map +1 -1
  126. package/dist/zspec/zcl/definition/status.d.ts +21 -10
  127. package/dist/zspec/zcl/definition/status.d.ts.map +1 -1
  128. package/dist/zspec/zcl/definition/status.js +11 -0
  129. package/dist/zspec/zcl/definition/status.js.map +1 -1
  130. package/dist/zspec/zcl/definition/tstype.d.ts +57 -48
  131. package/dist/zspec/zcl/definition/tstype.d.ts.map +1 -1
  132. package/dist/zspec/zcl/utils.d.ts +7 -4
  133. package/dist/zspec/zcl/utils.d.ts.map +1 -1
  134. package/dist/zspec/zcl/utils.js +133 -240
  135. package/dist/zspec/zcl/utils.js.map +1 -1
  136. package/dist/zspec/zcl/zclFrame.d.ts +4 -4
  137. package/dist/zspec/zcl/zclFrame.d.ts.map +1 -1
  138. package/dist/zspec/zcl/zclFrame.js +19 -103
  139. package/dist/zspec/zcl/zclFrame.js.map +1 -1
  140. package/dist/zspec/zcl/zclStatusError.d.ts +1 -1
  141. package/dist/zspec/zcl/zclStatusError.d.ts.map +1 -1
  142. package/dist/zspec/zcl/zclStatusError.js +2 -2
  143. package/dist/zspec/zcl/zclStatusError.js.map +1 -1
  144. package/package.json +1 -1
  145. package/scripts/clusters-typegen.ts +44 -139
  146. package/src/adapter/adapter.ts +38 -3
  147. package/src/adapter/adapterDiscovery.ts +2 -1
  148. package/src/adapter/deconz/adapter/deconzAdapter.ts +24 -51
  149. package/src/adapter/deconz/driver/constants.ts +1 -1
  150. package/src/adapter/ember/adapter/emberAdapter.ts +23 -10
  151. package/src/adapter/ember/adapter/oneWaitress.ts +16 -6
  152. package/src/adapter/ezsp/adapter/ezspAdapter.ts +27 -48
  153. package/src/adapter/ezsp/driver/index.ts +1 -1
  154. package/src/adapter/ezsp/driver/types/index.ts +99 -99
  155. package/src/adapter/serialPort.ts +9 -0
  156. package/src/adapter/z-stack/adapter/adapter-backup.ts +1 -1
  157. package/src/adapter/z-stack/adapter/adapter-nv-memory.ts +1 -1
  158. package/src/adapter/z-stack/adapter/manager.ts +16 -2
  159. package/src/adapter/z-stack/adapter/tstype.ts +1 -0
  160. package/src/adapter/z-stack/adapter/zStackAdapter.ts +34 -81
  161. package/src/adapter/z-stack/constants/index.ts +1 -1
  162. package/src/adapter/z-stack/unpi/constants.ts +1 -1
  163. package/src/adapter/zboss/adapter/zbossAdapter.ts +23 -54
  164. package/src/adapter/zboss/driver.ts +8 -1
  165. package/src/adapter/zboss/uart.ts +14 -1
  166. package/src/adapter/zigate/adapter/zigateAdapter.ts +17 -48
  167. package/src/adapter/zoh/adapter/zohAdapter.ts +27 -50
  168. package/src/controller/controller.ts +12 -2
  169. package/src/controller/greenPower.ts +16 -9
  170. package/src/controller/helpers/ota.ts +37 -11
  171. package/src/controller/helpers/zclFrameConverter.ts +20 -17
  172. package/src/controller/model/device.ts +192 -79
  173. package/src/controller/model/endpoint.ts +36 -24
  174. package/src/controller/model/group.ts +4 -4
  175. package/src/controller/touchlink.ts +3 -3
  176. package/src/utils/timeService.ts +2 -2
  177. package/src/zspec/zcl/buffaloZcl.ts +226 -100
  178. package/src/zspec/zcl/definition/cluster.ts +2713 -2822
  179. package/src/zspec/zcl/definition/clusters-types.ts +80 -1135
  180. package/src/zspec/zcl/definition/enums.ts +0 -1
  181. package/src/zspec/zcl/definition/foundation.ts +703 -216
  182. package/src/zspec/zcl/definition/status.ts +22 -11
  183. package/src/zspec/zcl/definition/tstype.ts +59 -58
  184. package/src/zspec/zcl/utils.ts +137 -264
  185. package/src/zspec/zcl/zclFrame.ts +25 -130
  186. package/src/zspec/zcl/zclStatusError.ts +2 -2
  187. package/test/adapter/ember/emberAdapter.test.ts +191 -4
  188. package/test/adapter/ezsp/uart.test.ts +10 -10
  189. package/test/adapter/z-stack/adapter.test.ts +88 -32
  190. package/test/adapter/zoh/zohAdapter.test.ts +4 -4
  191. package/test/controller.test.ts +822 -248
  192. package/test/device-ota.test.ts +141 -16
  193. package/test/device.test.ts +731 -0
  194. package/test/requests.bench.ts +2 -0
  195. package/test/zcl.test.ts +70 -95
  196. package/test/zspec/zcl/buffalo.test.ts +251 -11
  197. package/test/zspec/zcl/foundation.test.ts +990 -0
  198. package/test/zspec/zcl/frame.test.ts +84 -69
  199. package/test/zspec/zcl/utils.test.ts +105 -81
  200. package/tsconfig.json +0 -1
  201. package/scripts/check-clusters-changes.ts +0 -328
  202. package/scripts/clusters-changes.log +0 -584
  203. package/scripts/utils.ts +0 -88
  204. package/scripts/zap-update-clusters-report.json +0 -303
  205. package/scripts/zap-update-clusters.ts +0 -1520
  206. package/scripts/zap-update-types.ts +0 -707
  207. package/scripts/zap-xml-clusters-overrides-data.ts +0 -52
  208. package/scripts/zap-xml-clusters-overrides.ts +0 -400
  209. 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
  }
@@ -290,6 +297,21 @@ export class Device extends Entity<ControllerEventMap> {
290
297
  this.#scheduledOta = scheduledOta;
291
298
  }
292
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
+
293
315
  public createEndpoint(id: number): Endpoint {
294
316
  if (this.getEndpoint(id)) {
295
317
  throw new Error(`Device '${this.ieeeAddr}' already has an endpoint '${id}'`);
@@ -348,72 +370,141 @@ export class Device extends Entity<ControllerEventMap> {
348
370
  return this.endpoints.find((e) => e.hasPendingRequests()) !== undefined;
349
371
  }
350
372
 
351
- 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> {
352
379
  if (!Device.devices.get(this.databaseID)?.has(this.ieeeAddr)) {
353
380
  // prevent race conditions where device gets deleted during processing
354
381
  return;
355
382
  }
356
383
 
357
- if (frame.header.isGlobal) {
358
- // Response to read requests
359
- if (frame.command.name === "read" && !this._customReadResponse?.(frame, endpoint)) {
360
- const attributes: {[s: string]: KeyValue} = {
361
- ...endpoint.clusters,
362
- };
384
+ if (this.type === "GreenPower") {
385
+ // nothing below applies
386
+ return;
387
+ }
363
388
 
364
- const isTimeReadRequest = dataPayload.clusterID === Zcl.Clusters.genTime.ID;
365
- if (isTimeReadRequest) {
366
- attributes.genTime = {
367
- attributes: timeService.getTimeClusterAttributes(),
368
- };
369
- }
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
+ }
370
402
 
371
- if (frame.cluster.name in attributes) {
372
403
  const response: KeyValue = {};
373
404
 
374
- for (const entry of frame.payload) {
375
- 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;
376
413
 
377
- if (name && name in attributes[frame.cluster.name].attributes) {
378
- 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;
379
446
  }
380
447
  }
381
448
 
382
449
  try {
383
- await endpoint.readResponse(frame.cluster.ID, frame.header.transactionSequenceNumber, response, {
450
+ await endpoint.readResponse(cluster.ID, header.transactionSequenceNumber, response, {
384
451
  srcEndpoint: dataPayload.destinationEndpoint,
385
452
  });
386
453
  } catch (error) {
387
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)
388
457
  }
458
+
459
+ break;
460
+ }
461
+ case "defaultRsp": {
462
+ sendDefaultResponse = false; // per spec
463
+ break;
389
464
  }
390
465
  }
391
- } else if (frame.header.isSpecific) {
392
- switch (frame.cluster.name) {
466
+ } else if (header.isSpecific) {
467
+ switch (cluster.name) {
393
468
  case "ssIasZone": {
394
- if (frame.command.name === "enrollReq") {
469
+ if (command.name === "enrollReq") {
395
470
  // Respond to enroll requests
396
471
  logger.debug(`IAS - '${this.ieeeAddr}' responding to enroll response`, NS);
397
472
 
398
- 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
+ }
399
486
  }
400
487
  break;
401
488
  }
402
489
  case "genPollCtrl": {
403
- if (frame.command.name === "checkin") {
490
+ if (command.name === "checkin") {
491
+ let startedFastPolling = false;
492
+
404
493
  // Handle check-in from sleeping end devices
405
494
  try {
406
495
  if (this.hasPendingRequests() || this._checkinInterval === undefined) {
407
496
  logger.debug(`check-in from ${this.ieeeAddr}: accepting fast-poll`, NS);
408
497
  await endpoint.command(
409
- frame.cluster.name as "genPollCtrl",
498
+ cluster.name as "genPollCtrl",
410
499
  "checkinRsp",
500
+ {startFastPolling: 1, fastPollTimeout: 0},
411
501
  {
412
- startFastPolling: 1,
413
- fastPollTimeout: 0,
502
+ transactionSequenceNumber: header.transactionSequenceNumber,
503
+ disableDefaultResponse: true,
504
+ sendPolicy: "immediate",
414
505
  },
415
- {sendPolicy: "immediate"},
416
506
  );
507
+ startedFastPolling = true;
417
508
 
418
509
  // This is a good time to read the checkin interval if we haven't stored it previously
419
510
  if (this._checkinInterval === undefined) {
@@ -427,24 +518,35 @@ export class Device extends Entity<ControllerEventMap> {
427
518
  }
428
519
 
429
520
  await Promise.all(this.endpoints.map(async (e) => await e.sendPendingRequests(true)));
430
- // We *must* end fast-poll when we're done sending things. Otherwise
431
- // we cause undue power-drain.
432
- logger.debug(`check-in from ${this.ieeeAddr}: stopping fast-poll`, NS);
433
- await endpoint.command(frame.cluster.name as "genPollCtrl", "fastPollStop", {}, {sendPolicy: "immediate"});
434
521
  } else {
435
522
  logger.debug(`check-in from ${this.ieeeAddr}: declining fast-poll`, NS);
436
523
  await endpoint.command(
437
- frame.cluster.name as "genPollCtrl",
524
+ cluster.name as "genPollCtrl",
438
525
  "checkinRsp",
526
+ {startFastPolling: 0, fastPollTimeout: 0},
439
527
  {
440
- startFastPolling: 0,
441
- fastPollTimeout: 0,
528
+ transactionSequenceNumber: header.transactionSequenceNumber,
529
+ disableDefaultResponse: true,
530
+ sendPolicy: "immediate",
442
531
  },
443
- {sendPolicy: "immediate"},
444
532
  );
445
533
  }
534
+
535
+ sendDefaultResponse = false; // per spec, sending a specific response
446
536
  } catch (error) {
447
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
+ }
448
550
  }
449
551
  }
450
552
  break;
@@ -453,38 +555,28 @@ export class Device extends Entity<ControllerEventMap> {
453
555
  }
454
556
 
455
557
  // Send a default response if necessary.
456
- const isDefaultResponse = frame.header.isGlobal && frame.command.name === "defaultRsp";
457
- const commandHasResponse = frame.command.response !== undefined;
458
- const disableDefaultResponse = frame.header.frameControl.disableDefaultResponse;
459
558
  /* v8 ignore next */
460
559
  const disableTuyaDefaultResponse = this.manufacturerName?.startsWith("_TZ") && process.env.DISABLE_TUYA_DEFAULT_RESPONSE;
461
560
  // Sometimes messages are received twice, prevent responding twice
462
- const alreadyResponded = this._lastDefaultResponseSequenceNumber === frame.header.transactionSequenceNumber;
561
+ const alreadyResponded = this._lastDefaultResponseSequenceNumber === header.transactionSequenceNumber;
463
562
 
464
563
  if (
465
- this.type !== "GreenPower" &&
466
- !dataPayload.wasBroadcast &&
467
- !disableDefaultResponse &&
468
- !isDefaultResponse &&
469
- !commandHasResponse &&
470
564
  !this._skipDefaultResponse &&
565
+ sendDefaultResponse &&
566
+ (!header.frameControl.disableDefaultResponse || defaultResponseStatus !== Zcl.Status.SUCCESS) &&
471
567
  !alreadyResponded &&
472
568
  !disableTuyaDefaultResponse
473
569
  ) {
474
570
  try {
475
- this._lastDefaultResponseSequenceNumber = frame.header.transactionSequenceNumber;
476
- // In the ZCL it is not documented what the direction of the default response should be
477
- // In https://github.com/Koenkk/zigbee2mqtt/issues/18096 a commandResponse (SERVER_TO_CLIENT)
478
- // is send and the device expects a CLIENT_TO_SERVER back.
479
- // Previously SERVER_TO_CLIENT was always used.
480
- // Therefore for non-global commands we inverse the direction.
481
- const direction = frame.header.isGlobal
482
- ? Zcl.Direction.SERVER_TO_CLIENT
483
- : frame.header.frameControl.direction === Zcl.Direction.CLIENT_TO_SERVER
484
- ? Zcl.Direction.SERVER_TO_CLIENT
485
- : Zcl.Direction.CLIENT_TO_SERVER;
486
-
487
- 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
+ });
488
580
  } catch (error) {
489
581
  logger.debug(`Default response to ${this.ieeeAddr} failed (${error})`, NS);
490
582
  }
@@ -521,7 +613,7 @@ export class Device extends Entity<ControllerEventMap> {
521
613
 
522
614
  // default: no timeout (messages expire immediately after first send attempt)
523
615
  let pendingRequestTimeout = 0;
524
- 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) {
525
617
  // default for devices that support genPollCtrl cluster (RX off when idle): 1 day
526
618
  pendingRequestTimeout = 86400000;
527
619
  }
@@ -1361,37 +1453,42 @@ export class Device extends Entity<ControllerEventMap> {
1361
1453
  // Zigbee does not have an official pinging mechanism. Use a read request
1362
1454
  // of a mandatory basic cluster attribute to keep it as lightweight as
1363
1455
  // possible.
1364
- 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];
1365
1457
  await endpoint.read("genBasic", ["zclVersion"], {disableRecovery, sendPolicy: "immediate"});
1366
1458
  }
1367
1459
 
1368
- public addCustomCluster(name: string, cluster: ClusterDefinition): void {
1460
+ public addCustomCluster(name: string, cluster: Cluster): void {
1369
1461
  assert(
1370
- ![Zcl.Clusters.touchlink.ID, Zcl.Clusters.greenPower.ID].includes(cluster.ID),
1462
+ cluster.ID !== Zcl.Clusters.touchlink.ID && cluster.ID !== Zcl.Clusters.greenPower.ID,
1371
1463
  "Overriding of greenPower or touchlink cluster is not supported",
1372
1464
  );
1373
- if (Zcl.Utils.isClusterName(name)) {
1374
- const existingCluster = this._customClusters[name] ?? Zcl.Clusters[name];
1375
1465
 
1466
+ if (Zcl.Utils.isClusterName(name)) {
1376
1467
  // Extend existing cluster
1468
+ const existingCluster = this._customClusters[name] ?? Zcl.Clusters[name];
1377
1469
  assert(existingCluster.ID === cluster.ID, `Custom cluster ID (${cluster.ID}) should match existing cluster ID (${existingCluster.ID})`);
1378
- cluster = {
1470
+
1471
+ const extendedCluster: Cluster = {
1472
+ name: cluster.name,
1379
1473
  ID: cluster.ID,
1380
1474
  manufacturerCode: cluster.manufacturerCode,
1381
1475
  attributes: {...existingCluster.attributes, ...cluster.attributes},
1382
1476
  commands: {...existingCluster.commands, ...cluster.commands},
1383
1477
  commandsResponse: {...existingCluster.commandsResponse, ...cluster.commandsResponse},
1384
1478
  };
1479
+
1480
+ this._customClusters[name] = extendedCluster;
1481
+ } else {
1482
+ this._customClusters[name] = cluster;
1385
1483
  }
1386
- this._customClusters[name] = cluster;
1387
1484
  }
1388
1485
 
1389
1486
  #waitForOtaCommand<Co extends string>(
1390
1487
  endpointId: number,
1391
1488
  commandId: number,
1392
- transactionSequenceNumber: number | undefined,
1489
+ defaultRspCommandId: number | undefined,
1393
1490
  timeout: number,
1394
- ): {promise: Promise<TZclFrame<"genOta", Co>>; cancel: () => void} {
1491
+ ): {promise: Promise<TZclFrame<"genOta", Co> | TFoundationZclFrame<"defaultRsp">>; cancel: () => void} {
1395
1492
  const adapter = Entity.getAdapterByID(this.databaseID);
1396
1493
  if (!adapter) {
1397
1494
  throw new Error(`No adapter found for database ID ${this.databaseID}`);
@@ -1401,18 +1498,19 @@ export class Device extends Entity<ControllerEventMap> {
1401
1498
  endpointId,
1402
1499
  Zcl.FrameType.SPECIFIC,
1403
1500
  Zcl.Direction.CLIENT_TO_SERVER,
1404
- transactionSequenceNumber,
1405
- Zcl.Clusters.genOta.ID,
1501
+ undefined,
1502
+ GEN_OTA_CLUSTER_ID,
1406
1503
  commandId,
1504
+ defaultRspCommandId,
1407
1505
  timeout,
1408
1506
  );
1409
- const promise = new Promise<Zcl.Frame & {payload: TClusterPayload<"genOta", Co>}>((resolve, reject) => {
1507
+ const promise = new Promise<TZclFrame<"genOta", Co> | TFoundationZclFrame<"defaultRsp">>((resolve, reject) => {
1410
1508
  waiter.promise.then(
1411
1509
  (payload) => {
1412
1510
  try {
1413
1511
  const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, this.customClusters);
1414
1512
 
1415
- resolve(frame);
1513
+ resolve(frame as TZclFrame<"genOta", Co> | TFoundationZclFrame<"defaultRsp">);
1416
1514
  } catch (error) {
1417
1515
  reject(error);
1418
1516
  }
@@ -1464,7 +1562,7 @@ export class Device extends Entity<ControllerEventMap> {
1464
1562
  const queryNextImageRequest = this.#waitForOtaCommand<"queryNextImageRequest">(
1465
1563
  endpoint.ID,
1466
1564
  Zcl.Clusters.genOta.commands.queryNextImageRequest.ID,
1467
- undefined,
1565
+ Zcl.Clusters.genOta.commandsResponse.imageNotify.ID,
1468
1566
  60000,
1469
1567
  );
1470
1568
 
@@ -1473,7 +1571,9 @@ export class Device extends Entity<ControllerEventMap> {
1473
1571
 
1474
1572
  const response = await queryNextImageRequest.promise;
1475
1573
 
1476
- return [response.payload, response.header.transactionSequenceNumber];
1574
+ assert(response.header.isSpecific);
1575
+
1576
+ return [(response as TZclFrame<"genOta", "queryNextImageRequest">).payload, response.header.transactionSequenceNumber];
1477
1577
  } catch {
1478
1578
  queryNextImageRequest.cancel();
1479
1579
 
@@ -1666,9 +1766,15 @@ export class Device extends Entity<ControllerEventMap> {
1666
1766
  let endResult: TZclFrame<"genOta", "upgradeEndRequest">;
1667
1767
 
1668
1768
  try {
1669
- 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">;
1670
1775
  } finally {
1671
1776
  this.#otaInProgress = false;
1777
+ this.#otaAbortController = undefined;
1672
1778
  }
1673
1779
 
1674
1780
  logger.debug(() => `Received upgrade end request for ${this.ieeeAddr}: ${JSON.stringify(endResult.payload)}`, NS);
@@ -1745,7 +1851,7 @@ export class Device extends Entity<ControllerEventMap> {
1745
1851
  await endpoint.defaultResponse(
1746
1852
  Zcl.Clusters.genOta.commands.upgradeEndRequest.ID,
1747
1853
  Zcl.Status.SUCCESS,
1748
- Zcl.Clusters.genOta.ID,
1854
+ GEN_OTA_CLUSTER_ID,
1749
1855
  endResult.header.transactionSequenceNumber,
1750
1856
  );
1751
1857
  } catch (error) {
@@ -1758,6 +1864,13 @@ export class Device extends Entity<ControllerEventMap> {
1758
1864
  }
1759
1865
  }
1760
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
+
1761
1874
  scheduleOta(source: OtaSource): void {
1762
1875
  assert(
1763
1876
  this.endpoints.some((e) => e.supportsOutputCluster("genOta")),