zigbee-clusters 2.9.0 → 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
  }
@@ -579,32 +581,22 @@ export interface MeteringClusterAttributes {
579
581
  currentTier3SummationReceived?: number;
580
582
  currentTier4SummationDelivered?: number;
581
583
  currentTier4SummationReceived?: number;
582
- status?: unknown;
584
+ status?: Partial<{ checkMeter: boolean; lowBattery: boolean; tamperDetect: boolean; powerFailure: boolean; powerQuality: boolean; leakDetect: boolean; serviceDisconnect: boolean }>;
583
585
  remainingBatteryLife?: number;
584
586
  hoursInOperation?: number;
585
587
  hoursInFault?: number;
586
- extendedStatus?: unknown;
587
588
  unitOfMeasure?: unknown;
588
589
  multiplier?: number;
589
590
  divisor?: number;
590
- summationFormatting?: unknown;
591
- demandFormatting?: unknown;
592
- historicalConsumptionFormatting?: unknown;
593
- meteringDeviceType?: unknown;
594
591
  siteId?: Buffer;
595
592
  meterSerialNumber?: Buffer;
596
593
  energyCarrierUnitOfMeasure?: unknown;
597
- energyCarrierSummationFormatting?: unknown;
598
- energyCarrierDemandFormatting?: unknown;
599
594
  temperatureUnitOfMeasure?: unknown;
600
- temperatureFormatting?: unknown;
601
595
  moduleSerialNumber?: Buffer;
602
596
  operatingTariffLabelDelivered?: Buffer;
603
597
  operatingTariffLabelReceived?: Buffer;
604
598
  customerIdNumber?: Buffer;
605
599
  alternativeUnitOfMeasure?: unknown;
606
- alternativeDemandFormatting?: unknown;
607
- alternativeConsumptionFormatting?: unknown;
608
600
  instantaneousDemand?: number;
609
601
  currentDayConsumptionDelivered?: number;
610
602
  currentDayConsumptionReceived?: number;
@@ -714,14 +706,6 @@ export interface MeteringClusterAttributes {
714
706
  currentTier4Block14SummationDelivered?: number;
715
707
  currentTier4Block15SummationDelivered?: number;
716
708
  currentTier4Block16SummationDelivered?: number;
717
- genericAlarmMask?: unknown;
718
- electricityAlarmMask?: unknown;
719
- genericFlowPressureAlarmMask?: unknown;
720
- waterSpecificAlarmMask?: unknown;
721
- heatAndCoolingSpecificAlarmMask?: unknown;
722
- gasSpecificAlarmMask?: unknown;
723
- extendedGenericAlarmMask?: unknown;
724
- manufacturerAlarmMask?: unknown;
725
709
  currentNoTierBlock1SummationReceived?: number;
726
710
  currentNoTierBlock2SummationReceived?: number;
727
711
  currentNoTierBlock3SummationReceived?: number;
@@ -742,12 +726,10 @@ export interface MeteringClusterAttributes {
742
726
  billToDateTimeStampDelivered?: number;
743
727
  projectedBillDelivered?: number;
744
728
  projectedBillTimeStampDelivered?: number;
745
- billDeliveredTrailingDigit?: unknown;
746
729
  billToDateReceived?: number;
747
730
  billToDateTimeStampReceived?: number;
748
731
  projectedBillReceived?: number;
749
732
  projectedBillTimeStampReceived?: number;
750
- billReceivedTrailingDigit?: unknown;
751
733
  proposedChangeSupplyImplementationTime?: number;
752
734
  proposedChangeSupplyStatus?: unknown;
753
735
  uncontrolledFlowThreshold?: number;
@@ -759,7 +741,7 @@ export interface MeteringClusterAttributes {
759
741
  }
760
742
 
761
743
  export interface MeteringCluster extends ZCLNodeCluster {
762
- readAttributes<K extends 'currentSummationDelivered' | 'currentSummationReceived' | 'currentMaxDemandDelivered' | 'currentMaxDemandReceived' | 'dftSummation' | 'dailyFreezeTime' | 'powerFactor' | 'readingSnapShotTime' | 'currentMaxDemandDeliveredTime' | 'currentMaxDemandReceivedTime' | 'defaultUpdatePeriod' | 'fastPollUpdatePeriod' | 'currentBlockPeriodConsumptionDelivered' | 'dailyConsumptionTarget' | 'currentBlock' | 'profileIntervalPeriod' | 'currentTier1SummationDelivered' | 'currentTier1SummationReceived' | 'currentTier2SummationDelivered' | 'currentTier2SummationReceived' | 'currentTier3SummationDelivered' | 'currentTier3SummationReceived' | 'currentTier4SummationDelivered' | 'currentTier4SummationReceived' | 'status' | 'remainingBatteryLife' | 'hoursInOperation' | 'hoursInFault' | 'extendedStatus' | 'unitOfMeasure' | 'multiplier' | 'divisor' | 'summationFormatting' | 'demandFormatting' | 'historicalConsumptionFormatting' | 'meteringDeviceType' | 'siteId' | 'meterSerialNumber' | 'energyCarrierUnitOfMeasure' | 'energyCarrierSummationFormatting' | 'energyCarrierDemandFormatting' | 'temperatureUnitOfMeasure' | 'temperatureFormatting' | 'moduleSerialNumber' | 'operatingTariffLabelDelivered' | 'operatingTariffLabelReceived' | 'customerIdNumber' | 'alternativeUnitOfMeasure' | 'alternativeDemandFormatting' | 'alternativeConsumptionFormatting' | 'instantaneousDemand' | 'currentDayConsumptionDelivered' | 'currentDayConsumptionReceived' | 'previousDayConsumptionDelivered' | 'previousDayConsumptionReceived' | 'currentPartialProfileIntervalStartTimeDelivered' | 'currentPartialProfileIntervalStartTimeReceived' | 'currentPartialProfileIntervalValueDelivered' | 'currentPartialProfileIntervalValueReceived' | 'currentDayMaxPressure' | 'currentDayMinPressure' | 'previousDayMaxPressure' | 'previousDayMinPressure' | 'currentDayMaxDemand' | 'previousDayMaxDemand' | 'currentMonthMaxDemand' | 'currentYearMaxDemand' | 'currentDayMaxEnergyCarrierDemand' | 'previousDayMaxEnergyCarrierDemand' | 'currentMonthMaxEnergyCarrierDemand' | 'currentMonthMinEnergyCarrierDemand' | 'currentYearMaxEnergyCarrierDemand' | 'currentYearMinEnergyCarrierDemand' | 'maxNumberOfPeriodsDelivered' | 'currentDemandDelivered' | 'demandLimit' | 'demandIntegrationPeriod' | 'numberOfDemandSubintervals' | 'demandLimitArmDuration' | 'currentNoTierBlock1SummationDelivered' | 'currentNoTierBlock2SummationDelivered' | 'currentNoTierBlock3SummationDelivered' | 'currentNoTierBlock4SummationDelivered' | 'currentNoTierBlock5SummationDelivered' | 'currentNoTierBlock6SummationDelivered' | 'currentNoTierBlock7SummationDelivered' | 'currentNoTierBlock8SummationDelivered' | 'currentNoTierBlock9SummationDelivered' | 'currentNoTierBlock10SummationDelivered' | 'currentNoTierBlock11SummationDelivered' | 'currentNoTierBlock12SummationDelivered' | 'currentNoTierBlock13SummationDelivered' | 'currentNoTierBlock14SummationDelivered' | 'currentNoTierBlock15SummationDelivered' | 'currentNoTierBlock16SummationDelivered' | 'currentTier1Block1SummationDelivered' | 'currentTier1Block2SummationDelivered' | 'currentTier1Block3SummationDelivered' | 'currentTier1Block4SummationDelivered' | 'currentTier1Block5SummationDelivered' | 'currentTier1Block6SummationDelivered' | 'currentTier1Block7SummationDelivered' | 'currentTier1Block8SummationDelivered' | 'currentTier1Block9SummationDelivered' | 'currentTier1Block10SummationDelivered' | 'currentTier1Block11SummationDelivered' | 'currentTier1Block12SummationDelivered' | 'currentTier1Block13SummationDelivered' | 'currentTier1Block14SummationDelivered' | 'currentTier1Block15SummationDelivered' | 'currentTier1Block16SummationDelivered' | 'currentTier2Block1SummationDelivered' | 'currentTier2Block2SummationDelivered' | 'currentTier2Block3SummationDelivered' | 'currentTier2Block4SummationDelivered' | 'currentTier2Block5SummationDelivered' | 'currentTier2Block6SummationDelivered' | 'currentTier2Block7SummationDelivered' | 'currentTier2Block8SummationDelivered' | 'currentTier2Block9SummationDelivered' | 'currentTier2Block10SummationDelivered' | 'currentTier2Block11SummationDelivered' | 'currentTier2Block12SummationDelivered' | 'currentTier2Block13SummationDelivered' | 'currentTier2Block14SummationDelivered' | 'currentTier2Block15SummationDelivered' | 'currentTier2Block16SummationDelivered' | 'currentTier3Block1SummationDelivered' | 'currentTier3Block2SummationDelivered' | 'currentTier3Block3SummationDelivered' | 'currentTier3Block4SummationDelivered' | 'currentTier3Block5SummationDelivered' | 'currentTier3Block6SummationDelivered' | 'currentTier3Block7SummationDelivered' | 'currentTier3Block8SummationDelivered' | 'currentTier3Block9SummationDelivered' | 'currentTier3Block10SummationDelivered' | 'currentTier3Block11SummationDelivered' | 'currentTier3Block12SummationDelivered' | 'currentTier3Block13SummationDelivered' | 'currentTier3Block14SummationDelivered' | 'currentTier3Block15SummationDelivered' | 'currentTier3Block16SummationDelivered' | 'currentTier4Block1SummationDelivered' | 'currentTier4Block2SummationDelivered' | 'currentTier4Block3SummationDelivered' | 'currentTier4Block4SummationDelivered' | 'currentTier4Block5SummationDelivered' | 'currentTier4Block6SummationDelivered' | 'currentTier4Block7SummationDelivered' | 'currentTier4Block8SummationDelivered' | 'currentTier4Block9SummationDelivered' | 'currentTier4Block10SummationDelivered' | 'currentTier4Block11SummationDelivered' | 'currentTier4Block12SummationDelivered' | 'currentTier4Block13SummationDelivered' | 'currentTier4Block14SummationDelivered' | 'currentTier4Block15SummationDelivered' | 'currentTier4Block16SummationDelivered' | 'genericAlarmMask' | 'electricityAlarmMask' | 'genericFlowPressureAlarmMask' | 'waterSpecificAlarmMask' | 'heatAndCoolingSpecificAlarmMask' | 'gasSpecificAlarmMask' | 'extendedGenericAlarmMask' | 'manufacturerAlarmMask' | 'currentNoTierBlock1SummationReceived' | 'currentNoTierBlock2SummationReceived' | 'currentNoTierBlock3SummationReceived' | 'currentNoTierBlock4SummationReceived' | 'currentNoTierBlock5SummationReceived' | 'currentNoTierBlock6SummationReceived' | 'currentNoTierBlock7SummationReceived' | 'currentNoTierBlock8SummationReceived' | 'currentNoTierBlock9SummationReceived' | 'currentNoTierBlock10SummationReceived' | 'currentNoTierBlock11SummationReceived' | 'currentNoTierBlock12SummationReceived' | 'currentNoTierBlock13SummationReceived' | 'currentNoTierBlock14SummationReceived' | 'currentNoTierBlock15SummationReceived' | 'currentNoTierBlock16SummationReceived' | 'billToDateDelivered' | 'billToDateTimeStampDelivered' | 'projectedBillDelivered' | 'projectedBillTimeStampDelivered' | 'billDeliveredTrailingDigit' | 'billToDateReceived' | 'billToDateTimeStampReceived' | 'projectedBillReceived' | 'projectedBillTimeStampReceived' | 'billReceivedTrailingDigit' | 'proposedChangeSupplyImplementationTime' | 'proposedChangeSupplyStatus' | 'uncontrolledFlowThreshold' | 'uncontrolledFlowThresholdUnitOfMeasure' | 'uncontrolledFlowMultiplier' | 'uncontrolledFlowDivisor' | 'flowStabilisationPeriod' | 'flowMeasurementPeriod'>(attributeNames: K[], opts?: { timeout?: number }): Promise<Pick<MeteringClusterAttributes, K>>;
744
+ readAttributes<K extends 'currentSummationDelivered' | 'currentSummationReceived' | 'currentMaxDemandDelivered' | 'currentMaxDemandReceived' | 'dftSummation' | 'dailyFreezeTime' | 'powerFactor' | 'readingSnapShotTime' | 'currentMaxDemandDeliveredTime' | 'currentMaxDemandReceivedTime' | 'defaultUpdatePeriod' | 'fastPollUpdatePeriod' | 'currentBlockPeriodConsumptionDelivered' | 'dailyConsumptionTarget' | 'currentBlock' | 'profileIntervalPeriod' | 'currentTier1SummationDelivered' | 'currentTier1SummationReceived' | 'currentTier2SummationDelivered' | 'currentTier2SummationReceived' | 'currentTier3SummationDelivered' | 'currentTier3SummationReceived' | 'currentTier4SummationDelivered' | 'currentTier4SummationReceived' | 'status' | 'remainingBatteryLife' | 'hoursInOperation' | 'hoursInFault' | 'unitOfMeasure' | 'multiplier' | 'divisor' | 'siteId' | 'meterSerialNumber' | 'energyCarrierUnitOfMeasure' | 'temperatureUnitOfMeasure' | 'moduleSerialNumber' | 'operatingTariffLabelDelivered' | 'operatingTariffLabelReceived' | 'customerIdNumber' | 'alternativeUnitOfMeasure' | 'instantaneousDemand' | 'currentDayConsumptionDelivered' | 'currentDayConsumptionReceived' | 'previousDayConsumptionDelivered' | 'previousDayConsumptionReceived' | 'currentPartialProfileIntervalStartTimeDelivered' | 'currentPartialProfileIntervalStartTimeReceived' | 'currentPartialProfileIntervalValueDelivered' | 'currentPartialProfileIntervalValueReceived' | 'currentDayMaxPressure' | 'currentDayMinPressure' | 'previousDayMaxPressure' | 'previousDayMinPressure' | 'currentDayMaxDemand' | 'previousDayMaxDemand' | 'currentMonthMaxDemand' | 'currentYearMaxDemand' | 'currentDayMaxEnergyCarrierDemand' | 'previousDayMaxEnergyCarrierDemand' | 'currentMonthMaxEnergyCarrierDemand' | 'currentMonthMinEnergyCarrierDemand' | 'currentYearMaxEnergyCarrierDemand' | 'currentYearMinEnergyCarrierDemand' | 'maxNumberOfPeriodsDelivered' | 'currentDemandDelivered' | 'demandLimit' | 'demandIntegrationPeriod' | 'numberOfDemandSubintervals' | 'demandLimitArmDuration' | 'currentNoTierBlock1SummationDelivered' | 'currentNoTierBlock2SummationDelivered' | 'currentNoTierBlock3SummationDelivered' | 'currentNoTierBlock4SummationDelivered' | 'currentNoTierBlock5SummationDelivered' | 'currentNoTierBlock6SummationDelivered' | 'currentNoTierBlock7SummationDelivered' | 'currentNoTierBlock8SummationDelivered' | 'currentNoTierBlock9SummationDelivered' | 'currentNoTierBlock10SummationDelivered' | 'currentNoTierBlock11SummationDelivered' | 'currentNoTierBlock12SummationDelivered' | 'currentNoTierBlock13SummationDelivered' | 'currentNoTierBlock14SummationDelivered' | 'currentNoTierBlock15SummationDelivered' | 'currentNoTierBlock16SummationDelivered' | 'currentTier1Block1SummationDelivered' | 'currentTier1Block2SummationDelivered' | 'currentTier1Block3SummationDelivered' | 'currentTier1Block4SummationDelivered' | 'currentTier1Block5SummationDelivered' | 'currentTier1Block6SummationDelivered' | 'currentTier1Block7SummationDelivered' | 'currentTier1Block8SummationDelivered' | 'currentTier1Block9SummationDelivered' | 'currentTier1Block10SummationDelivered' | 'currentTier1Block11SummationDelivered' | 'currentTier1Block12SummationDelivered' | 'currentTier1Block13SummationDelivered' | 'currentTier1Block14SummationDelivered' | 'currentTier1Block15SummationDelivered' | 'currentTier1Block16SummationDelivered' | 'currentTier2Block1SummationDelivered' | 'currentTier2Block2SummationDelivered' | 'currentTier2Block3SummationDelivered' | 'currentTier2Block4SummationDelivered' | 'currentTier2Block5SummationDelivered' | 'currentTier2Block6SummationDelivered' | 'currentTier2Block7SummationDelivered' | 'currentTier2Block8SummationDelivered' | 'currentTier2Block9SummationDelivered' | 'currentTier2Block10SummationDelivered' | 'currentTier2Block11SummationDelivered' | 'currentTier2Block12SummationDelivered' | 'currentTier2Block13SummationDelivered' | 'currentTier2Block14SummationDelivered' | 'currentTier2Block15SummationDelivered' | 'currentTier2Block16SummationDelivered' | 'currentTier3Block1SummationDelivered' | 'currentTier3Block2SummationDelivered' | 'currentTier3Block3SummationDelivered' | 'currentTier3Block4SummationDelivered' | 'currentTier3Block5SummationDelivered' | 'currentTier3Block6SummationDelivered' | 'currentTier3Block7SummationDelivered' | 'currentTier3Block8SummationDelivered' | 'currentTier3Block9SummationDelivered' | 'currentTier3Block10SummationDelivered' | 'currentTier3Block11SummationDelivered' | 'currentTier3Block12SummationDelivered' | 'currentTier3Block13SummationDelivered' | 'currentTier3Block14SummationDelivered' | 'currentTier3Block15SummationDelivered' | 'currentTier3Block16SummationDelivered' | 'currentTier4Block1SummationDelivered' | 'currentTier4Block2SummationDelivered' | 'currentTier4Block3SummationDelivered' | 'currentTier4Block4SummationDelivered' | 'currentTier4Block5SummationDelivered' | 'currentTier4Block6SummationDelivered' | 'currentTier4Block7SummationDelivered' | 'currentTier4Block8SummationDelivered' | 'currentTier4Block9SummationDelivered' | 'currentTier4Block10SummationDelivered' | 'currentTier4Block11SummationDelivered' | 'currentTier4Block12SummationDelivered' | 'currentTier4Block13SummationDelivered' | 'currentTier4Block14SummationDelivered' | 'currentTier4Block15SummationDelivered' | 'currentTier4Block16SummationDelivered' | 'currentNoTierBlock1SummationReceived' | 'currentNoTierBlock2SummationReceived' | 'currentNoTierBlock3SummationReceived' | 'currentNoTierBlock4SummationReceived' | 'currentNoTierBlock5SummationReceived' | 'currentNoTierBlock6SummationReceived' | 'currentNoTierBlock7SummationReceived' | 'currentNoTierBlock8SummationReceived' | 'currentNoTierBlock9SummationReceived' | 'currentNoTierBlock10SummationReceived' | 'currentNoTierBlock11SummationReceived' | 'currentNoTierBlock12SummationReceived' | 'currentNoTierBlock13SummationReceived' | 'currentNoTierBlock14SummationReceived' | 'currentNoTierBlock15SummationReceived' | 'currentNoTierBlock16SummationReceived' | 'billToDateDelivered' | 'billToDateTimeStampDelivered' | 'projectedBillDelivered' | 'projectedBillTimeStampDelivered' | 'billToDateReceived' | 'billToDateTimeStampReceived' | 'projectedBillReceived' | 'projectedBillTimeStampReceived' | 'proposedChangeSupplyImplementationTime' | 'proposedChangeSupplyStatus' | 'uncontrolledFlowThreshold' | 'uncontrolledFlowThresholdUnitOfMeasure' | 'uncontrolledFlowMultiplier' | 'uncontrolledFlowDivisor' | 'flowStabilisationPeriod' | 'flowMeasurementPeriod'>(attributeNames: K[], opts?: { timeout?: number }): Promise<Pick<MeteringClusterAttributes, K>>;
763
745
  readAttributes(attributeNames: Array<keyof MeteringClusterAttributes | number>, opts?: { timeout?: number }): Promise<Partial<MeteringClusterAttributes> & Record<number, unknown>>;
764
746
  writeAttributes(attributes: Partial<MeteringClusterAttributes>, opts?: { timeout?: number }): Promise<unknown>;
765
747
  on<K extends keyof MeteringClusterAttributes & string>(eventName: `attr.${K}`, listener: (value: MeteringClusterAttributes[K]) => void): this;
@@ -868,7 +850,36 @@ export interface OnOffCluster extends ZCLNodeCluster {
868
850
  export interface OnOffSwitchCluster extends ZCLNodeCluster {
869
851
  }
870
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
+
871
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 }>;
872
883
  }
873
884
 
874
885
  export interface PollControlClusterAttributes {
@@ -1185,7 +1196,7 @@ export interface ClusterAttributesByName {
1185
1196
  occupancySensing: OccupancySensingClusterAttributes;
1186
1197
  onOff: OnOffClusterAttributes;
1187
1198
  onOffSwitch: Record<string, unknown>;
1188
- ota: Record<string, unknown>;
1199
+ ota: OTAClusterAttributes;
1189
1200
  pollControl: PollControlClusterAttributes;
1190
1201
  powerConfiguration: PowerConfigurationClusterAttributes;
1191
1202
  powerProfile: Record<string, unknown>;
@@ -1217,6 +1228,7 @@ export type ZCLNodeEndpoint = {
1217
1228
  clusters: ClusterRegistry & {
1218
1229
  [clusterName: string]: ZCLNodeCluster | undefined;
1219
1230
  };
1231
+ bind(clusterName: string, impl: BoundCluster): void;
1220
1232
  };
1221
1233
 
1222
1234
  export interface ZCLNode {
@@ -1506,6 +1518,14 @@ export const CLUSTER: {
1506
1518
  COMMANDS: unknown;
1507
1519
  };
1508
1520
  };
1521
+ export class BoundCluster {
1522
+
1523
+ }
1524
+
1525
+ export class ZCLError extends Error {
1526
+ zclStatus: ZCLEnum8Status;
1527
+ constructor(zclStatus?: ZCLEnum8Status);
1528
+ }
1509
1529
  export const AlarmsCluster: {
1510
1530
  new (...args: any[]): AlarmsCluster;
1511
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
 
@@ -33,34 +33,51 @@ const ATTRIBUTES = {
33
33
  currentTier4SummationReceived: { id: 0x0107, type: ZCLDataTypes.uint48 }, // 263, Optional
34
34
 
35
35
  // Meter Status (0x0200 - 0x02FF)
36
- status: { id: 0x0200, type: ZCLDataTypes.map8 }, // 512, Optional
36
+ status: { // 512, Optional
37
+ id: 0x0200,
38
+ type: ZCLDataTypes.map8(
39
+ 'checkMeter',
40
+ 'lowBattery',
41
+ 'tamperDetect',
42
+ 'powerFailure',
43
+ 'powerQuality',
44
+ 'leakDetect',
45
+ 'serviceDisconnect',
46
+ ),
47
+ },
37
48
  remainingBatteryLife: { id: 0x0201, type: ZCLDataTypes.uint8 }, // 513, Optional
38
49
  hoursInOperation: { id: 0x0202, type: ZCLDataTypes.uint24 }, // 514, Optional
39
50
  hoursInFault: { id: 0x0203, type: ZCLDataTypes.uint24 }, // 515, Optional
40
- extendedStatus: { id: 0x0204, type: ZCLDataTypes.map64 }, // 516, Optional
51
+ // TODO: map64 bitmap with general flags (bits 0-13) and meter-type-specific flags (bits 24+)
52
+ // extendedStatus: { id: 0x0204, type: ZCLDataTypes.map64() }, // 516, Optional
41
53
 
42
54
  // Formatting Set (0x0300 - 0x03FF)
43
55
  unitOfMeasure: { id: 0x0300, type: ZCLDataTypes.enum8 }, // 768, Mandatory
44
56
  multiplier: { id: 0x0301, type: ZCLDataTypes.uint24 }, // 769, Optional
45
57
  divisor: { id: 0x0302, type: ZCLDataTypes.uint24 }, // 770, Optional
46
- summationFormatting: { id: 0x0303, type: ZCLDataTypes.map8 }, // 771, Mandatory
47
- demandFormatting: { id: 0x0304, type: ZCLDataTypes.map8 }, // 772, Optional
48
- historicalConsumptionFormatting: { id: 0x0305, type: ZCLDataTypes.map8 }, // 773, Optional
49
- meteringDeviceType: { id: 0x0306, type: ZCLDataTypes.map8 }, // 774, Mandatory
58
+ // TODO: map8 packed fields — bits 0-2: right digits, 3-6: left digits, 7: suppress zeros
59
+ // summationFormatting: { id: 0x0303, type: ZCLDataTypes.map8() }, // 771, Mandatory
60
+ // demandFormatting: { id: 0x0304, type: ZCLDataTypes.map8() }, // 772, Optional
61
+ // historicalConsumptionFormatting: { id: 0x0305, type: ZCLDataTypes.map8() }, // 773, Optional
62
+ // TODO: ZCL spec says enum values, but wire type is map8 for backwards compat
63
+ // meteringDeviceType: { id: 0x0306, type: ZCLDataTypes.map8() }, // 774, Mandatory
50
64
  siteId: { id: 0x0307, type: ZCLDataTypes.octstr }, // 775, Optional
51
65
  meterSerialNumber: { id: 0x0308, type: ZCLDataTypes.octstr }, // 776, Optional
52
66
  energyCarrierUnitOfMeasure: { id: 0x0309, type: ZCLDataTypes.enum8 }, // 777, Optional
53
- energyCarrierSummationFormatting: { id: 0x030A, type: ZCLDataTypes.map8 }, // 778, Optional
54
- energyCarrierDemandFormatting: { id: 0x030B, type: ZCLDataTypes.map8 }, // 779, Optional
67
+ // TODO: map8 packed fields — bits 0-2: right digits, 3-6: left digits, 7: suppress zeros
68
+ // energyCarrierSummationFormatting: { id: 0x030A, type: ZCLDataTypes.map8() }, // 778, Optional
69
+ // energyCarrierDemandFormatting: { id: 0x030B, type: ZCLDataTypes.map8() }, // 779, Optional
55
70
  temperatureUnitOfMeasure: { id: 0x030C, type: ZCLDataTypes.enum8 }, // 780, Optional
56
- temperatureFormatting: { id: 0x030D, type: ZCLDataTypes.map8 }, // 781, Optional
71
+ // TODO: map8 packed fields — bits 0-2: right digits, 3-6: left digits, 7: suppress zeros
72
+ // temperatureFormatting: { id: 0x030D, type: ZCLDataTypes.map8() }, // 781, Optional
57
73
  moduleSerialNumber: { id: 0x030E, type: ZCLDataTypes.octstr }, // 782, Optional
58
74
  operatingTariffLabelDelivered: { id: 0x030F, type: ZCLDataTypes.octstr }, // 783, Optional
59
75
  operatingTariffLabelReceived: { id: 0x0310, type: ZCLDataTypes.octstr }, // 784, Optional
60
76
  customerIdNumber: { id: 0x0311, type: ZCLDataTypes.octstr }, // 785, Optional
61
77
  alternativeUnitOfMeasure: { id: 0x0312, type: ZCLDataTypes.enum8 }, // 786, Optional
62
- alternativeDemandFormatting: { id: 0x0313, type: ZCLDataTypes.map8 }, // 787, Optional
63
- alternativeConsumptionFormatting: { id: 0x0314, type: ZCLDataTypes.map8 }, // 788, Optional
78
+ // TODO: map8 packed fields — bits 0-2: right digits, 3-6: left digits, 7: suppress zeros
79
+ // alternativeDemandFormatting: { id: 0x0313, type: ZCLDataTypes.map8() }, // 787, Optional
80
+ // alternativeConsumptionFormatting: { id: 0x0314, type: ZCLDataTypes.map8() }, // 788, Optional
64
81
 
65
82
  // Historical Consumption (0x0400 - 0x04FF)
66
83
  instantaneousDemand: { id: 0x0400, type: ZCLDataTypes.int24 }, // 1024, Optional
@@ -193,14 +210,15 @@ const ATTRIBUTES = {
193
210
  currentTier4Block16SummationDelivered: { id: 0x074F, type: ZCLDataTypes.uint48 },
194
211
 
195
212
  // Alarms (0x0800 - 0x08FF)
196
- genericAlarmMask: { id: 0x0800, type: ZCLDataTypes.map16 }, // 2048, Optional
197
- electricityAlarmMask: { id: 0x0801, type: ZCLDataTypes.map32 }, // 2049, Optional
198
- genericFlowPressureAlarmMask: { id: 0x0802, type: ZCLDataTypes.map16 }, // 2050, Optional
199
- waterSpecificAlarmMask: { id: 0x0803, type: ZCLDataTypes.map16 }, // 2051, Optional
200
- heatAndCoolingSpecificAlarmMask: { id: 0x0804, type: ZCLDataTypes.map16 }, // 2052, Optional
201
- gasSpecificAlarmMask: { id: 0x0805, type: ZCLDataTypes.map16 }, // 2053, Optional
202
- extendedGenericAlarmMask: { id: 0x0806, type: ZCLDataTypes.map48 }, // 2054, Optional
203
- manufacturerAlarmMask: { id: 0x0807, type: ZCLDataTypes.map16 }, // 2055, Optional
213
+ // TODO: map16/32/48 alarm masks bits map to alarm codes in their group (spec 10.4.2.2.9.1)
214
+ // genericAlarmMask: { id: 0x0800, type: ZCLDataTypes.map16() }, // 2048, Optional
215
+ // electricityAlarmMask: { id: 0x0801, type: ZCLDataTypes.map32() }, // 2049, Optional
216
+ // genericFlowPressureAlarmMask: { id: 0x0802, type: ZCLDataTypes.map16() }, // 2050, Optional
217
+ // waterSpecificAlarmMask: { id: 0x0803, type: ZCLDataTypes.map16() }, // 2051, Optional
218
+ // heatAndCoolingSpecificAlarmMask: { id: 0x0804, type: ZCLDataTypes.map16() }, // 2052, Optional
219
+ // gasSpecificAlarmMask: { id: 0x0805, type: ZCLDataTypes.map16() }, // 2053, Optional
220
+ // extendedGenericAlarmMask: { id: 0x0806, type: ZCLDataTypes.map48() }, // 2054, Optional
221
+ // manufacturerAlarmMask: { id: 0x0807, type: ZCLDataTypes.map16() }, // 2055, Optional
204
222
 
205
223
  // Block Information Received (0x0900 - 0x09FF)
206
224
  // All attributes in this section are Optional
@@ -226,12 +244,14 @@ const ATTRIBUTES = {
226
244
  billToDateTimeStampDelivered: { id: 0x0A01, type: ZCLDataTypes.uint32 }, // 2561, Optional
227
245
  projectedBillDelivered: { id: 0x0A02, type: ZCLDataTypes.uint32 }, // 2562, Optional
228
246
  projectedBillTimeStampDelivered: { id: 0x0A03, type: ZCLDataTypes.uint32 }, // 2563, Optional
229
- billDeliveredTrailingDigit: { id: 0x0A04, type: ZCLDataTypes.map8 }, // 2564, Optional
247
+ // TODO: map8 packed field most significant nibble = digits right of decimal
248
+ // billDeliveredTrailingDigit: { id: 0x0A04, type: ZCLDataTypes.map8() }, // 2564, Optional
230
249
  billToDateReceived: { id: 0x0A10, type: ZCLDataTypes.uint32 }, // 2576, Optional
231
250
  billToDateTimeStampReceived: { id: 0x0A11, type: ZCLDataTypes.uint32 }, // 2577, Optional
232
251
  projectedBillReceived: { id: 0x0A12, type: ZCLDataTypes.uint32 }, // 2578, Optional
233
252
  projectedBillTimeStampReceived: { id: 0x0A13, type: ZCLDataTypes.uint32 }, // 2579, Optional
234
- billReceivedTrailingDigit: { id: 0x0A14, type: ZCLDataTypes.map8 }, // 2580, Optional
253
+ // TODO: map8 packed field most significant nibble = digits right of decimal
254
+ // billReceivedTrailingDigit: { id: 0x0A14, type: ZCLDataTypes.map8() }, // 2580, Optional
235
255
 
236
256
  // Supply Control (0x0B00 - 0x0BFF)
237
257
  proposedChangeSupplyImplementationTime: { // Optional
@@ -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.0",
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);