zigbee-clusters 2.9.1 → 2.10.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.
package/AGENTS.md CHANGED
@@ -303,6 +303,7 @@ attr3: { ... },
303
303
  | `id` | Yes | Always 4-digit hex format (0x0000); add decimal comment if > 9 |
304
304
  | `args` | If has params | Object with typed fields |
305
305
  | `response` | If expects response | Has own `id` and `args` |
306
+ | `encodeMissingFieldsBehavior` | Controls the behavior of missing fields when encoding the struct | Set to `skip` if some fields are optional, omit if all fields are mandatory |
306
307
  | `direction` | For server→client | `Cluster.DIRECTION_SERVER_TO_CLIENT` |
307
308
 
308
309
  #### Command Sections
@@ -333,11 +334,54 @@ lockDoor: {
333
334
  },
334
335
  ```
335
336
 
336
- **Server→client commands** (events/notifications) should be evaluated per case:
337
- - Implement if commonly needed (e.g., `operationEventNotification` for door locks)
338
- - Skip obscure or rarely-used notifications unless specifically requested
337
+ **Server→client commands** should only be added as standalone commands when they can be sent **unsolicited** (not just as a response to a request). Examples:
338
+ - Unsolicited notifications (e.g., `operationEventNotification` for door locks) — add as standalone
339
+ - Responses that the server can also send unsolicited (e.g., `upgradeEndResponse` for synchronized upgrades) — add as standalone
340
+ - Responses that are **only** sent in reply to a request (e.g., `queryNextImageResponse`) — do **NOT** add as standalone; they are already covered by the inline `response:` on the request command
339
341
  - These require `direction: Cluster.DIRECTION_SERVER_TO_CLIENT`
340
342
 
343
+ #### Optional fields
344
+ Some commands contain optional fields. If this is the case add `encodeMissingFieldsBehavior: 'skip'` to the command definition.
345
+
346
+ ```javascript
347
+ lockDoor: {
348
+ id: 0x0000,
349
+ encodeMissingFieldsBehavior: 'skip',
350
+ args: { pinCode: ZCLDataTypes.octstr },
351
+ response: {
352
+ id: 0x0000,
353
+ args: { status: ZCLDataTypes.uint8 },
354
+ },
355
+ }
356
+ ```
357
+
358
+ #### Command variants
359
+ Some commands might change their structure based on a status field. If this is the case, add `encodeMissingFieldsBehavior: 'skip'`. And add a comment above each field
360
+ when the field should be present.
361
+
362
+ **IMPORTANT**: This applies to `response:` structs too, not just the top-level command. If a response has variant fields (e.g., different fields depending on a status value), add `encodeMissingFieldsBehavior: 'skip'` to the response and include all variant fields with comments indicating when each group is present.
363
+
364
+ ```javascript
365
+ imageBlockRequest: {
366
+ id: 0x0003,
367
+ args: { ... },
368
+ response: {
369
+ id: 0x0005,
370
+ encodeMissingFieldsBehavior: 'skip',
371
+ args: {
372
+ status: ZCLDataTypes.enum8Status,
373
+ // When status is SUCCESS
374
+ manufacturerCode: ZCLDataTypes.uint16,
375
+ fileOffset: ZCLDataTypes.uint32,
376
+ imageData: ZCLDataTypes.buffer,
377
+ // When status is WAIT_FOR_DATA
378
+ currentTime: ZCLDataTypes.uint32,
379
+ requestTime: ZCLDataTypes.uint32,
380
+ },
381
+ },
382
+ }
383
+ ```
384
+
341
385
  ---
342
386
 
343
387
  ### ZCLDataTypes Reference
package/index.d.ts CHANGED
@@ -31,6 +31,8 @@ type ZCLNodeConstructorInput = {
31
31
  ) => Promise<void>;
32
32
  };
33
33
 
34
+ type ZCLEnum8Status = 'SUCCESS' | 'FAILURE' | 'NOT_AUTHORIZED' | 'RESERVED_FIELD_NOT_ZERO' | 'MALFORMED_COMMAND' | 'UNSUP_CLUSTER_COMMAND' | 'UNSUP_GENERAL_COMMAND' | 'UNSUP_MANUF_CLUSTER_COMMAND' | 'UNSUP_MANUF_GENERAL_COMMAND' | 'INVALID_FIELD' | 'UNSUPPORTED_ATTRIBUTE' | 'INVALID_VALUE' | 'READ_ONLY' | 'INSUFFICIENT_SPACE' | 'DUPLICATE_EXISTS' | 'NOT_FOUND' | 'UNREPORTABLE_ATTRIBUTE' | 'INVALID_DATA_TYPE' | 'INVALID_SELECTOR' | 'WRITE_ONLY' | 'INCONSISTENT_STARTUP_STATE' | 'DEFINED_OUT_OF_BAND' | 'INCONSISTENT' | 'ACTION_DENIED' | 'TIMEOUT' | 'ABORT' | 'INVALID_IMAGE' | 'WAIT_FOR_DATA' | 'NO_IMAGE_AVAILABLE' | 'REQUIRE_MORE_IMAGE' | 'NOTIFICATION_PENDING' | 'HARDWARE_FAILURE' | 'SOFTWARE_FAILURE' | 'CALIBRATION_ERROR' | 'UNSUPPORTED_CLUSTER';
35
+
34
36
  export interface ZCLNodeCluster extends EventEmitter {
35
37
  discoverCommandsGenerated(params?: {
36
38
  startValue?: number;
@@ -449,10 +451,10 @@ export interface GroupsCluster extends ZCLNodeCluster {
449
451
  writeAttributes(attributes: Partial<GroupsClusterAttributes>, opts?: { timeout?: number }): Promise<unknown>;
450
452
  on<K extends keyof GroupsClusterAttributes & string>(eventName: `attr.${K}`, listener: (value: GroupsClusterAttributes[K]) => void): this;
451
453
  once<K extends keyof GroupsClusterAttributes & string>(eventName: `attr.${K}`, listener: (value: GroupsClusterAttributes[K]) => void): this;
452
- addGroup(args: { groupId: number; groupName: string }, opts?: ClusterCommandOptions): Promise<{ status: 'SUCCESS' | 'FAILURE' | 'NOT_AUTHORIZED' | 'RESERVED_FIELD_NOT_ZERO' | 'MALFORMED_COMMAND' | 'UNSUP_CLUSTER_COMMAND' | 'UNSUP_GENERAL_COMMAND' | 'UNSUP_MANUF_CLUSTER_COMMAND' | 'UNSUP_MANUF_GENERAL_COMMAND' | 'INVALID_FIELD' | 'UNSUPPORTED_ATTRIBUTE' | 'INVALID_VALUE' | 'READ_ONLY' | 'INSUFFICIENT_SPACE' | 'DUPLICATE_EXISTS' | 'NOT_FOUND' | 'UNREPORTABLE_ATTRIBUTE' | 'INVALID_DATA_TYPE' | 'INVALID_SELECTOR' | 'WRITE_ONLY' | 'INCONSISTENT_STARTUP_STATE' | 'DEFINED_OUT_OF_BAND' | 'INCONSISTENT' | 'ACTION_DENIED' | 'TIMEOUT' | 'ABORT' | 'INVALID_IMAGE' | 'WAIT_FOR_DATA' | 'NO_IMAGE_AVAILABLE' | 'REQUIRE_MORE_IMAGE' | 'NOTIFICATION_PENDING' | 'HARDWARE_FAILURE' | 'SOFTWARE_FAILURE' | 'CALIBRATION_ERROR' | 'UNSUPPORTED_CLUSTER'; groupId: number }>;
453
- viewGroup(args: { groupId: number }, opts?: ClusterCommandOptions): Promise<{ status: 'SUCCESS' | 'FAILURE' | 'NOT_AUTHORIZED' | 'RESERVED_FIELD_NOT_ZERO' | 'MALFORMED_COMMAND' | 'UNSUP_CLUSTER_COMMAND' | 'UNSUP_GENERAL_COMMAND' | 'UNSUP_MANUF_CLUSTER_COMMAND' | 'UNSUP_MANUF_GENERAL_COMMAND' | 'INVALID_FIELD' | 'UNSUPPORTED_ATTRIBUTE' | 'INVALID_VALUE' | 'READ_ONLY' | 'INSUFFICIENT_SPACE' | 'DUPLICATE_EXISTS' | 'NOT_FOUND' | 'UNREPORTABLE_ATTRIBUTE' | 'INVALID_DATA_TYPE' | 'INVALID_SELECTOR' | 'WRITE_ONLY' | 'INCONSISTENT_STARTUP_STATE' | 'DEFINED_OUT_OF_BAND' | 'INCONSISTENT' | 'ACTION_DENIED' | 'TIMEOUT' | 'ABORT' | 'INVALID_IMAGE' | 'WAIT_FOR_DATA' | 'NO_IMAGE_AVAILABLE' | 'REQUIRE_MORE_IMAGE' | 'NOTIFICATION_PENDING' | 'HARDWARE_FAILURE' | 'SOFTWARE_FAILURE' | 'CALIBRATION_ERROR' | 'UNSUPPORTED_CLUSTER'; groupId: number; groupNames: string }>;
454
+ addGroup(args: { groupId: number; groupName: string }, opts?: ClusterCommandOptions): Promise<{ status: ZCLEnum8Status; groupId: number }>;
455
+ viewGroup(args: { groupId: number }, opts?: ClusterCommandOptions): Promise<{ status: ZCLEnum8Status; groupId: number; groupNames: string }>;
454
456
  getGroupMembership(args: { groupIds: number[] }, opts?: ClusterCommandOptions): Promise<{ capacity: number; groups: number[] }>;
455
- removeGroup(args: { groupId: number }, opts?: ClusterCommandOptions): Promise<{ status: 'SUCCESS' | 'FAILURE' | 'NOT_AUTHORIZED' | 'RESERVED_FIELD_NOT_ZERO' | 'MALFORMED_COMMAND' | 'UNSUP_CLUSTER_COMMAND' | 'UNSUP_GENERAL_COMMAND' | 'UNSUP_MANUF_CLUSTER_COMMAND' | 'UNSUP_MANUF_GENERAL_COMMAND' | 'INVALID_FIELD' | 'UNSUPPORTED_ATTRIBUTE' | 'INVALID_VALUE' | 'READ_ONLY' | 'INSUFFICIENT_SPACE' | 'DUPLICATE_EXISTS' | 'NOT_FOUND' | 'UNREPORTABLE_ATTRIBUTE' | 'INVALID_DATA_TYPE' | 'INVALID_SELECTOR' | 'WRITE_ONLY' | 'INCONSISTENT_STARTUP_STATE' | 'DEFINED_OUT_OF_BAND' | 'INCONSISTENT' | 'ACTION_DENIED' | 'TIMEOUT' | 'ABORT' | 'INVALID_IMAGE' | 'WAIT_FOR_DATA' | 'NO_IMAGE_AVAILABLE' | 'REQUIRE_MORE_IMAGE' | 'NOTIFICATION_PENDING' | 'HARDWARE_FAILURE' | 'SOFTWARE_FAILURE' | 'CALIBRATION_ERROR' | 'UNSUPPORTED_CLUSTER'; groupId: number }>;
457
+ removeGroup(args: { groupId: number }, opts?: ClusterCommandOptions): Promise<{ status: ZCLEnum8Status; groupId: number }>;
456
458
  removeAllGroups(opts?: ClusterCommandOptions): Promise<void>;
457
459
  addGroupIfIdentify(args: { groupId: number; groupName: string }, opts?: ClusterCommandOptions): Promise<void>;
458
460
  }
@@ -848,7 +850,36 @@ export interface OnOffCluster extends ZCLNodeCluster {
848
850
  export interface OnOffSwitchCluster extends ZCLNodeCluster {
849
851
  }
850
852
 
853
+ export interface OTAClusterAttributes {
854
+ upgradeServerID?: string;
855
+ fileOffset?: number;
856
+ currentFileVersion?: number;
857
+ currentZigBeeStackVersion?: number;
858
+ downloadedFileVersion?: number;
859
+ downloadedZigBeeStackVersion?: number;
860
+ imageUpgradeStatus?: 'normal' | 'downloadInProgress' | 'downloadComplete' | 'waitingToUpgrade' | 'countDown' | 'waitForMore' | 'waitingToUpgradeViaExternalEvent';
861
+ manufacturerID?: number;
862
+ imageTypeID?: number;
863
+ minimumBlockPeriod?: number;
864
+ imageStamp?: number;
865
+ upgradeActivationPolicy?: 'otaServerActivationAllowed' | 'outOfBandActivationOnly';
866
+ upgradeTimeoutPolicy?: 'applyUpgradeAfterTimeout' | 'doNotApplyUpgradeAfterTimeout';
867
+ }
868
+
851
869
  export interface OTACluster extends ZCLNodeCluster {
870
+ readAttributes<K extends 'upgradeServerID' | 'fileOffset' | 'currentFileVersion' | 'currentZigBeeStackVersion' | 'downloadedFileVersion' | 'downloadedZigBeeStackVersion' | 'imageUpgradeStatus' | 'manufacturerID' | 'imageTypeID' | 'minimumBlockPeriod' | 'imageStamp' | 'upgradeActivationPolicy' | 'upgradeTimeoutPolicy'>(attributeNames: K[], opts?: { timeout?: number }): Promise<Pick<OTAClusterAttributes, K>>;
871
+ readAttributes(attributeNames: Array<keyof OTAClusterAttributes | number>, opts?: { timeout?: number }): Promise<Partial<OTAClusterAttributes> & Record<number, unknown>>;
872
+ writeAttributes(attributes: Partial<OTAClusterAttributes>, opts?: { timeout?: number }): Promise<unknown>;
873
+ on<K extends keyof OTAClusterAttributes & string>(eventName: `attr.${K}`, listener: (value: OTAClusterAttributes[K]) => void): this;
874
+ once<K extends keyof OTAClusterAttributes & string>(eventName: `attr.${K}`, listener: (value: OTAClusterAttributes[K]) => void): this;
875
+ imageNotify(args?: { payloadType?: 'queryJitter' | 'queryJitterAndManufacturerCode' | 'queryJitterAndManufacturerCodeAndImageType' | 'queryJitterAndManufacturerCodeAndImageTypeAndNewFileVersion'; queryJitter?: number; manufacturerCode?: number; imageType?: number; newFileVersion?: number }, opts?: ClusterCommandOptions): Promise<void>;
876
+ queryNextImageRequest(args?: { fieldControl?: Partial<{ hardwareVersionPresent: boolean }>; manufacturerCode?: number; imageType?: number; fileVersion?: number; hardwareVersion?: number }, opts?: ClusterCommandOptions): Promise<{ status?: ZCLEnum8Status; manufacturerCode?: number; imageType?: number; fileVersion?: number; imageSize?: number }>;
877
+ imageBlockRequest(args?: { fieldControl?: Partial<{ requestNodeAddressPresent: boolean; minimumBlockPeriodPresent: boolean }>; manufacturerCode?: number; imageType?: number; fileVersion?: number; fileOffset?: number; maximumDataSize?: number; requestNodeAddress?: string; minimumBlockPeriod?: number }, opts?: ClusterCommandOptions): Promise<{ status?: ZCLEnum8Status; manufacturerCode?: number; imageType?: number; fileVersion?: number; fileOffset?: number; dataSize?: number; imageData?: Buffer; currentTime?: number; requestTime?: number; minimumBlockPeriod?: number }>;
878
+ imagePageRequest(args?: { fieldControl?: Partial<{ requestNodeAddressPresent: boolean }>; manufacturerCode?: number; imageType?: number; fileVersion?: number; fileOffset?: number; maximumDataSize?: number; pageSize?: number; responseSpacing?: number; requestNodeAddress?: string }, opts?: ClusterCommandOptions): Promise<{ status?: ZCLEnum8Status; manufacturerCode?: number; imageType?: number; fileVersion?: number; fileOffset?: number; dataSize?: number; imageData?: Buffer; currentTime?: number; requestTime?: number; minimumBlockPeriod?: number }>;
879
+ imageBlockResponse(args?: { status?: ZCLEnum8Status; manufacturerCode?: number; imageType?: number; fileVersion?: number; fileOffset?: number; dataSize?: number; imageData?: Buffer; currentTime?: number; requestTime?: number; minimumBlockPeriod?: number }, opts?: ClusterCommandOptions): Promise<void>;
880
+ upgradeEndRequest(args: { status: ZCLEnum8Status; manufacturerCode: number; imageType: number; fileVersion: number }, opts?: ClusterCommandOptions): Promise<{ manufacturerCode: number; imageType: number; fileVersion: number; currentTime: number; upgradeTime: number }>;
881
+ upgradeEndResponse(args: { manufacturerCode: number; imageType: number; fileVersion: number; currentTime: number; upgradeTime: number }, opts?: ClusterCommandOptions): Promise<void>;
882
+ queryDeviceSpecificFileRequest(args: { requestNodeAddress: string; manufacturerCode: number; imageType: number; fileVersion: number; zigBeeStackVersion: number }, opts?: ClusterCommandOptions): Promise<{ status?: ZCLEnum8Status; manufacturerCode?: number; imageType?: number; fileVersion?: number; imageSize?: number }>;
852
883
  }
853
884
 
854
885
  export interface PollControlClusterAttributes {
@@ -1165,7 +1196,7 @@ export interface ClusterAttributesByName {
1165
1196
  occupancySensing: OccupancySensingClusterAttributes;
1166
1197
  onOff: OnOffClusterAttributes;
1167
1198
  onOffSwitch: Record<string, unknown>;
1168
- ota: Record<string, unknown>;
1199
+ ota: OTAClusterAttributes;
1169
1200
  pollControl: PollControlClusterAttributes;
1170
1201
  powerConfiguration: PowerConfigurationClusterAttributes;
1171
1202
  powerProfile: Record<string, unknown>;
@@ -1197,6 +1228,7 @@ export type ZCLNodeEndpoint = {
1197
1228
  clusters: ClusterRegistry & {
1198
1229
  [clusterName: string]: ZCLNodeCluster | undefined;
1199
1230
  };
1231
+ bind(clusterName: string, impl: BoundCluster): void;
1200
1232
  };
1201
1233
 
1202
1234
  export interface ZCLNode {
@@ -1486,6 +1518,14 @@ export const CLUSTER: {
1486
1518
  COMMANDS: unknown;
1487
1519
  };
1488
1520
  };
1521
+ export class BoundCluster {
1522
+
1523
+ }
1524
+
1525
+ export class ZCLError extends Error {
1526
+ zclStatus: ZCLEnum8Status;
1527
+ constructor(zclStatus?: ZCLEnum8Status);
1528
+ }
1489
1529
  export const AlarmsCluster: {
1490
1530
  new (...args: any[]): AlarmsCluster;
1491
1531
  ID: 9;
package/index.js CHANGED
@@ -8,6 +8,7 @@ const Clusters = require('./lib/clusters');
8
8
  const BoundCluster = require('./lib/BoundCluster');
9
9
  const zclTypes = require('./lib/zclTypes');
10
10
  const zclFrames = require('./lib/zclFrames');
11
+ const { ZCLError } = require('./lib/util');
11
12
 
12
13
  /**
13
14
  * Enables or disables debug logging.
@@ -55,6 +56,7 @@ module.exports = {
55
56
  ZCLDataTypes,
56
57
  ZCLDataType,
57
58
  ZCLStruct,
59
+ ZCLError,
58
60
  ...Clusters,
59
61
  debug,
60
62
  ZIGBEE_PROFILE_ID,
@@ -317,7 +317,7 @@ class BoundCluster {
317
317
  const result = await this[command.name](args, meta, frame, rawFrame);
318
318
  if (command.response && command.response.args) {
319
319
  // eslint-disable-next-line new-cap
320
- return [command.response.id, new command.response.args(result)];
320
+ return [command.response.id, new command.response.args(result), command.response];
321
321
  }
322
322
  // eslint-disable-next-line consistent-return
323
323
  return;
package/lib/Cluster.js CHANGED
@@ -789,7 +789,7 @@ class Cluster extends EventEmitter {
789
789
  const response = await handler.call(this, args, meta, frame, rawFrame);
790
790
  if (command.response && command.response.args) {
791
791
  // eslint-disable-next-line new-cap
792
- return [command.response.id, new command.response.args(response)];
792
+ return [command.response.id, new command.response.args(response), command.response];
793
793
  }
794
794
  // eslint-disable-next-line consistent-return
795
795
  return;
@@ -1027,7 +1027,9 @@ class Cluster extends EventEmitter {
1027
1027
  clusterClass.commandsById = Object.entries(clusterClass.commands).reduce((r, [name, _cmd]) => {
1028
1028
  const cmd = { ..._cmd, name };
1029
1029
  if (cmd.args) {
1030
- cmd.args = ZCLStruct(`${clusterClass.NAME}.${name}`, cmd.args);
1030
+ cmd.args = ZCLStruct(`${clusterClass.NAME}.${name}`, cmd.args, {
1031
+ encodeMissingFieldsBehavior: cmd.encodeMissingFieldsBehavior,
1032
+ });
1031
1033
  if (_cmd === GLOBAL_COMMANDS.defaultResponse) {
1032
1034
  clusterClass.defaultResponseArgsType = cmd.args;
1033
1035
  }
@@ -1045,7 +1047,9 @@ class Cluster extends EventEmitter {
1045
1047
  res.id = cmd.id;
1046
1048
  }
1047
1049
  if (res.args) {
1048
- res.args = ZCLStruct(`${clusterClass.NAME}.${res.name}`, res.args);
1050
+ res.args = ZCLStruct(`${clusterClass.NAME}.${res.name}`, res.args, {
1051
+ encodeMissingFieldsBehavior: res.encodeMissingFieldsBehavior,
1052
+ });
1049
1053
  }
1050
1054
  if (cmd.global) res.global = true;
1051
1055
  if (cmd.manufacturerSpecific) res.manufacturerSpecific = true;
@@ -1096,7 +1100,9 @@ class Cluster extends EventEmitter {
1096
1100
  }
1097
1101
 
1098
1102
  if (cmd.args) {
1099
- const CommandArgs = ZCLStruct(`${this.name}.${cmdName}`, cmd.args);
1103
+ const CommandArgs = ZCLStruct(`${this.name}.${cmdName}`, cmd.args, {
1104
+ encodeMissingFieldsBehavior: cmd.encodeMissingFieldsBehavior,
1105
+ });
1100
1106
  payload.data = new CommandArgs(args);
1101
1107
  }
1102
1108
 
package/lib/Endpoint.js CHANGED
@@ -7,7 +7,8 @@ const BoundCluster = require('./BoundCluster');
7
7
  const { ZCLStandardHeader, ZCLMfgSpecificHeader } = require('./zclFrames');
8
8
 
9
9
  let { debug } = require('./util');
10
- const { getLogId } = require('./util');
10
+ const { getLogId, ZCLError } = require('./util');
11
+ const { ZCLDataTypes } = require('./zclTypes');
11
12
 
12
13
  debug = debug.extend('endpoint');
13
14
 
@@ -120,7 +121,10 @@ class Endpoint extends EventEmitter {
120
121
 
121
122
  // If cluster specific error, respond with a default response error frame
122
123
  if (clusterSpecificError) {
123
- const defaultResponseErrorFrame = this.makeDefaultResponseFrame(frame, false);
124
+ const status = clusterSpecificError instanceof ZCLError
125
+ ? clusterSpecificError.zclStatus : undefined;
126
+
127
+ const defaultResponseErrorFrame = this.makeDefaultResponseFrame(frame, false, status);
124
128
  this.sendFrame(clusterId, defaultResponseErrorFrame.toBuffer()).catch(err => {
125
129
  debug(`${this.getLogId(clusterId)}, error while sending default error response`, err, { response: defaultResponseErrorFrame });
126
130
  });
@@ -135,7 +139,10 @@ class Endpoint extends EventEmitter {
135
139
  // If a cluster specific response was generated, set the response data
136
140
  // and cmdId in the response frame.
137
141
  if (clusterSpecificResponse) {
138
- const [cmdId, data] = clusterSpecificResponse;
142
+ const [cmdId, data, cmd] = clusterSpecificResponse;
143
+ if (!cmd.global) {
144
+ responseFrame.frameControl.clusterSpecific = true;
145
+ }
139
146
  responseFrame.data = data.toBuffer();
140
147
  responseFrame.cmdId = cmdId;
141
148
  }
@@ -185,9 +192,10 @@ class Endpoint extends EventEmitter {
185
192
  * Returns a default response frame with an error status code.
186
193
  * @param {*} receivedFrame
187
194
  * @param {boolean} success
195
+ * @param {string} [status] - Optional ZCL status code to use in the response
188
196
  * @returns {ZCLStandardHeader|ZCLMfgSpecificHeader}
189
197
  */
190
- makeDefaultResponseFrame(receivedFrame, success) {
198
+ makeDefaultResponseFrame(receivedFrame, success, status) {
191
199
  let responseFrame;
192
200
  if (receivedFrame instanceof ZCLStandardHeader) {
193
201
  responseFrame = new ZCLStandardHeader();
@@ -205,6 +213,19 @@ class Endpoint extends EventEmitter {
205
213
  responseFrame.trxSequenceNumber = receivedFrame.trxSequenceNumber;
206
214
  responseFrame.cmdId = 0x0B;
207
215
  responseFrame.data = Buffer.from([receivedFrame.cmdId, success ? 0 : 1]);
216
+
217
+ // If not successful, set the status code in the response frame data
218
+ // Note that if the status code is invalid, enum8Status.toBuffer will throw an error,
219
+ // and the default error status will be FAILURE. This also allows overriding the status code
220
+ // with SUCCESS, which is intentional. The OTA Cluster requires this in some cases.
221
+ if (!success && typeof status === 'string') {
222
+ try {
223
+ ZCLDataTypes.enum8Status.toBuffer(responseFrame.data, status, 1);
224
+ } catch (err) {
225
+ // Ignore invalid status codes and keep the default FAILURE status.
226
+ }
227
+ }
228
+
208
229
  return responseFrame;
209
230
  }
210
231
 
@@ -1,15 +1,286 @@
1
1
  'use strict';
2
2
 
3
3
  const Cluster = require('../Cluster');
4
+ const { ZCLDataTypes } = require('../zclTypes');
4
5
 
5
- const ATTRIBUTES = {};
6
+ // ============================================================================
7
+ // Server Attributes
8
+ // ============================================================================
9
+ const ATTRIBUTES = {
10
+ // OTA Upgrade Cluster Attributes (0x0000 - 0x000C)
6
11
 
7
- const COMMANDS = {};
12
+ // The IEEE address of the upgrade server resulted from the discovery of the
13
+ // upgrade server's identity. If the value is set to a non-zero value and
14
+ // corresponds to an IEEE address of a device that is no longer accessible, a
15
+ // device MAY choose to discover a new Upgrade Server depending on its own
16
+ // security policies.
17
+ upgradeServerID: { id: 0x0000, type: ZCLDataTypes.EUI64 }, // Mandatory
18
+
19
+ // The parameter indicates the current location in the OTA upgrade image. It is
20
+ // essentially the (start of the) address of the image data that is being
21
+ // transferred from the OTA server to the client.
22
+ fileOffset: { id: 0x0001, type: ZCLDataTypes.uint32 }, // Optional
23
+
24
+ // The file version of the running firmware image on the device.
25
+ currentFileVersion: { id: 0x0002, type: ZCLDataTypes.uint32 }, // Optional
26
+
27
+ // The ZigBee stack version of the running image on the device.
28
+ currentZigBeeStackVersion: { id: 0x0003, type: ZCLDataTypes.uint16 }, // Optional
29
+
30
+ // The file version of the downloaded image on additional memory space on the
31
+ // device.
32
+ downloadedFileVersion: { id: 0x0004, type: ZCLDataTypes.uint32 }, // Optional
33
+
34
+ // The ZigBee stack version of the downloaded image on additional memory space
35
+ // on the device.
36
+ downloadedZigBeeStackVersion: { id: 0x0005, type: ZCLDataTypes.uint16 }, // Optional
37
+
38
+ // The upgrade status of the client device. The status indicates where the client
39
+ // device is at in terms of the download and upgrade process.
40
+ imageUpgradeStatus: { // Mandatory
41
+ id: 0x0006,
42
+ type: ZCLDataTypes.enum8({
43
+ normal: 0x00,
44
+ downloadInProgress: 0x01,
45
+ downloadComplete: 0x02,
46
+ waitingToUpgrade: 0x03,
47
+ countDown: 0x04,
48
+ waitForMore: 0x05,
49
+ waitingToUpgradeViaExternalEvent: 0x06,
50
+ }),
51
+ },
52
+
53
+ // The ZigBee assigned value for the manufacturer of the device.
54
+ manufacturerID: { id: 0x0007, type: ZCLDataTypes.uint16 }, // Optional
55
+
56
+ // The image type identifier of the file that the client is currently downloading,
57
+ // or a file that has been completely downloaded but not upgraded to yet.
58
+ imageTypeID: { id: 0x0008, type: ZCLDataTypes.uint16 }, // Optional
59
+
60
+ // This attribute acts as a rate limiting feature for the server to slow down the
61
+ // client download and prevent saturating the network with block requests. The
62
+ // value is in milliseconds.
63
+ minimumBlockPeriod: { id: 0x0009, type: ZCLDataTypes.uint16 }, // Optional
64
+
65
+ // A 32 bit value used as a second verification to identify the image. The value
66
+ // must be consistent during the lifetime of the same image and unique for each
67
+ // different build of the image.
68
+ imageStamp: { id: 0x000A, type: ZCLDataTypes.uint32 }, // 10, Optional
69
+
70
+ // Indicates what behavior the client device supports for activating a fully
71
+ // downloaded but not installed upgrade image.
72
+ upgradeActivationPolicy: { // Optional
73
+ id: 0x000B, // 11
74
+ type: ZCLDataTypes.enum8({
75
+ otaServerActivationAllowed: 0x00,
76
+ outOfBandActivationOnly: 0x01,
77
+ }),
78
+ },
79
+
80
+ // Dictates the behavior of the device in situations where an explicit activation
81
+ // command cannot be retrieved.
82
+ upgradeTimeoutPolicy: { // Optional
83
+ id: 0x000C, // 12
84
+ type: ZCLDataTypes.enum8({
85
+ applyUpgradeAfterTimeout: 0x00,
86
+ doNotApplyUpgradeAfterTimeout: 0x01,
87
+ }),
88
+ },
89
+ };
90
+
91
+ // Response to the Image Block Request or Image Page Request. The payload varies
92
+ // based on the status field.
93
+ const IMAGE_BLOCK_RESPONSE = {
94
+ id: 0x0005,
95
+ encodeMissingFieldsBehavior: 'skip',
96
+ args: {
97
+ status: ZCLDataTypes.enum8Status,
98
+ // When status is SUCCESS
99
+ manufacturerCode: ZCLDataTypes.uint16,
100
+ imageType: ZCLDataTypes.uint16,
101
+ fileVersion: ZCLDataTypes.uint32,
102
+ fileOffset: ZCLDataTypes.uint32,
103
+ dataSize: ZCLDataTypes.uint8,
104
+ imageData: ZCLDataTypes.buffer,
105
+ // When status is WAIT_FOR_DATA
106
+ currentTime: ZCLDataTypes.uint32,
107
+ requestTime: ZCLDataTypes.uint32,
108
+ minimumBlockPeriod: ZCLDataTypes.uint16,
109
+ },
110
+ };
111
+
112
+ const UPGRADE_END_RESPONSE = {
113
+ id: 0x0007,
114
+ args: {
115
+ manufacturerCode: ZCLDataTypes.uint16,
116
+ imageType: ZCLDataTypes.uint16,
117
+ fileVersion: ZCLDataTypes.uint32,
118
+ currentTime: ZCLDataTypes.uint32,
119
+ upgradeTime: ZCLDataTypes.uint32,
120
+ },
121
+ };
122
+
123
+ // ============================================================================
124
+ // Commands
125
+ // ============================================================================
126
+ const COMMANDS = {
127
+ // --- Server to Client Commands ---
128
+
129
+ // The purpose of sending Image Notify command is so the server has a way to
130
+ // notify client devices of when the OTA upgrade images are available for them.
131
+ imageNotify: { // Optional
132
+ id: 0x0000,
133
+ direction: Cluster.DIRECTION_SERVER_TO_CLIENT,
134
+ encodeMissingFieldsBehavior: 'skip',
135
+ frameControl: ['directionToClient', 'clusterSpecific', 'disableDefaultResponse'],
136
+ args: {
137
+ payloadType: ZCLDataTypes.enum8({
138
+ queryJitter: 0x00,
139
+ queryJitterAndManufacturerCode: 0x01,
140
+ queryJitterAndManufacturerCodeAndImageType: 0x02,
141
+ queryJitterAndManufacturerCodeAndImageTypeAndNewFileVersion: 0x03,
142
+ }),
143
+ queryJitter: ZCLDataTypes.uint8,
144
+ // Present when payloadType >= 0x01
145
+ manufacturerCode: ZCLDataTypes.uint16,
146
+ // Present when payloadType >= 0x02
147
+ imageType: ZCLDataTypes.uint16,
148
+ // Present when payloadType >= 0x03
149
+ newFileVersion: ZCLDataTypes.uint32,
150
+ },
151
+ },
152
+
153
+ // --- Client to Server Commands ---
154
+
155
+ // Client queries the server if new OTA upgrade image is available.
156
+ queryNextImageRequest: { // Mandatory
157
+ id: 0x0001,
158
+ encodeMissingFieldsBehavior: 'skip',
159
+ args: {
160
+ fieldControl: ZCLDataTypes.map8('hardwareVersionPresent'),
161
+ manufacturerCode: ZCLDataTypes.uint16,
162
+ imageType: ZCLDataTypes.uint16,
163
+ fileVersion: ZCLDataTypes.uint32,
164
+ // Present when fieldControl bit 0 is set
165
+ hardwareVersion: ZCLDataTypes.uint16,
166
+ },
167
+ response: {
168
+ id: 0x0002,
169
+ encodeMissingFieldsBehavior: 'skip',
170
+ args: {
171
+ status: ZCLDataTypes.enum8Status,
172
+ // Remaining fields only present when status is SUCCESS
173
+ manufacturerCode: ZCLDataTypes.uint16,
174
+ imageType: ZCLDataTypes.uint16,
175
+ fileVersion: ZCLDataTypes.uint32,
176
+ imageSize: ZCLDataTypes.uint32,
177
+ },
178
+ },
179
+ },
180
+
181
+ // Client requests a block of data from the OTA upgrade image.
182
+ imageBlockRequest: { // Mandatory
183
+ id: 0x0003,
184
+ encodeMissingFieldsBehavior: 'skip',
185
+ args: {
186
+ fieldControl: ZCLDataTypes.map8(
187
+ 'requestNodeAddressPresent',
188
+ 'minimumBlockPeriodPresent',
189
+ ),
190
+ manufacturerCode: ZCLDataTypes.uint16,
191
+ imageType: ZCLDataTypes.uint16,
192
+ fileVersion: ZCLDataTypes.uint32,
193
+ fileOffset: ZCLDataTypes.uint32,
194
+ maximumDataSize: ZCLDataTypes.uint8,
195
+ // Present when fieldControl bit 0 is set
196
+ requestNodeAddress: ZCLDataTypes.EUI64,
197
+ // Present when fieldControl bit 1 is set
198
+ minimumBlockPeriod: ZCLDataTypes.uint16,
199
+ },
200
+ response: IMAGE_BLOCK_RESPONSE,
201
+ },
202
+
203
+ // Client requests pages of data from the OTA upgrade image. Using Image Page
204
+ // Request reduces the number of requests sent from the client to the upgrade
205
+ // server, compared to using Image Block Request command.
206
+ imagePageRequest: { // Optional
207
+ id: 0x0004,
208
+ encodeMissingFieldsBehavior: 'skip',
209
+ args: {
210
+ fieldControl: ZCLDataTypes.map8('requestNodeAddressPresent'),
211
+ manufacturerCode: ZCLDataTypes.uint16,
212
+ imageType: ZCLDataTypes.uint16,
213
+ fileVersion: ZCLDataTypes.uint32,
214
+ fileOffset: ZCLDataTypes.uint32,
215
+ maximumDataSize: ZCLDataTypes.uint8,
216
+ pageSize: ZCLDataTypes.uint16,
217
+ responseSpacing: ZCLDataTypes.uint16,
218
+ // Present when fieldControl bit 0 is set
219
+ requestNodeAddress: ZCLDataTypes.EUI64,
220
+ },
221
+ response: IMAGE_BLOCK_RESPONSE,
222
+ },
223
+
224
+ imageBlockResponse: { // Mandatory
225
+ ...IMAGE_BLOCK_RESPONSE,
226
+ direction: Cluster.DIRECTION_SERVER_TO_CLIENT,
227
+ frameControl: ['directionToClient', 'clusterSpecific', 'disableDefaultResponse'],
228
+ },
229
+
230
+ // Sent by the client when it has completed downloading an image. The status
231
+ // value SHALL be SUCCESS, INVALID_IMAGE, REQUIRE_MORE_IMAGE, or ABORT.
232
+ upgradeEndRequest: { // Mandatory
233
+ id: 0x0006,
234
+ args: {
235
+ status: ZCLDataTypes.enum8Status,
236
+ manufacturerCode: ZCLDataTypes.uint16,
237
+ imageType: ZCLDataTypes.uint16,
238
+ fileVersion: ZCLDataTypes.uint32,
239
+ },
240
+ response: UPGRADE_END_RESPONSE,
241
+ },
242
+
243
+ // --- Server to Client Commands ---
244
+
245
+ // Response to the Upgrade End Request, indicating when the client should upgrade
246
+ // to the new image.
247
+ upgradeEndResponse: { // Mandatory
248
+ ...UPGRADE_END_RESPONSE,
249
+ direction: Cluster.DIRECTION_SERVER_TO_CLIENT,
250
+ frameControl: ['directionToClient', 'clusterSpecific', 'disableDefaultResponse'],
251
+ },
252
+
253
+ // Client requests a device specific file such as security credential,
254
+ // configuration or log.
255
+ queryDeviceSpecificFileRequest: { // Optional
256
+ id: 0x0008,
257
+ args: {
258
+ requestNodeAddress: ZCLDataTypes.EUI64,
259
+ manufacturerCode: ZCLDataTypes.uint16,
260
+ imageType: ZCLDataTypes.uint16,
261
+ fileVersion: ZCLDataTypes.uint32,
262
+ zigBeeStackVersion: ZCLDataTypes.uint16,
263
+ },
264
+ response: {
265
+ id: 0x0009,
266
+ encodeMissingFieldsBehavior: 'skip',
267
+ args: {
268
+ status: ZCLDataTypes.enum8Status,
269
+ // Remaining fields only present when status is SUCCESS
270
+ manufacturerCode: ZCLDataTypes.uint16,
271
+ imageType: ZCLDataTypes.uint16,
272
+ fileVersion: ZCLDataTypes.uint32,
273
+ imageSize: ZCLDataTypes.uint32,
274
+ },
275
+ },
276
+ },
277
+
278
+ };
8
279
 
9
280
  class OTACluster extends Cluster {
10
281
 
11
282
  static get ID() {
12
- return 25; // 0x19
283
+ return 0x0019; // 25
13
284
  }
14
285
 
15
286
  static get NAME() {
package/lib/util/index.js CHANGED
@@ -21,8 +21,18 @@ function getLogId(endpointId, clusterName, clusterId) {
21
21
  return `ep: ${endpointId}, cl: ${clusterName} (${clusterId})`;
22
22
  }
23
23
 
24
+ class ZCLError extends Error {
25
+
26
+ constructor(zclStatus = 'FAILURE') {
27
+ super(`ZCL Error: ${zclStatus}`);
28
+ this.zclStatus = zclStatus;
29
+ }
30
+
31
+ }
32
+
24
33
  module.exports = {
25
34
  debug,
26
35
  getLogId,
27
36
  getPropertyDescriptor,
37
+ ZCLError,
28
38
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zigbee-clusters",
3
- "version": "2.9.1",
3
+ "version": "2.10.0",
4
4
  "description": "Zigbee Cluster Library for Node.js",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -47,7 +47,7 @@
47
47
  "watch": "^1.0.2"
48
48
  },
49
49
  "dependencies": {
50
- "@athombv/data-types": "^1.1.3",
50
+ "@athombv/data-types": "^1.2.0",
51
51
  "debug": "^4.1.1"
52
52
  }
53
53
  }
@@ -9,6 +9,7 @@
9
9
 
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
+ const { ZCLDataTypes } = require('../lib/zclTypes');
12
13
 
13
14
  const OUTPUT_FILE = path.join(__dirname, '../index.d.ts');
14
15
 
@@ -17,7 +18,7 @@ const OUTPUT_FILE = path.join(__dirname, '../index.d.ts');
17
18
  * @param {object} dataType - ZCLDataType object with shortName and args
18
19
  * @returns {string} TypeScript type string
19
20
  */
20
- function zclTypeToTS(dataType) {
21
+ function zclTypeToTS(dataType, useEnum8StatusType = true) {
21
22
  if (!dataType || !dataType.shortName) return 'unknown';
22
23
 
23
24
  const { shortName, args } = dataType;
@@ -49,6 +50,10 @@ function zclTypeToTS(dataType) {
49
50
  return 'Buffer';
50
51
  }
51
52
 
53
+ if (dataType === ZCLDataTypes.enum8Status && useEnum8StatusType) {
54
+ return 'ZCLEnum8Status';
55
+ }
56
+
52
57
  // Enum types - extract keys from args[0]
53
58
  if (/^enum(4|8|16|32)$/.test(shortName)) {
54
59
  if (args && args[0] && typeof args[0] === 'object') {
@@ -111,6 +116,8 @@ function parseCluster(ClusterClass, exportName) {
111
116
  for (const [name, def] of Object.entries(cmds)) {
112
117
  const cmdArgs = [];
113
118
  const responseArgs = [];
119
+ const cmdArgsOptional = def && def.encodeMissingFieldsBehavior === 'skip';
120
+ const responseArgsOptional = def && def.response && def.response.encodeMissingFieldsBehavior === 'skip';
114
121
  if (def && def.args) {
115
122
  for (const [argName, argType] of Object.entries(def.args)) {
116
123
  cmdArgs.push({
@@ -128,7 +135,9 @@ function parseCluster(ClusterClass, exportName) {
128
135
  });
129
136
  }
130
137
  }
131
- commands.push({ name, args: cmdArgs, responseArgs });
138
+ commands.push({
139
+ name, args: cmdArgs, responseArgs, cmdArgsOptional, responseArgsOptional,
140
+ });
132
141
  }
133
142
 
134
143
  return {
@@ -188,13 +197,14 @@ function generateClusterInterface(cluster) {
188
197
  // Determine return type based on response args
189
198
  let returnType = 'void';
190
199
  if (cmd.responseArgs && cmd.responseArgs.length > 0) {
191
- returnType = `{ ${cmd.responseArgs.map(a => `${a.name}: ${a.tsType}`).join('; ')} }`;
200
+ const sep = cmd.responseArgsOptional ? '?: ' : ': ';
201
+ returnType = `{ ${cmd.responseArgs.map(a => `${a.name}${sep}${a.tsType}`).join('; ')} }`;
192
202
  }
193
203
 
194
204
  if (cmd.args.length > 0) {
195
- // Buffer arguments are optional - ZCL allows empty octet strings
196
- const allArgsOptional = cmd.args.every(a => a.tsType === 'Buffer');
197
- const argsType = `{ ${cmd.args.map(a => `${a.name}${a.tsType === 'Buffer' ? '?' : ''}: ${a.tsType}`).join('; ')} }`;
205
+ // Args are optional when encodeMissingFieldsBehavior is 'skip', or when all args are Buffers
206
+ const allArgsOptional = cmd.cmdArgsOptional || cmd.args.every(a => a.tsType === 'Buffer');
207
+ const argsType = `{ ${cmd.args.map(a => `${a.name}${(cmd.cmdArgsOptional || a.tsType === 'Buffer') ? '?' : ''}: ${a.tsType}`).join('; ')} }`;
198
208
  // If all args are optional, make the entire args object optional
199
209
  lines.push(` ${cmd.name}(args${allArgsOptional ? '?' : ''}: ${argsType}, opts?: ClusterCommandOptions): Promise<${returnType}>;`);
200
210
  } else {
@@ -270,6 +280,8 @@ type ZCLNodeConstructorInput = {
270
280
  meta?: unknown
271
281
  ) => Promise<void>;
272
282
  };
283
+
284
+ type ZCLEnum8Status = ${zclTypeToTS(ZCLDataTypes.enum8Status, false)};
273
285
  `);
274
286
 
275
287
  // Base ZCLNodeCluster interface
@@ -372,6 +384,7 @@ type ZCLNodeConstructorInput = {
372
384
  clusters: ClusterRegistry & {
373
385
  [clusterName: string]: ZCLNodeCluster | undefined;
374
386
  };
387
+ bind(clusterName: string, impl: BoundCluster): void;
375
388
  };
376
389
 
377
390
  export interface ZCLNode {
@@ -402,6 +415,15 @@ export const CLUSTER: {
402
415
  }
403
416
  lines.push('};');
404
417
 
418
+ lines.push(`export class BoundCluster {
419
+
420
+ }
421
+
422
+ export class ZCLError extends Error {
423
+ zclStatus: ZCLEnum8Status;
424
+ constructor(zclStatus?: ZCLEnum8Status);
425
+ }`);
426
+
405
427
  // Export all cluster classes
406
428
  for (const cluster of clusters) {
407
429
  const interfaceName = toInterfaceName(cluster);