From 632b4990b645dae3e77a67569adf1a97de2a9eb2 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Mon, 26 Jan 2026 19:22:44 +0100 Subject: [PATCH 1/9] feat(types): auto-generate TypeScript interfaces for all 46 clusters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add scripts/generate-types.js to parse cluster definitions and generate TypeScript interfaces with proper attribute/command typing - Generate typed attributes for each cluster with: - Proper enum8/enum16 types as string literal unions - Proper map8/map16 types as Partial> - Correct primitive type mappings (uint8→number, bool→boolean, etc.) - Generate typed command methods with proper argument types - Add ClusterRegistry interface for type-safe cluster access - Add npm run generate-types script for regenerating types - Add typescript as devDependency for type verification Usage: const cluster = endpoint.clusters.doorLock; if (cluster) { const attrs = await cluster.readAttributes(['lockState', 'lockType']); // attrs.lockState is typed as 'notFullyLocked' | 'locked' | 'unlocked' | ... } --- index.d.ts | 1139 +++++++++++++++++++++++++++++-------- package-lock.json | 58 +- package.json | 3 + scripts/generate-types.js | 573 +++++++++++++++++++ 4 files changed, 1504 insertions(+), 269 deletions(-) create mode 100644 scripts/generate-types.js diff --git a/index.d.ts b/index.d.ts index 90d3e58..5ae2133 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,3 +1,6 @@ +// Auto-generated TypeScript definitions for zigbee-clusters +// Generated by scripts/generate-types.js + import * as EventEmitter from "events"; type EndpointDescriptor = { @@ -10,294 +13,887 @@ type ConstructorOptions = { endpointDescriptors: EndpointDescriptor[]; sendFrame: (endpointId: number, clusterId: number, frame: Buffer) => Promise; }; -interface ZCLNodeCluster extends EventEmitter { + +export interface ZCLNodeCluster extends EventEmitter { /** - * Command which requests the remote cluster to report its generated commands. Generated - * commands are commands which may be sent by the remote cluster. - * - * TODO: handle the case where `lastResponse===false`. It might be possible that there are - * more commands to be reported than can be transmitted in one report (in practice very - * unlikely though). If `lastResponse===false` invoke `discoverCommandsGenerated` again - * starting from the index where the previous invocation stopped (`maxResults`). - * - * TODO: The manufacturer-specific sub-field SHALL be set to 0 to discover standard commands - * in a ZigBee cluster or 1 to discover manufacturer-specific commands in either a standard or - * a manufacturer-specific cluster. A manufacturer ID in this field of 0xffff (wildcard) will - * discover any manufacture- specific - * commands. - * - * @param {object} [opts=] - * @param {number} [opts.startValue=0] - * @param {number} [opts.maxResults=250] - * @returns {Promise} + * Command which requests the remote cluster to report its generated commands. */ - discoverCommandsGenerated({ - startValue, - maxResults, - }?: { + discoverCommandsGenerated(opts?: { startValue?: number; maxResults?: number; }): Promise; + /** - * Command which requests the remote cluster to report its received commands. Received - * commands are commands which may be received by the remote cluster. - * - * TODO: handle the case where `lastResponse===false`. It might be possible that there are - * more commands to be reported than can be transmitted in one report (in practice very - * unlikely though). If `lastResponse===false` invoke `discoverCommandsGenerated` again - * starting from the index where the previous invocation stopped (`maxResults`). - * - * TODO: The manufacturer-specific sub-field SHALL be set to 0 to discover standard commands - * in a ZigBee cluster or 1 to discover manufacturer-specific commands in either a standard or - * a manufacturer-specific cluster. A manufacturer ID in this field of 0xffff (wildcard) will - * discover any manufacture- specific commands. - * - * @param {object} [opts=] - * @param {number} [opts.startValue=0] - * @param {number} [opts.maxResults=255] - * @returns {Promise} + * Command which requests the remote cluster to report its received commands. */ - discoverCommandsReceived({ - startValue, - maxResults, - }?: { + discoverCommandsReceived(opts?: { startValue?: number; maxResults?: number; }): Promise; + /** * Command which reads a given set of attributes from the remote cluster. - * Note: do not mix regular and manufacturer specific attributes. - * @param {string[]} attributeNames - * @param {{timeout: number}} [opts=] - * @returns {Promise>} - Object with values (e.g. `{ onOff: true }`) */ readAttributes( attributeNames: string[], - opts?: { - timeout: number; - } - ): Promise<{ - [x: string]: unknown; - }>; + opts?: { timeout?: number } + ): Promise<{ [x: string]: unknown }>; + /** * Command which writes a given set of attribute key-value pairs to the remote cluster. - * Note: do not mix regular and manufacturer specific attributes. - * @param {object} attributes - Object with attribute names as keys and their values (e.g. `{ - * onOff: true, fakeAttributeName: 10 }`. - * @returns {Promise<*|{attributes: *}>} */ - writeAttributes(attributes?: object): Promise< - | any - | { - attributes: any; - } - >; + writeAttributes(attributes?: object): Promise; + /** - * Command which configures attribute reporting for the given `attributes` on the remote cluster. - * Note: do not mix regular and manufacturer specific attributes. - * @param {object} attributes - Attribute reporting configuration (e.g. `{ onOff: { - * minInterval: 0, maxInterval: 300, minChange: 1 } }`) - * @returns {Promise} + * Command which configures attribute reporting for the given attributes on the remote cluster. */ configureReporting(attributes?: object): Promise; + /** - * @typedef {object} ReadReportingConfiguration - * @property {ZCLDataTypes.enum8Status} status - * @property {'reported'|'received'} direction - * @property {number} attributeId - * @property {ZCLDataType.id} [attributeDataType] - * @property {number} [minInterval] - * @property {number} [maxInterval] - * @property {number} [minChange] - * @property {number} [timeoutPeriod] - */ - /** - * Command which retrieves the reporting configurations for the given `attributes` from the - * remote cluster. Currently this only takes the 'reported' into account, this represents the - * reports the remote cluster would sent out, instead of receive (which is likely the most - * interesting). - * Note: do not mix regular and manufacturer specific attributes. - * @param {Array} attributes - Array with number/strings (either attribute id, or attribute name). - * @returns {Promise} - Returns array with - * ReadReportingConfiguration objects per attribute. + * Command which retrieves the reporting configurations for the given attributes. */ - readReportingConfiguration(attributes?: any[]): Promise< - { - status: any; - direction: "reported" | "received"; - attributeId: number; - attributeDataType?: number; - minInterval?: number; - maxInterval?: number; - minChange?: number; - timeoutPeriod?: number; - }[] - >; + readReportingConfiguration(attributes?: (string | number)[]): Promise<{ + status: string; + direction: 'reported' | 'received'; + attributeId: number; + attributeDataType?: number; + minInterval?: number; + maxInterval?: number; + minChange?: number; + timeoutPeriod?: number; + }[]>; + /** * Command which discovers the implemented attributes on the remote cluster. - * - * TODO: handle the case where `lastResponse===false`. It might be possible that there are - * more commands to be reported than can be transmitted in one report (in practice very - * unlikely though). If `lastResponse===false` invoke `discoverCommandsGenerated` again - * starting from the index where the previous invocation stopped (`maxResults`). - * - * TODO: The manufacturer specific sub-field SHALL be set to 0 to discover standard attributes - * in a ZigBee cluster or 1 to discover manufacturer specific attributes in either a standard - * or a manufacturer specific cluster. - * - * @returns {Promise} - Array with string or number values (depending on if the - * attribute - * is implemented in zigbee-clusters or not). */ - discoverAttributes(): Promise; + discoverAttributes(): Promise<(string | number)[]>; + /** - * Command which discovers the implemented attributes on the remote cluster, the difference with - * `discoverAttributes` is that this command also reports the access control field of the - * attribute (whether it is readable/writable/reportable). - * - * TODO: handle the case where `lastResponse===false`. It might be possible that there are - * more commands to be reported than can be transmitted in one report (in practice very - * unlikely though). If `lastResponse===false` invoke `discoverCommandsGenerated` again - * starting from the index where the previous invocation stopped (`maxResults`). - * - * TODO: The manufacturer-specific sub-field SHALL be set to 0 to discover standard attributes - * in a ZigBee cluster or 1 to discover manufacturer-specific attributes in either a standard - * or a manufacturer- specific cluster. A manufacturer ID in this field of 0xffff (wildcard) - * will discover any manufacture-specific attributes. - * - * @returns {Promise} - Returns an array with objects with attribute names as keys and - * following object as values: `{name: string, id: number, acl: { readable: boolean, writable: - * boolean, reportable: boolean } }`. Note that `name` is optional based on whether the - * attribute is implemented in zigbee-clusters. + * Command which discovers the implemented attributes with access control info. */ - discoverAttributesExtended(): Promise; + discoverAttributesExtended(): Promise<{ + name?: string; + id: number; + acl: { readable: boolean; writable: boolean; reportable: boolean }; + }[]>; +} + +export interface AlarmsCluster extends ZCLNodeCluster { + resetAllAlarms(): Promise; + getAlarm(): Promise; + resetAlarmLog(): Promise; +} + +export interface AnalogInputClusterAttributes { + description?: string; + maxPresentValue?: unknown; + minPresentValue?: unknown; + outOfService?: boolean; + presentValue?: unknown; + reliability?: 'noFaultDetected' | 'noSensor' | 'overRange' | 'underRange' | 'openLoop' | 'shortedLoop' | 'noOutput' | 'unreliableOther' | 'processError' | 'configurationError'; + resolution?: unknown; + statusFlags?: Partial<{ inAlarm: boolean; fault: boolean; overridden: boolean; outOfService: boolean }>; + applicationType?: number; +} + +export interface AnalogInputCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface AnalogOutputClusterAttributes { + description?: string; + maxPresentValue?: unknown; + minPresentValue?: unknown; + outOfService?: boolean; + presentValue?: unknown; + reliability?: 'noFaultDetected' | 'overRange' | 'underRange' | 'openLoop' | 'shortedLoop' | 'unreliableOther' | 'processError' | 'configurationError'; + relinquishDefault?: unknown; + resolution?: unknown; + statusFlags?: Partial<{ inAlarm: boolean; fault: boolean; overridden: boolean; outOfService: boolean }>; + applicationType?: number; +} + +export interface AnalogOutputCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; } -interface BasicCluster extends ZCLNodeCluster { +export interface AnalogValueClusterAttributes { + description?: string; + outOfService?: boolean; + presentValue?: unknown; + reliability?: 'noFaultDetected' | 'overRange' | 'underRange' | 'openLoop' | 'shortedLoop' | 'unreliableOther' | 'processError' | 'configurationError'; + relinquishDefault?: unknown; + statusFlags?: Partial<{ inAlarm: boolean; fault: boolean; overridden: boolean; outOfService: boolean }>; + applicationType?: number; +} + +export interface AnalogValueCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface BallastConfigurationClusterAttributes { + physicalMinLevel?: number; + physicalMaxLevel?: number; + ballastStatus?: Partial<{ nonOperational: boolean; lampNotInSocket: boolean }>; + minLevel?: number; + maxLevel?: number; + powerOnLevel?: number; + powerOnFadeTime?: number; + intrinsicBallastFactor?: number; + ballastFactorAdjustment?: number; + lampQuantity?: number; + lampType?: string; + lampManufacturer?: string; + lampRatedHours?: number; + lampBurnHours?: number; + lampAlarmMode?: Partial<{ lampBurnHours: boolean }>; + lampBurnHoursTripPoint?: number; +} + +export interface BallastConfigurationCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface BasicClusterAttributes { + zclVersion?: number; + appVersion?: number; + stackVersion?: number; + hwVersion?: number; + manufacturerName?: string; + modelId?: string; + dateCode?: string; + powerSource?: unknown; + appProfileVersion?: number; + locationDesc?: string; + physicalEnv?: unknown; + deviceEnabled?: boolean; + alarmMask?: Partial<{ hardwareFault: boolean; softwareFault: boolean }>; + disableLocalConfig?: Partial<{ factoryResetDisabled: boolean; configurationDisabled: boolean }>; + swBuildId?: string; +} + +export interface BasicCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; factoryReset(): Promise; } -interface PowerConfigurationCluster extends ZCLNodeCluster {} +export interface BinaryInputClusterAttributes { + activeText?: string; + description?: string; + inactiveText?: string; + outOfService?: boolean; + polarity?: 'normal' | 'reverse'; + presentValue?: boolean; + reliability?: 'noFaultDetected' | 'noSensor' | 'overRange' | 'underRange' | 'openLoop' | 'shortedLoop' | 'noOutput' | 'unreliableOther' | 'processError' | 'configurationError'; + statusFlags?: Partial<{ inAlarm: boolean; fault: boolean; overridden: boolean; outOfService: boolean }>; + applicationType?: number; +} -interface OnOffCluster extends ZCLNodeCluster { - setOn(): Promise; +export interface BinaryInputCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface BinaryOutputClusterAttributes { + activeText?: string; + description?: string; + inactiveText?: string; + minimumOffTime?: number; + minimumOnTime?: number; + outOfService?: boolean; + polarity?: 'normal' | 'reverse'; + presentValue?: boolean; + reliability?: 'noFaultDetected' | 'overRange' | 'underRange' | 'openLoop' | 'shortedLoop' | 'unreliableOther' | 'processError' | 'configurationError'; + relinquishDefault?: boolean; + statusFlags?: Partial<{ inAlarm: boolean; fault: boolean; overridden: boolean; outOfService: boolean }>; + applicationType?: number; +} + +export interface BinaryOutputCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface BinaryValueClusterAttributes { + activeText?: string; + description?: string; + inactiveText?: string; + minimumOffTime?: number; + minimumOnTime?: number; + outOfService?: boolean; + polarity?: 'normal' | 'reverse'; + presentValue?: boolean; + reliability?: 'noFaultDetected' | 'overRange' | 'underRange' | 'openLoop' | 'shortedLoop' | 'unreliableOther' | 'processError' | 'configurationError'; + relinquishDefault?: boolean; + statusFlags?: Partial<{ inAlarm: boolean; fault: boolean; overridden: boolean; outOfService: boolean }>; + applicationType?: number; +} + +export interface BinaryValueCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface ColorControlClusterAttributes { + currentHue?: number; + currentSaturation?: number; + currentX?: number; + currentY?: number; + colorTemperatureMireds?: number; + colorMode?: 'currentHueAndCurrentSaturation' | 'currentXAndCurrentY' | 'colorTemperatureMireds'; + colorCapabilities?: Partial<{ hueAndSaturation: boolean; enhancedHue: boolean; colorLoop: boolean; xy: boolean; colorTemperature: boolean }>; + colorTempPhysicalMinMireds?: number; + colorTempPhysicalMaxMireds?: number; +} + +export interface ColorControlCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; + moveToHue(args: { hue: number; direction: 'shortestDistance' | 'longestDistance' | 'up' | 'down'; transitionTime: number }): Promise; + moveToSaturation(args: { saturation: number; transitionTime: number }): Promise; + moveToHueAndSaturation(args: { hue: number; saturation: number; transitionTime: number }): Promise; + moveToColor(args: { colorX: number; colorY: number; transitionTime: number }): Promise; + moveToColorTemperature(args: { colorTemperature: number; transitionTime: number }): Promise; +} + +export interface DehumidificationControlCluster extends ZCLNodeCluster { +} + +export interface DeviceTemperatureClusterAttributes { + currentTemperature?: number; + minTempExperienced?: number; + maxTempExperienced?: number; + overTempTotalDwell?: number; + deviceTempAlarmMask?: Partial<{ deviceTemperatureTooLow: boolean; deviceTemperatureTooHigh: boolean }>; + lowTempThreshold?: number; + highTempThreshold?: number; + lowTempDwellTripPoint?: number; + highTempDwellTripPoint?: number; +} + +export interface DeviceTemperatureCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface DiagnosticsCluster extends ZCLNodeCluster { +} + +export interface DoorLockClusterAttributes { + lockState?: 'notFullyLocked' | 'locked' | 'unlocked' | 'undefined'; + lockType?: 'deadBolt' | 'magnetic' | 'other' | 'mortise' | 'rim' | 'latchBolt' | 'cylindricalLock' | 'tubularLock' | 'interconnectedLock' | 'deadLatch' | 'doorFurniture'; + actuatorEnabled?: boolean; + doorState?: 'open' | 'closed' | 'errorJammed' | 'errorForcedOpen' | 'errorUnspecified' | 'undefined'; + doorOpenEvents?: number; + doorClosedEvents?: number; + openPeriod?: number; + numberOfLogRecordsSupported?: number; + numberOfTotalUsersSupported?: number; + numberOfPINUsersSupported?: number; + numberOfRFIDUsersSupported?: number; + numberOfWeekDaySchedulesSupportedPerUser?: number; + numberOfYearDaySchedulesSupportedPerUser?: number; + numberOfHolidaySchedulesSupported?: number; + maxPINCodeLength?: number; + minPINCodeLength?: number; + maxRFIDCodeLength?: number; + minRFIDCodeLength?: number; + enableLogging?: boolean; + language?: string; + ledSettings?: number; + autoRelockTime?: number; + soundVolume?: number; + operatingMode?: 'normal' | 'vacation' | 'privacy' | 'noRFLockOrUnlock' | 'passage'; + supportedOperatingModes?: Partial<{ normal: boolean; vacation: boolean; privacy: boolean; noRFLockOrUnlock: boolean; passage: boolean }>; + defaultConfigurationRegister?: Partial<{ enableLocalProgramming: boolean; keypadInterfaceDefaultAccess: boolean; rfInterfaceDefaultAccess: boolean; reserved3: boolean; reserved4: boolean; soundEnabled: boolean; autoRelockTimeSet: boolean; ledSettingsSet: boolean }>; + enableLocalProgramming?: boolean; + enableOneTouchLocking?: boolean; + enableInsideStatusLED?: boolean; + enablePrivacyModeButton?: boolean; + wrongCodeEntryLimit?: number; + userCodeTemporaryDisableTime?: number; + sendPINOverTheAir?: boolean; + requirePINforRFOperation?: boolean; + securityLevel?: 'network' | 'apsLinkKey'; + alarmMask?: Partial<{ deadboltJammed: boolean; lockResetToFactoryDefaults: boolean; reserved2: boolean; rfModulePowerCycled: boolean; tamperAlarmWrongCodeEntryLimit: boolean; tamperAlarmFrontEscutcheonRemoved: boolean; forcedDoorOpenUnderDoorLockedCondition: boolean }>; + keypadOperationEventMask?: Partial<{ unknownOrManufacturerSpecificKeypadOperationEvent: boolean; lockSourceKeypad: boolean; unlockSourceKeypad: boolean; lockSourceKeypadErrorInvalidPIN: boolean; lockSourceKeypadErrorInvalidSchedule: boolean; unlockSourceKeypadErrorInvalidCode: boolean; unlockSourceKeypadErrorInvalidSchedule: boolean; nonAccessUserOperationEventSourceKeypad: boolean }>; + rfOperationEventMask?: Partial<{ unknownOrManufacturerSpecificKeypadOperationEvent: boolean; lockSourceRF: boolean; unlockSourceRF: boolean; lockSourceRFErrorInvalidCode: boolean; lockSourceRFErrorInvalidSchedule: boolean; unlockSourceRFErrorInvalidCode: boolean; unlockSourceRFErrorInvalidSchedule: boolean }>; + manualOperationEventMask?: Partial<{ unknownOrManufacturerSpecificManualOperationEvent: boolean; thumbturnLock: boolean; thumbturnUnlock: boolean; oneTouchLock: boolean; keyLock: boolean; keyUnlock: boolean; autoLock: boolean; scheduleLock: boolean; scheduleUnlock: boolean; manualLock: boolean; manualUnlock: boolean }>; + rfidOperationEventMask?: Partial<{ unknownOrManufacturerSpecificKeypadOperationEvent: boolean; lockSourceRFID: boolean; unlockSourceRFID: boolean; lockSourceRFIDErrorInvalidRFIDID: boolean; lockSourceRFIDErrorInvalidSchedule: boolean; unlockSourceRFIDErrorInvalidRFIDID: boolean; unlockSourceRFIDErrorInvalidSchedule: boolean }>; + keypadProgrammingEventMask?: Partial<{ unknownOrManufacturerSpecificKeypadProgrammingEvent: boolean; masterCodeChanged: boolean; pinCodeAdded: boolean; pinCodeDeleted: boolean; pinCodeChanged: boolean }>; + rfProgrammingEventMask?: Partial<{ unknownOrManufacturerSpecificRFProgrammingEvent: boolean; reserved1: boolean; pinCodeAdded: boolean; pinCodeDeleted: boolean; pinCodeChanged: boolean; rfidCodeAdded: boolean; rfidCodeDeleted: boolean }>; + rfidProgrammingEventMask?: Partial<{ unknownOrManufacturerSpecificRFIDProgrammingEvent: boolean; rfidCodeAdded: boolean; rfidCodeDeleted: boolean }>; +} + +export interface DoorLockCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; + lockDoor(args: { pinCode?: Buffer }): Promise; + unlockDoor(args: { pinCode?: Buffer }): Promise; + toggle(args: { pinCode?: Buffer }): Promise; + unlockWithTimeout(args: { timeout: number; pinCode?: Buffer }): Promise; + getLogRecord(args: { logIndex: number }): Promise; + setPINCode(args: { userId: number; userStatus: 'available' | 'occupiedEnabled' | 'occupiedDisabled' | 'notSupported'; userType: 'unrestricted' | 'yearDayScheduleUser' | 'weekDayScheduleUser' | 'masterUser' | 'nonAccessUser' | 'notSupported'; pinCode?: Buffer }): Promise; + getPINCode(args: { userId: number }): Promise; + clearPINCode(args: { userId: number }): Promise; + clearAllPINCodes(): Promise; + setUserStatus(args: { userId: number; userStatus: 'available' | 'occupiedEnabled' | 'occupiedDisabled' | 'notSupported' }): Promise; + getUserStatus(args: { userId: number }): Promise; + setWeekDaySchedule(args: { scheduleId: number; userId: number; daysMask: Partial<{ sunday: boolean; monday: boolean; tuesday: boolean; wednesday: boolean; thursday: boolean; friday: boolean; saturday: boolean }>; startHour: number; startMinute: number; endHour: number; endMinute: number }): Promise; + getWeekDaySchedule(args: { scheduleId: number; userId: number }): Promise; + clearWeekDaySchedule(args: { scheduleId: number; userId: number }): Promise; + setYearDaySchedule(args: { scheduleId: number; userId: number; localStartTime: number; localEndTime: number }): Promise; + getYearDaySchedule(args: { scheduleId: number; userId: number }): Promise; + clearYearDaySchedule(args: { scheduleId: number; userId: number }): Promise; + setHolidaySchedule(args: { holidayScheduleId: number; localStartTime: number; localEndTime: number; operatingModeDuringHoliday: 'normal' | 'vacation' | 'privacy' | 'noRFLockOrUnlock' | 'passage' }): Promise; + getHolidaySchedule(args: { holidayScheduleId: number }): Promise; + clearHolidaySchedule(args: { holidayScheduleId: number }): Promise; + setUserType(args: { userId: number; userType: 'unrestricted' | 'yearDayScheduleUser' | 'weekDayScheduleUser' | 'masterUser' | 'nonAccessUser' | 'notSupported' }): Promise; + getUserType(args: { userId: number }): Promise; + setRFIDCode(args: { userId: number; userStatus: 'available' | 'occupiedEnabled' | 'occupiedDisabled' | 'notSupported'; userType: 'unrestricted' | 'yearDayScheduleUser' | 'weekDayScheduleUser' | 'masterUser' | 'nonAccessUser' | 'notSupported'; rfidCode?: Buffer }): Promise; + getRFIDCode(args: { userId: number }): Promise; + clearRFIDCode(args: { userId: number }): Promise; + clearAllRFIDCodes(): Promise; +} + +export interface ElectricalMeasurementClusterAttributes { + measurementType?: Partial<{ activeMeasurementAC: boolean; reactiveMeasurementAC: boolean; apparentMeasurementAC: boolean; phaseAMeasurement: boolean; phaseBMeasurement: boolean; phaseCMeasurement: boolean; dcMeasurement: boolean; harmonicsMeasurement: boolean; powerQualityMeasurement: boolean }>; + acFrequency?: number; + measuredPhase1stHarmonicCurrent?: number; + acFrequencyMultiplier?: number; + acFrequencyDivisor?: number; + phaseHarmonicCurrentMultiplier?: number; + rmsVoltage?: number; + rmsCurrent?: number; + activePower?: number; + reactivePower?: number; + acVoltageMultiplier?: number; + acVoltageDivisor?: number; + acCurrentMultiplier?: number; + acCurrentDivisor?: number; + acPowerMultiplier?: number; + acPowerDivisor?: number; + acAlarmsMask?: Partial<{ voltageOverload: boolean; currentOverload: boolean; activePowerOverload: boolean; reactivePowerOverload: boolean; averageRMSOverVoltage: boolean; averageRMSUnderVoltage: boolean; rmsExtremeOverVoltage: boolean; rmsExtremeUnderVoltage: boolean; rmsVoltageSag: boolean; rmsVoltageSwell: boolean }>; + acVoltageOverload?: number; + acCurrentOverload?: number; + acActivePowerOverload?: number; +} + +export interface ElectricalMeasurementCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface FanControlCluster extends ZCLNodeCluster { +} + +export interface FlowMeasurementClusterAttributes { + measuredValue?: number; + minMeasuredValue?: number; + maxMeasuredValue?: number; + tolerance?: number; +} + +export interface FlowMeasurementCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface GroupsClusterAttributes { + nameSupport?: Partial<{ groupNames: boolean }>; +} + +export interface GroupsCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; + addGroup(args: { groupId: number; groupName: string }): Promise; + viewGroup(args: { groupId: number }): Promise; + getGroupMembership(args: { groupIds: number }): Promise; + removeGroup(args: { groupId: number }): Promise; + removeAllGroups(): Promise; + addGroupIfIdentify(args: { groupId: number; groupName: string }): Promise; +} + +export interface IasACECluster extends ZCLNodeCluster { +} + +export interface IasWDCluster extends ZCLNodeCluster { +} + +export interface IasZoneClusterAttributes { + zoneState?: 'notEnrolled' | 'enrolled'; + zoneType?: 'standardCIE' | 'motionSensor' | 'contactSwitch' | 'fireSensor' | 'waterSensor' | 'cabonMonoxideSensor' | 'personalEmergencyDevice' | 'vibrationMovementSensor' | 'remoteControl' | 'keyfob' | 'keypad' | 'standardWarningDevice' | 'glassBreakSensor' | 'securityRepeater' | 'invalidZoneType'; + zoneStatus?: unknown; + iasCIEAddress?: string; + zoneId?: number; +} + +export interface IasZoneCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; + zoneStatusChangeNotification(args: { zoneStatus: unknown; extendedStatus: number; zoneId: number; delay: number }): Promise; + zoneEnrollResponse(args: { enrollResponseCode: 'success' | 'notSupported' | 'noEnrollPermit' | 'tooManyZones'; zoneId: number }): Promise; + zoneEnrollRequest(args: { zoneType: 'standard' | 'motionSensor' | 'contactSwitch' | 'fireSensor' | 'waterSensor' | 'carbonMonoxideSensor' | 'personalEmergencyDevice' | 'vibrationMovementSensor' | 'remoteControl' | 'keyFob' | 'keyPad' | 'standardWarningDevice' | 'glassBreakSensor' | 'securityRepeater' | 'invalid'; manufacturerCode: number }): Promise; + initiateNormalOperationMode(): Promise; +} + +export interface IdentifyClusterAttributes { + identifyTime?: number; +} + +export interface IdentifyCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; + identify(args: { identifyTime: number }): Promise; + identifyQuery(args: { timeout: number }): Promise; + triggerEffect(args: { effectIdentifier: 'blink' | 'breathe' | 'okay' | 'channelChange' | 'finish' | 'stop'; effectVariant: number }): Promise; +} + +export interface IlluminanceLevelSensingClusterAttributes { + levelStatus?: 'illuminanceOnTarget' | 'illuminanceBelowTarget' | 'illuminanceAboveTarget'; + lightSensorType?: 'photodiode' | 'cmos' | 'unknown'; + illuminanceTargetLevel?: number; +} + +export interface IlluminanceLevelSensingCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface IlluminanceMeasurementClusterAttributes { + measuredValue?: number; + minMeasuredValue?: number; + maxMeasuredValue?: number; + tolerance?: number; + lightSensorType?: 'photodiode' | 'cmos' | 'unknown'; +} + +export interface IlluminanceMeasurementCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface LevelControlClusterAttributes { + currentLevel?: number; + remainingTime?: number; + onOffTransitionTime?: number; + onLevel?: number; + onTransitionTime?: number; + offTransitionTime?: number; + defaultMoveRate?: number; +} + +export interface LevelControlCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; + moveToLevel(args: { level: number; transitionTime: number }): Promise; + move(args: { moveMode: 'up' | 'down'; rate: number }): Promise; + step(args: { mode: 'up' | 'down'; stepSize: number; transitionTime: number }): Promise; + stop(): Promise; + moveToLevelWithOnOff(args: { level: number; transitionTime: number }): Promise; + moveWithOnOff(args: { moveMode: 'up' | 'down'; rate: number }): Promise; + stepWithOnOff(args: { mode: 'up' | 'down'; stepSize: number; transitionTime: number }): Promise; + stopWithOnOff(): Promise; +} + +export interface MeteringClusterAttributes { + currentSummationDelivered?: number; + currentSummationReceived?: number; + currentMaxDemandDelivered?: number; + currentMaxDemandReceived?: number; + dftSummation?: number; + dailyFreezeTime?: number; + powerFactor?: number; + readingSnapShotTime?: number; + currentMaxDemandDeliveredTime?: number; + currentMaxDemandReceivedTime?: number; + defaultUpdatePeriod?: number; + fastPollUpdatePeriod?: number; + currentBlockPeriodConsumptionDelivered?: number; + dailyConsumptionTarget?: number; + currentBlock?: unknown; + profileIntervalPeriod?: unknown; + currentTier1SummationDelivered?: number; + currentTier1SummationReceived?: number; + currentTier2SummationDelivered?: number; + currentTier2SummationReceived?: number; + currentTier3SummationDelivered?: number; + currentTier3SummationReceived?: number; + currentTier4SummationDelivered?: number; + currentTier4SummationReceived?: number; + status?: unknown; + remainingBatteryLife?: number; + hoursInOperation?: number; + hoursInFault?: number; + extendedStatus?: unknown; + unitOfMeasure?: unknown; + multiplier?: number; + divisor?: number; + summationFormatting?: unknown; + demandFormatting?: unknown; + historicalConsumptionFormatting?: unknown; + meteringDeviceType?: unknown; + siteId?: Buffer; + meterSerialNumber?: Buffer; + energyCarrierUnitOfMeasure?: unknown; + energyCarrierSummationFormatting?: unknown; + energyCarrierDemandFormatting?: unknown; + temperatureUnitOfMeasure?: unknown; + temperatureFormatting?: unknown; + moduleSerialNumber?: Buffer; + operatingTariffLabelDelivered?: Buffer; + operatingTariffLabelReceived?: Buffer; + customerIdNumber?: Buffer; + alternativeUnitOfMeasure?: unknown; + alternativeDemandFormatting?: unknown; + alternativeConsumptionFormatting?: unknown; + instantaneousDemand?: number; + currentDayConsumptionDelivered?: number; + currentDayConsumptionReceived?: number; + previousDayConsumptionDelivered?: number; + previousDayConsumptionReceived?: number; + currentPartialProfileIntervalStartTimeDelivered?: number; + currentPartialProfileIntervalStartTimeReceived?: number; + currentPartialProfileIntervalValueDelivered?: number; + currentPartialProfileIntervalValueReceived?: number; + currentDayMaxPressure?: number; + currentDayMinPressure?: number; + previousDayMaxPressure?: number; + previousDayMinPressure?: number; + currentDayMaxDemand?: number; + previousDayMaxDemand?: number; + currentMonthMaxDemand?: number; + currentYearMaxDemand?: number; + currentDayMaxEnergyCarrierDemand?: number; + previousDayMaxEnergyCarrierDemand?: number; + currentMonthMaxEnergyCarrierDemand?: number; + currentMonthMinEnergyCarrierDemand?: number; + currentYearMaxEnergyCarrierDemand?: number; + currentYearMinEnergyCarrierDemand?: number; + maxNumberOfPeriodsDelivered?: number; + currentDemandDelivered?: number; + demandLimit?: number; + demandIntegrationPeriod?: number; + numberOfDemandSubintervals?: number; + demandLimitArmDuration?: number; +} + +export interface MeteringCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface MultistateInputClusterAttributes { + description?: string; + numberOfStates?: number; + outOfService?: boolean; + presentValue?: number; + reliability?: 'noFaultDetected' | 'noSensor' | 'overRange' | 'underRange' | 'openLoop' | 'shortedLoop' | 'noOutput' | 'unreliableOther' | 'processError' | 'multiStateFault' | 'configurationError'; + statusFlags?: Partial<{ inAlarm: boolean; fault: boolean; overridden: boolean; outOfService: boolean }>; + applicationType?: number; +} + +export interface MultistateInputCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface MultistateOutputClusterAttributes { + description?: string; + numberOfStates?: number; + outOfService?: boolean; + presentValue?: number; + reliability?: 'noFaultDetected' | 'overRange' | 'underRange' | 'openLoop' | 'shortedLoop' | 'unreliableOther' | 'processError' | 'multiStateFault' | 'configurationError'; + relinquishDefault?: number; + statusFlags?: Partial<{ inAlarm: boolean; fault: boolean; overridden: boolean; outOfService: boolean }>; + applicationType?: number; +} + +export interface MultistateOutputCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface MultistateValueClusterAttributes { + description?: string; + numberOfStates?: number; + outOfService?: boolean; + presentValue?: number; + reliability?: 'noFaultDetected' | 'noSensor' | 'overRange' | 'underRange' | 'openLoop' | 'shortedLoop' | 'noOutput' | 'unreliableOther' | 'processError' | 'multiStateFault' | 'configurationError'; + relinquishDefault?: number; + statusFlags?: Partial<{ inAlarm: boolean; fault: boolean; overridden: boolean; outOfService: boolean }>; + applicationType?: number; +} + +export interface MultistateValueCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface OccupancySensingClusterAttributes { + occupancy?: Partial<{ occupied: boolean }>; + occupancySensorType?: 'pir' | 'ultrasonic' | 'pirAndUltrasonic' | 'physicalContact'; + occupancySensorTypeBitmap?: Partial<{ pir: boolean; ultrasonic: boolean; physicalContact: boolean }>; + pirOccupiedToUnoccupiedDelay?: number; + pirUnoccupiedToOccupiedDelay?: number; + pirUnoccupiedToOccupiedThreshold?: number; + ultrasonicOccupiedToUnoccupiedDelay?: number; + ultrasonicUnoccupiedToOccupiedDelay?: number; + ultrasonicUnoccupiedToOccupiedThreshold?: number; + physicalContactOccupiedToUnoccupiedDelay?: number; + physicalContactUnoccupiedToOccupiedDelay?: number; + physicalContactUnoccupiedToOccupiedThreshold?: number; +} + +export interface OccupancySensingCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface OnOffClusterAttributes { + onOff?: boolean; + onTime?: number; + offWaitTime?: number; +} + +export interface OnOffCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; setOff(): Promise; + setOn(): Promise; toggle(): Promise; - offWithEffect({ - effectIdentifier, - effectVariant, - }: { - effectIdentifier: number; - effectVariant: number; - }): Promise; + offWithEffect(args: { effectIdentifier: number; effectVariant: number }): Promise; onWithRecallGlobalScene(): Promise; - onWithTimedOff({ - onOffControl, - onTime, - offWaitTime, - }: { - onOffControl: number; - onTime: number; - offWaitTime: number; - }): Promise; -} - -interface LevelControlCluster extends ZCLNodeCluster { - moveToLevel({ level, transitionTime }: { level: number; transitionTime: number }): Promise; - move({ moveMode, rate }: { moveMode: "up" | "down"; rate: number }): Promise; - step({ - moveMode, - stepSize, - transitionTime, - }: { - moveMode: "up" | "down"; - stepSize: number; - transitionTime: number; - }): Promise; - moveToLevelWithOnOff({ - level, - transitionTime, - }: { - level: number; - transitionTime: number; - }): Promise; - moveWithOnOff({ moveMode, rate }: { moveMode: "up" | "down"; rate: number }): Promise; - stepWithOnOff({ - moveMode, - stepSize, - transitionTime, - }: { - moveMode: "up" | "down"; - stepSize: number; - transitionTime: number; - }): Promise; - stopWithOnOff(): Promise; + onWithTimedOff(args: { onOffControl: number; onTime: number; offWaitTime: number }): Promise; +} + +export interface OnOffSwitchCluster extends ZCLNodeCluster { +} + +export interface OtaCluster extends ZCLNodeCluster { } -interface ColorControlCluster extends ZCLNodeCluster { - moveToHue({ - hue, - direction, - transitionTime, - }: { - hue: number; - direction: "shortestDistance" | "longestDistance" | "up" | "down"; - transitionTime: number; - }): Promise; - moveToSaturation({ - saturation, - transitionTime, - }: { - saturation: number; - transitionTime: number; - }): Promise; - moveToHueAndSaturation({ - hue, - saturation, - transitionTime, - }: { - hue: number; - saturation: number; - transitionTime: number; - }): Promise; - moveToColor({ - colorX, - colorY, - transitionTime, - }: { - colorX: number; - colorY: number; - transitionTime: number; - }): Promise; - moveToColorTemperature({ - colorTemperature, - transitionTime, - }: { - colorTemperature: number; - transitionTime: number; - }): Promise; -} - -interface MeteringCluster extends ZCLNodeCluster {} - -interface ElectricalMeasurementCluster extends ZCLNodeCluster {} - -interface PollControlCluster extends ZCLNodeCluster { +export interface PollControlClusterAttributes { + checkInInterval?: number; + longPollInterval?: number; + shortPollInterval?: number; + fastPollTimeout?: number; + checkInIntervalMin?: number; + longPollIntervalMin?: number; + fastPollTimeoutMax?: number; +} + +export interface PollControlCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; fastPollStop(): Promise; - setLongPollInterval(opts: { newLongPollInterval: number }): Promise; - setShortPollInterval(opts: { newShortPollInterval: number }): Promise; + setLongPollInterval(args: { newLongPollInterval: number }): Promise; + setShortPollInterval(args: { newShortPollInterval: number }): Promise; +} + +export interface PowerConfigurationClusterAttributes { + batteryVoltage?: number; + batteryPercentageRemaining?: number; + batterySize?: 'noBattery' | 'builtIn' | 'other' | 'AA' | 'AAA' | 'C' | 'D' | 'CR2' | 'CR123A' | 'unknown'; + batteryQuantity?: number; + batteryRatedVoltage?: number; + batteryVoltageMinThreshold?: number; + batteryAlarmState?: Partial<{ batteryThresholdBatterySource1: boolean; batteryThreshold1BatterySource1: boolean; batteryThreshold2BatterySource1: boolean; batteryThreshold3BatterySource1: boolean; reserved4: boolean; reserved5: boolean; reserved6: boolean; reserved7: boolean; reserved8: boolean; reserved9: boolean; batteryThresholdBatterySource2: boolean; batteryThreshold1BatterySource2: boolean; batteryThreshold2BatterySource2: boolean; batteryThreshold3BatterySource2: boolean; reserved14: boolean; reserved15: boolean; reserved16: boolean; reserved17: boolean; reserved18: boolean; reserved19: boolean; batteryThresholdBatterySource3: boolean; batteryThreshold1BatterySource3: boolean; batteryThreshold2BatterySource3: boolean; batteryThreshold3BatterySource3: boolean; reserved24: boolean; reserved25: boolean; reserved26: boolean; reserved27: boolean; reserved28: boolean; reserved29: boolean; mainsPowerSupplyLostUnavailable: boolean }>; +} + +export interface PowerConfigurationCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; } -type ZCLNodeEndpoint = { - clusters: { - onOff?: OnOffCluster; - levelControl?: LevelControlCluster; - colorControl?: ColorControlCluster; +export interface PowerProfileCluster extends ZCLNodeCluster { +} + +export interface PressureMeasurementClusterAttributes { + measuredValue?: number; + minMeasuredValue?: number; + maxMeasuredValue?: number; + tolerance?: number; + scaledValue?: number; + minScaledValue?: number; + maxScaledValue?: number; + scaledTolerance?: number; + scale?: number; +} + +export interface PressureMeasurementCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface PumpConfigurationAndControlCluster extends ZCLNodeCluster { +} + +export interface RelativeHumidityClusterAttributes { + measuredValue?: number; + minMeasuredValue?: number; + maxMeasuredValue?: number; + tolerance?: number; +} + +export interface RelativeHumidityCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface ScenesCluster extends ZCLNodeCluster { +} + +export interface ShadeConfigurationCluster extends ZCLNodeCluster { +} + +export interface TemperatureMeasurementClusterAttributes { + measuredValue?: number; + minMeasuredValue?: number; + maxMeasuredValue?: number; +} + +export interface TemperatureMeasurementCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; +} + +export interface ThermostatClusterAttributes { + localTemperature?: number; + outdoorTemperature?: number; + occupancy?: Partial<{ occupied: boolean }>; + absMinHeatSetpointLimit?: number; + absMaxHeatSetpointLimit?: number; + absMinCoolSetpointLimit?: number; + absMaxCoolSetpointLimit?: number; + pICoolingDemand?: number; + pIHeatingDemand?: number; + localTemperatureCalibration?: number; + occupiedCoolingSetpoint?: number; + occupiedHeatingSetpoint?: number; + unoccupiedCoolingSetpoint?: number; + unoccupiedHeatingSetpoint?: number; + minHeatSetpointLimit?: number; + maxHeatSetpointLimit?: number; + minCoolSetpointLimit?: number; + maxCoolSetpointLimit?: number; + minSetpointDeadBand?: number; + remoteSensing?: Partial<{ localTemperature: boolean; outdoorTemperature: boolean; occupancy: boolean }>; + controlSequenceOfOperation?: 'cooling' | 'coolingWithReheat' | 'heating' | 'heatingWithReheat' | 'coolingAndHeating4Pipes' | 'coolingAndHeating4PipesWithReheat'; + systemMode?: 'off' | 'auto' | 'cool' | 'heat' | 'emergencyHeating' | 'precooling' | 'fanOnly' | 'dry' | 'sleep'; + alarmMask?: Partial<{ initializationFailure: boolean; hardwareFailure: boolean; selfCalibrationFailure: boolean }>; +} + +export interface ThermostatCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; + setSetpoint(args: { mode: 'heat' | 'cool' | 'both'; amount: number }): Promise; +} + +export interface TimeCluster extends ZCLNodeCluster { +} + +export interface TouchlinkCluster extends ZCLNodeCluster { + getGroups(args: { startIdx: number }): Promise; +} + +export interface WindowCoveringClusterAttributes { + windowCoveringType?: 'rollershade' | 'rollershade2Motor' | 'rollershadeExterior' | 'rollershadeExterior2Motor' | 'drapery' | 'awning' | 'shutter' | 'tiltBlindTiltOnly' | 'tiltBlindLiftAndTilt' | 'projectorScreen'; + physicalClosedLimitLift?: number; + physicalClosedLimitTilt?: number; + currentPositionLift?: number; + currentPositionTilt?: number; + numberofActuationsLift?: number; + numberofActuationsTilt?: number; + configStatus?: Partial<{ operational: boolean; online: boolean; reversalLiftCommands: boolean; controlLift: boolean; controlTilt: boolean; encoderLift: boolean; encoderTilt: boolean; reserved: boolean }>; + currentPositionLiftPercentage?: number; + currentPositionTiltPercentage?: number; + installedOpenLimitLift?: number; + installedClosedLimitLift?: number; + installedOpenLimitTilt?: number; + installedClosedLimitTilt?: number; + velocityLift?: number; + accelerationTimeLift?: number; + decelerationTimeLift?: number; + mode?: Partial<{ motorDirectionReversed: boolean; calibrationMode: boolean; maintenanceMode: boolean; ledFeedback: boolean }>; + intermediateSetpointsLift?: Buffer; + intermediateSetpointsTilt?: Buffer; +} + +export interface WindowCoveringCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): Promise; + upOpen(): Promise; + downClose(): Promise; + stop(): Promise; + goToLiftValue(args: { liftValue: number }): Promise; + goToLiftPercentage(args: { percentageLiftValue: number }): Promise; + goToTiltValue(args: { tiltValue: number }): Promise; + goToTiltPercentage(args: { percentageTiltValue: number }): Promise; +} + +/** Type-safe cluster registry */ +export interface ClusterRegistry { + alarms?: AlarmsCluster; + analogInput?: AnalogInputCluster; + analogOutput?: AnalogOutputCluster; + analogValue?: AnalogValueCluster; + ballastConfiguration?: BallastConfigurationCluster; + basic?: BasicCluster; + binaryInput?: BinaryInputCluster; + binaryOutput?: BinaryOutputCluster; + binaryValue?: BinaryValueCluster; + colorControl?: ColorControlCluster; + dehumidificationControl?: DehumidificationControlCluster; + deviceTemperature?: DeviceTemperatureCluster; + diagnostics?: DiagnosticsCluster; + doorLock?: DoorLockCluster; + electricalMeasurement?: ElectricalMeasurementCluster; + fanControl?: FanControlCluster; + flowMeasurement?: FlowMeasurementCluster; + groups?: GroupsCluster; + iasACE?: IasACECluster; + iasWD?: IasWDCluster; + iasZone?: IasZoneCluster; + identify?: IdentifyCluster; + illuminanceLevelSensing?: IlluminanceLevelSensingCluster; + illuminanceMeasurement?: IlluminanceMeasurementCluster; + levelControl?: LevelControlCluster; + metering?: MeteringCluster; + multistateInput?: MultistateInputCluster; + multistateOutput?: MultistateOutputCluster; + multistateValue?: MultistateValueCluster; + occupancySensing?: OccupancySensingCluster; + onOff?: OnOffCluster; + onOffSwitch?: OnOffSwitchCluster; + ota?: OtaCluster; + pollControl?: PollControlCluster; + powerConfiguration?: PowerConfigurationCluster; + powerProfile?: PowerProfileCluster; + pressureMeasurement?: PressureMeasurementCluster; + pumpConfigurationAndControl?: PumpConfigurationAndControlCluster; + relativeHumidity?: RelativeHumidityCluster; + scenes?: ScenesCluster; + shadeConfiguration?: ShadeConfigurationCluster; + temperatureMeasurement?: TemperatureMeasurementCluster; + thermostat?: ThermostatCluster; + time?: TimeCluster; + touchlink?: TouchlinkCluster; + windowCovering?: WindowCoveringCluster; +} + +export type ZCLNodeEndpoint = { + clusters: ClusterRegistry & { [clusterName: string]: ZCLNodeCluster | undefined; }; }; -interface ZCLNode { +export interface ZCLNode { endpoints: { [endpointId: number | string]: ZCLNodeEndpoint }; handleFrame: ( endpointId: number, @@ -308,16 +904,59 @@ interface ZCLNode { } declare module "zigbee-clusters" { - export var ZCLNode: { + export const ZCLNode: { new (options: ConstructorOptions): ZCLNode; }; export const CLUSTER: { - [key: string]: { ID: number; NAME: string; ATTRIBUTES: any; COMMANDS: any }; + [key: string]: { ID: number; NAME: string; ATTRIBUTES: unknown; COMMANDS: unknown }; }; - export var ZCLNodeCluster; - export var BasicCluster; - export var OnOffCluster; - export var LevelControlCluster; - export var ColorControlCluster; - export var PowerConfigurationCluster; + export { ZCLNodeCluster }; + export const AlarmsCluster: AlarmsCluster; + export const AnalogInputCluster: AnalogInputCluster; + export const AnalogOutputCluster: AnalogOutputCluster; + export const AnalogValueCluster: AnalogValueCluster; + export const BallastConfigurationCluster: BallastConfigurationCluster; + export const BasicCluster: BasicCluster; + export const BinaryInputCluster: BinaryInputCluster; + export const BinaryOutputCluster: BinaryOutputCluster; + export const BinaryValueCluster: BinaryValueCluster; + export const ColorControlCluster: ColorControlCluster; + export const DehumidificationControlCluster: DehumidificationControlCluster; + export const DeviceTemperatureCluster: DeviceTemperatureCluster; + export const DiagnosticsCluster: DiagnosticsCluster; + export const DoorLockCluster: DoorLockCluster; + export const ElectricalMeasurementCluster: ElectricalMeasurementCluster; + export const FanControlCluster: FanControlCluster; + export const FlowMeasurementCluster: FlowMeasurementCluster; + export const GroupsCluster: GroupsCluster; + export const IasACECluster: IasACECluster; + export const IasWDCluster: IasWDCluster; + export const IasZoneCluster: IasZoneCluster; + export const IdentifyCluster: IdentifyCluster; + export const IlluminanceLevelSensingCluster: IlluminanceLevelSensingCluster; + export const IlluminanceMeasurementCluster: IlluminanceMeasurementCluster; + export const LevelControlCluster: LevelControlCluster; + export const MeteringCluster: MeteringCluster; + export const MultistateInputCluster: MultistateInputCluster; + export const MultistateOutputCluster: MultistateOutputCluster; + export const MultistateValueCluster: MultistateValueCluster; + export const OccupancySensingCluster: OccupancySensingCluster; + export const OnOffCluster: OnOffCluster; + export const OnOffSwitchCluster: OnOffSwitchCluster; + export const OtaCluster: OtaCluster; + export const PollControlCluster: PollControlCluster; + export const PowerConfigurationCluster: PowerConfigurationCluster; + export const PowerProfileCluster: PowerProfileCluster; + export const PressureMeasurementCluster: PressureMeasurementCluster; + export const PumpConfigurationAndControlCluster: PumpConfigurationAndControlCluster; + export const RelativeHumidityCluster: RelativeHumidityCluster; + export const ScenesCluster: ScenesCluster; + export const ShadeConfigurationCluster: ShadeConfigurationCluster; + export const TemperatureMeasurementCluster: TemperatureMeasurementCluster; + export const ThermostatCluster: ThermostatCluster; + export const TimeCluster: TimeCluster; + export const TouchlinkCluster: TouchlinkCluster; + export const WindowCoveringCluster: WindowCoveringCluster; } + +export { ZCLNode, ZCLNodeCluster, ZCLNodeEndpoint, ClusterRegistry }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2aa2e83..83fe723 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ }, "devDependencies": { "@athombv/jsdoc-template": "^1.6.1", + "@types/node": "^25.0.10", "@types/sinon": "^17.0.3", "concurrently": "^5.2.0", "eslint": "^6.8.0", @@ -23,6 +24,7 @@ "mocha": "^10.1.0", "serve": "^14.0.1", "sinon": "^19.0.2", + "typescript": "^5.9.3", "watch": "^1.0.2" }, "engines": { @@ -429,13 +431,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.5.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.1.tgz", - "integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==", + "version": "25.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", + "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~7.16.0" } }, "node_modules/@types/sinon": { @@ -5533,6 +5535,20 @@ "node": ">=8" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", @@ -5546,11 +5562,11 @@ "dev": true }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, - "peer": true + "license": "MIT" }, "node_modules/update-browserslist-db": { "version": "1.1.0", @@ -6330,13 +6346,12 @@ "dev": true }, "@types/node": { - "version": "22.5.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.1.tgz", - "integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==", + "version": "25.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", + "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", "dev": true, - "peer": true, "requires": { - "undici-types": "~6.19.2" + "undici-types": "~7.16.0" } }, "@types/sinon": { @@ -10189,6 +10204,12 @@ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true }, + "typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true + }, "uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", @@ -10202,11 +10223,10 @@ "dev": true }, "undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "peer": true + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true }, "update-browserslist-db": { "version": "1.1.0", diff --git a/package.json b/package.json index acfd269..55550b0 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "scripts": { "test": "mocha --reporter list", "lint": "eslint .", + "generate-types": "node scripts/generate-types.js", "serve": "concurrently \"serve build/\" \"npm run build:watch\"", "build": "jsdoc --configure ./docs/jsdoc.json", "build:clean": "rm -rf ./build", @@ -32,6 +33,7 @@ "homepage": "https://github.com/athombv/node-zigbee-clusters#readme", "devDependencies": { "@athombv/jsdoc-template": "^1.6.1", + "@types/node": "^25.0.10", "@types/sinon": "^17.0.3", "concurrently": "^5.2.0", "eslint": "^6.8.0", @@ -41,6 +43,7 @@ "mocha": "^10.1.0", "serve": "^14.0.1", "sinon": "^19.0.2", + "typescript": "^5.9.3", "watch": "^1.0.2" }, "dependencies": { diff --git a/scripts/generate-types.js b/scripts/generate-types.js new file mode 100644 index 0000000..6bef031 --- /dev/null +++ b/scripts/generate-types.js @@ -0,0 +1,573 @@ +'use strict'; + +/* eslint-disable no-console, no-use-before-define */ + +/** + * Type generation script for zigbee-clusters + * Parses cluster definitions and generates TypeScript interfaces + */ + +const fs = require('fs'); +const path = require('path'); + +const CLUSTERS_DIR = path.join(__dirname, '../lib/clusters'); +const OUTPUT_FILE = path.join(__dirname, '../index.d.ts'); + +// Files to skip (not actual cluster definitions) +const SKIP_FILES = ['index.js']; + +/** + * Convert ZCLDataType to TypeScript type string + */ +function zclTypeToTS(typeStr) { + // Handle simple types - check these first (more specific matches) + if (typeStr.includes('ZCLDataTypes.bool')) return 'boolean'; + if (typeStr.includes('ZCLDataTypes.uint48')) return 'number'; + if (typeStr.includes('ZCLDataTypes.uint40')) return 'number'; + if (typeStr.includes('ZCLDataTypes.uint32')) return 'number'; + if (typeStr.includes('ZCLDataTypes.uint24')) return 'number'; + if (typeStr.includes('ZCLDataTypes.uint16')) return 'number'; + if (typeStr.includes('ZCLDataTypes.uint8')) return 'number'; + if (typeStr.includes('ZCLDataTypes.int32')) return 'number'; + if (typeStr.includes('ZCLDataTypes.int24')) return 'number'; + if (typeStr.includes('ZCLDataTypes.int16')) return 'number'; + if (typeStr.includes('ZCLDataTypes.int8')) return 'number'; + if (typeStr.includes('ZCLDataTypes.string')) return 'string'; + if (typeStr.includes('ZCLDataTypes.octstr')) return 'Buffer'; + if (typeStr.includes('ZCLDataTypes.data32')) return 'number'; + if (typeStr.includes('ZCLDataTypes.data24')) return 'number'; + if (typeStr.includes('ZCLDataTypes.data16')) return 'number'; + if (typeStr.includes('ZCLDataTypes.data8')) return 'number'; + if (typeStr.includes('ZCLDataTypes.EUI64')) return 'string'; + if (typeStr.includes('ZCLDataTypes.securityKey128')) return 'Buffer'; + if (typeStr.includes('ZCLDataTypes.buffer')) return 'Buffer'; + + // Handle enum8/enum16 - extract keys (multiline support) + const enumMatch = typeStr.match(/ZCLDataTypes\.enum(?:8|16)\(\{([\s\S]*?)\}\)/); + if (enumMatch) { + const enumBody = enumMatch[1]; + const keys = extractEnumKeys(enumBody); + if (keys.length > 0) { + return keys.map(k => `'${k}'`).join(' | '); + } + } + + // Handle map8/map16/map32 - extract flag names (can span multiple lines) + const mapMatch = typeStr.match(/ZCLDataTypes\.map(?:8|16|32)\(([\s\S]*?)\)/); + if (mapMatch) { + const mapArgs = mapMatch[1]; + const flags = extractMapFlags(mapArgs); + if (flags.length > 0) { + return `Partial<{ ${flags.map(f => `${f}: boolean`).join('; ')} }>`; + } + } + + // Handle Array0/Array8 + if (typeStr.includes('ZCLDataTypes.Array')) { + return 'unknown[]'; + } + + // Fallback + return 'unknown'; +} + +/** + * Extract enum keys from enum body string + */ +function extractEnumKeys(enumBody) { + const keys = []; + // Match patterns like: keyName: 0, or 'keyName': 0 + const keyPattern = /['"]?(\w+)['"]?\s*:/g; + let match = keyPattern.exec(enumBody); + while (match !== null) { + keys.push(match[1]); + match = keyPattern.exec(enumBody); + } + return keys; +} + +/** + * Extract map flag names from map arguments + */ +function extractMapFlags(mapArgs) { + const flags = []; + // Match quoted strings + const flagPattern = /['"](\w+)['"]/g; + let match = flagPattern.exec(mapArgs); + while (match !== null) { + flags.push(match[1]); + match = flagPattern.exec(mapArgs); + } + return flags; +} + +/** + * Parse a cluster file and extract definitions + */ +function parseClusterFile(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const fileName = path.basename(filePath, '.js'); + + // Extract cluster name + const nameMatch = content.match(/static\s+get\s+NAME\(\)\s*\{\s*return\s+['"](\w+)['"]/); + if (!nameMatch) { + console.warn(`Could not find NAME in ${fileName}`); + return null; + } + const clusterName = nameMatch[1]; + + // Extract cluster ID + const idMatch = content.match(/static\s+get\s+ID\(\)\s*\{\s*return\s+(\d+)/); + const clusterId = idMatch ? parseInt(idMatch[1], 10) : null; + + // Extract ATTRIBUTES block + const attributes = parseAttributesBlock(content); + + // Extract COMMANDS block + const commands = parseCommandsBlock(content); + + return { + fileName, + clusterName, + clusterId, + attributes, + commands, + }; +} + +/** + * Strip comments from code + */ +function stripComments(code) { + // Remove multi-line comments + code = code.replace(/\/\*[\s\S]*?\*\//g, ''); + // Remove single-line comments + code = code.replace(/\/\/.*$/gm, ''); + return code; +} + +/** + * Parse ATTRIBUTES block from file content + */ +function parseAttributesBlock(content) { + const attributes = []; + + // Find ATTRIBUTES object - handle both const ATTRIBUTES = { and ATTRIBUTES: { + const attrMatch = content.match(/(?:const\s+)?ATTRIBUTES\s*=\s*\{/); + if (!attrMatch) return attributes; + + const startIdx = attrMatch.index + attrMatch[0].length; + let braceCount = 1; + let idx = startIdx; + + // Find matching closing brace + while (braceCount > 0 && idx < content.length) { + if (content[idx] === '{') braceCount++; + else if (content[idx] === '}') braceCount--; + idx++; + } + + let attrBlock = content.substring(startIdx, idx - 1); + + // Strip comments for easier parsing + attrBlock = stripComments(attrBlock); + + // Split into top-level attribute entries + const attrEntries = splitTopLevelEntries(attrBlock); + + for (const entry of attrEntries) { + // Match attribute name - may have leading whitespace/newlines after comment removal + const nameMatch = entry.match(/^\s*(\w+)\s*:/); + if (!nameMatch) continue; + + const attrName = nameMatch[1]; + + // Extract the type definition - look for type: followed by the type value + const typeMatch = entry.match(/type\s*:\s*([\s\S]+?)(?:,\s*(?:id|manufacturerId)\s*:|$)/); + if (!typeMatch) { + // Try alternative: type is last in object + const typeMatchAlt = entry.match(/type\s*:\s*([\s\S]+?)\s*[,}]?\s*$/); + if (typeMatchAlt) { + const typeStr = typeMatchAlt[1].trim().replace(/,\s*$/, ''); + const tsType = zclTypeToTS(typeStr); + attributes.push({ name: attrName, tsType }); + } + continue; + } + + const typeStr = typeMatch[1].trim().replace(/,\s*$/, ''); + const tsType = zclTypeToTS(typeStr); + attributes.push({ name: attrName, tsType }); + } + + return attributes; +} + +/** + * Parse COMMANDS block from file content + */ +function parseCommandsBlock(content) { + const commands = []; + + const cmdMatch = content.match(/(?:const\s+)?COMMANDS\s*=\s*\{/); + if (!cmdMatch) return commands; + + const startIdx = cmdMatch.index + cmdMatch[0].length; + let braceCount = 1; + let idx = startIdx; + + while (braceCount > 0 && idx < content.length) { + if (content[idx] === '{') braceCount++; + else if (content[idx] === '}') braceCount--; + idx++; + } + + let cmdBlock = content.substring(startIdx, idx - 1); + + // Strip comments for easier parsing + cmdBlock = stripComments(cmdBlock); + + // Parse commands - need to handle nested braces for args + // Split by top-level command definitions + const cmdEntries = splitTopLevelEntries(cmdBlock); + + for (const entry of cmdEntries) { + const nameMatch = entry.match(/^\s*(\w+)\s*:/); + if (!nameMatch) continue; + + const cmdName = nameMatch[1]; + const args = parseCommandArgs(entry); + + commands.push({ name: cmdName, args }); + } + + return commands; +} + +/** + * Split a block into top-level entries (handling nested braces and parentheses) + */ +function splitTopLevelEntries(block) { + const entries = []; + let braceCount = 0; + let parenCount = 0; + let currentEntry = ''; + let inString = false; + let stringChar = ''; + + for (let i = 0; i < block.length; i++) { + const char = block[i]; + const prevChar = i > 0 ? block[i - 1] : ''; + + // Handle strings + if ((char === '"' || char === "'") && prevChar !== '\\') { + if (!inString) { + inString = true; + stringChar = char; + } else if (char === stringChar) { + inString = false; + } + } + + if (!inString) { + if (char === '{') braceCount++; + else if (char === '}') braceCount--; + else if (char === '(') parenCount++; + else if (char === ')') parenCount--; + else if (char === ',' && braceCount === 0 && parenCount === 0) { + if (currentEntry.trim()) { + entries.push(currentEntry.trim()); + } + currentEntry = ''; + continue; + } + } + + currentEntry += char; + } + + if (currentEntry.trim()) { + entries.push(currentEntry.trim()); + } + + return entries; +} + +/** + * Parse command arguments from a command entry + */ +function parseCommandArgs(cmdEntry) { + const args = []; + + // Find args block + const argsMatch = cmdEntry.match(/args\s*:\s*\{/); + if (!argsMatch) return args; + + const startIdx = argsMatch.index + argsMatch[0].length; + let braceCount = 1; + let idx = startIdx; + + while (braceCount > 0 && idx < cmdEntry.length) { + if (cmdEntry[idx] === '{') braceCount++; + else if (cmdEntry[idx] === '}') braceCount--; + idx++; + } + + const argsBlock = cmdEntry.substring(startIdx, idx - 1); + + // Parse each argument + const argEntries = splitTopLevelEntries(argsBlock); + + for (const argEntry of argEntries) { + const nameMatch = argEntry.match(/^\s*(\w+)\s*:/); + if (!nameMatch) continue; + + const argName = nameMatch[1]; + const typeStr = argEntry.substring(argEntry.indexOf(':') + 1).trim(); + const tsType = zclTypeToTS(typeStr); + + args.push({ name: argName, tsType }); + } + + return args; +} + +/** + * Convert cluster name to PascalCase interface name + */ +function toInterfaceName(clusterName) { + // Handle special cases + const name = clusterName.charAt(0).toUpperCase() + clusterName.slice(1); + return `${name}Cluster`; +} + +/** + * Generate TypeScript interface for a cluster + */ +function generateClusterInterface(cluster) { + const interfaceName = toInterfaceName(cluster.clusterName); + const lines = []; + + // Generate attributes interface + if (cluster.attributes.length > 0) { + lines.push(`export interface ${interfaceName}Attributes {`); + for (const attr of cluster.attributes) { + lines.push(` ${attr.name}?: ${attr.tsType};`); + } + lines.push('}'); + lines.push(''); + } + + // Generate cluster interface + lines.push(`export interface ${interfaceName} extends ZCLNodeCluster {`); + + // Add typed readAttributes if we have attributes + if (cluster.attributes.length > 0) { + const attrNames = cluster.attributes.map(a => `'${a.name}'`).join(' | '); + lines.push(` readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>;`); + lines.push(` writeAttributes(attributes: Partial<${interfaceName}Attributes>): Promise;`); + } + + // Add command methods + for (const cmd of cluster.commands) { + if (cmd.args.length > 0) { + const argsType = `{ ${cmd.args.map(a => `${a.name}${a.tsType === 'Buffer' ? '?' : ''}: ${a.tsType}`).join('; ')} }`; + lines.push(` ${cmd.name}(args: ${argsType}): Promise;`); + } else { + lines.push(` ${cmd.name}(): Promise;`); + } + } + + lines.push('}'); + + return lines.join('\n'); +} + +/** + * Generate the full index.d.ts file + */ +function generateTypesFile(clusters) { + const lines = []; + + // Header + lines.push('// Auto-generated TypeScript definitions for zigbee-clusters'); + lines.push('// Generated by scripts/generate-types.js'); + lines.push(''); + lines.push('import * as EventEmitter from "events";'); + lines.push(''); + + // Base types + lines.push(`type EndpointDescriptor = { + endpointId: number; + inputClusters: number[]; + outputClusters: number[]; +}; + +type ConstructorOptions = { + endpointDescriptors: EndpointDescriptor[]; + sendFrame: (endpointId: number, clusterId: number, frame: Buffer) => Promise; +}; +`); + + // Base ZCLNodeCluster interface + lines.push(`export interface ZCLNodeCluster extends EventEmitter { + /** + * Command which requests the remote cluster to report its generated commands. + */ + discoverCommandsGenerated(opts?: { + startValue?: number; + maxResults?: number; + }): Promise; + + /** + * Command which requests the remote cluster to report its received commands. + */ + discoverCommandsReceived(opts?: { + startValue?: number; + maxResults?: number; + }): Promise; + + /** + * Command which reads a given set of attributes from the remote cluster. + */ + readAttributes( + attributeNames: string[], + opts?: { timeout?: number } + ): Promise<{ [x: string]: unknown }>; + + /** + * Command which writes a given set of attribute key-value pairs to the remote cluster. + */ + writeAttributes(attributes?: object): Promise; + + /** + * Command which configures attribute reporting for the given attributes on the remote cluster. + */ + configureReporting(attributes?: object): Promise; + + /** + * Command which retrieves the reporting configurations for the given attributes. + */ + readReportingConfiguration(attributes?: (string | number)[]): Promise<{ + status: string; + direction: 'reported' | 'received'; + attributeId: number; + attributeDataType?: number; + minInterval?: number; + maxInterval?: number; + minChange?: number; + timeoutPeriod?: number; + }[]>; + + /** + * Command which discovers the implemented attributes on the remote cluster. + */ + discoverAttributes(): Promise<(string | number)[]>; + + /** + * Command which discovers the implemented attributes with access control info. + */ + discoverAttributesExtended(): Promise<{ + name?: string; + id: number; + acl: { readable: boolean; writable: boolean; reportable: boolean }; + }[]>; +} +`); + + // Generate individual cluster interfaces + for (const cluster of clusters) { + lines.push(generateClusterInterface(cluster)); + lines.push(''); + } + + // Generate cluster registry type + lines.push('/** Type-safe cluster registry */'); + lines.push('export interface ClusterRegistry {'); + for (const cluster of clusters) { + const interfaceName = toInterfaceName(cluster.clusterName); + lines.push(` ${cluster.clusterName}?: ${interfaceName};`); + } + lines.push('}'); + lines.push(''); + + // Generate endpoint type + lines.push(`export type ZCLNodeEndpoint = { + clusters: ClusterRegistry & { + [clusterName: string]: ZCLNodeCluster | undefined; + }; +}; + +export interface ZCLNode { + endpoints: { [endpointId: number | string]: ZCLNodeEndpoint }; + handleFrame: ( + endpointId: number, + clusterId: number, + frame: Buffer, + meta?: unknown + ) => Promise; +} +`); + + // Module declaration for CommonJS compatibility + lines.push(`declare module "zigbee-clusters" { + export const ZCLNode: { + new (options: ConstructorOptions): ZCLNode; + }; + export const CLUSTER: { + [key: string]: { ID: number; NAME: string; ATTRIBUTES: unknown; COMMANDS: unknown }; + }; + export { ZCLNodeCluster };`); + + // Export all cluster classes + for (const cluster of clusters) { + const interfaceName = toInterfaceName(cluster.clusterName); + lines.push(` export const ${interfaceName}: ${interfaceName};`); + } + + lines.push('}'); + lines.push(''); + + // Also export at top level for ESM + lines.push('export { ZCLNode, ZCLNodeCluster, ZCLNodeEndpoint, ClusterRegistry };'); + + return lines.join('\n'); +} + +/** + * Main entry point + */ +function main() { + console.log('Scanning cluster files...'); + + const files = fs.readdirSync(CLUSTERS_DIR) + .filter(f => f.endsWith('.js') && !SKIP_FILES.includes(f)); + + console.log(`Found ${files.length} cluster files`); + + const clusters = []; + + for (const file of files) { + const filePath = path.join(CLUSTERS_DIR, file); + try { + const cluster = parseClusterFile(filePath); + if (cluster) { + clusters.push(cluster); + console.log(` ✓ ${cluster.clusterName} (${cluster.attributes.length} attrs, ${cluster.commands.length} cmds)`); + } + } catch (err) { + console.warn(` ✗ Failed to parse ${file}: ${err.message}`); + } + } + + // Sort clusters alphabetically + clusters.sort((a, b) => a.clusterName.localeCompare(b.clusterName)); + + console.log(`\nGenerating ${OUTPUT_FILE}...`); + const output = generateTypesFile(clusters); + fs.writeFileSync(OUTPUT_FILE, output); + + console.log(`Done! Generated types for ${clusters.length} clusters.`); +} + +main(); From 90400949033a5e81825e6448b3aba90b2ff19ee8 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Mon, 26 Jan 2026 21:50:19 +0100 Subject: [PATCH 2/9] ci: add workflow to auto-generate TypeScript types Automatically regenerates index.d.ts when cluster definitions change: - Triggers on push to develop when lib/clusters/*.js files change - Runs generate-types script and commits updated types - Uses [skip ci] to prevent infinite loop --- .github/workflows/generate-types.yml | 60 ++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/generate-types.yml diff --git a/.github/workflows/generate-types.yml b/.github/workflows/generate-types.yml new file mode 100644 index 0000000..454eac6 --- /dev/null +++ b/.github/workflows/generate-types.yml @@ -0,0 +1,60 @@ +name: Generate TypeScript Types + +on: + push: + branches: + - develop + paths: + - 'lib/clusters/**/*.js' + - 'scripts/generate-types.js' + +jobs: + generate-types: + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: develop + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Generate TypeScript types + run: npm run generate-types + + - name: Check for changes + id: check-changes + run: | + if git diff --quiet index.d.ts; then + echo "changed=false" >> $GITHUB_OUTPUT + echo "No changes to index.d.ts" + else + echo "changed=true" >> $GITHUB_OUTPUT + echo "index.d.ts has been updated" + git diff --stat index.d.ts + fi + + - name: Commit and push changes + if: steps.check-changes.outputs.changed == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add index.d.ts + git commit -m "chore(types): auto-generate TypeScript definitions + + Updated by GitHub Actions after cluster changes. + + [skip ci]" + git push origin develop From 006c0fbe8dce360be6e29917f6937aac12666646 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Tue, 27 Jan 2026 09:40:03 +0100 Subject: [PATCH 3/9] docs: explain why Buffer command args are optional Address Copilot review feedback by documenting that Buffer arguments (octstr, securityKey128, buffer types) are intentionally optional because ZCL allows empty octet strings and the runtime serializes undefined values as empty Buffers. Co-Authored-By: Claude Opus 4.5 --- scripts/generate-types.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/generate-types.js b/scripts/generate-types.js index 6bef031..bb358f5 100644 --- a/scripts/generate-types.js +++ b/scripts/generate-types.js @@ -371,6 +371,10 @@ function generateClusterInterface(cluster) { // Add command methods for (const cmd of cluster.commands) { if (cmd.args.length > 0) { + // Buffer arguments (octstr, securityKey128, buffer) are optional because ZCL allows + // empty octet strings (length 0). The data-types library serializes undefined/omitted + // Buffer args as empty Buffers. Example: DoorLock.lockDoor({ pinCode }) - pinCode is + // optional when the lock doesn't require PIN authentication. const argsType = `{ ${cmd.args.map(a => `${a.name}${a.tsType === 'Buffer' ? '?' : ''}: ${a.tsType}`).join('; ')} }`; lines.push(` ${cmd.name}(args: ${argsType}): Promise;`); } else { From d7bcf6e2daae61f4714c3d8b79f56c430da914bd Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Mon, 9 Feb 2026 17:27:21 +0100 Subject: [PATCH 4/9] docs: add TypeScript types generation instructions to README --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index ef8f1fb..4f7cf69 100644 --- a/README.md +++ b/README.md @@ -301,6 +301,28 @@ zclNode.endpoints[1].clusters["scenes"].ikeaSceneMove({ mode: 0, transitionTime: This also works for `BoundClusters`, if a node sends commands to Homey using a custom cluster it is necessary to implement a custom `BoundCluster` and bind it to the `ZCLNode` instance. For an example check the implementation in the `com.ikea.tradfri` driver [remote_control](https://github.com/athombv/com.ikea.tradfri-example/tree/master/drivers/remote_control/device.js). +## TypeScript Types + +This project includes auto-generated TypeScript definitions (`index.d.ts`) for all clusters, attributes, and commands. + +### Manual Generation + +To regenerate TypeScript types after modifying cluster definitions: + +```bash +npm run generate-types +``` + +This runs `scripts/generate-types.js` which loads all cluster modules and generates typed interfaces. + +### Automatic Generation (GitHub Actions) + +TypeScript types are automatically regenerated when changes are pushed to the `develop` branch that affect: +- `lib/clusters/**/*.js` - cluster definitions +- `scripts/generate-types.js` - the generator script + +The workflow commits updated types back to `develop` if changes are detected. + ## Contributing Great if you'd like to contribute to this project, a few things to take note of before submitting a PR: From 6b501b2bbd0140088ec058d20cfb78fb5acdaa99 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Mon, 9 Feb 2026 17:28:14 +0100 Subject: [PATCH 5/9] refactor(types): simplify type generation script - Load cluster modules directly via require() instead of regex parsing - Use ZCLDataType shortName/args properties for type inference - Remove text parsing functions (stripComments, splitTopLevelEntries, etc.) - Reduce script from 578 to 353 lines (-39%) --- index.d.ts | 169 +++++++++++++++- scripts/generate-types.js | 397 +++++++++----------------------------- 2 files changed, 248 insertions(+), 318 deletions(-) diff --git a/index.d.ts b/index.d.ts index 5ae2133..2659494 100644 --- a/index.d.ts +++ b/index.d.ts @@ -166,10 +166,10 @@ export interface BasicClusterAttributes { manufacturerName?: string; modelId?: string; dateCode?: string; - powerSource?: unknown; + powerSource?: 'unknown' | 'mains' | 'mains3phase' | 'battery' | 'dc' | 'emergencyMains' | 'emergencyTransfer'; appProfileVersion?: number; locationDesc?: string; - physicalEnv?: unknown; + physicalEnv?: 'Unspecified' | 'Atrium' | 'Bar' | 'Courtyard' | 'Bathroom' | 'Bedroom' | 'BilliardRoom' | 'UtilityRoom' | 'Cellar' | 'StorageCloset' | 'Theater' | 'Office' | 'Deck' | 'Den' | 'DiningRoom' | 'ElectricalRoom' | 'Elevator' | 'Entry' | 'FamilyRoom' | 'MainFloor' | 'Upstairs' | 'Downstairs' | 'Basement' | 'Gallery' | 'GameRoom' | 'Garage' | 'Gym' | 'Hallway' | 'House' | 'Kitchen' | 'LaundryRoom' | 'Library' | 'MasterBedroom' | 'MudRoom' | 'Nursery' | 'Pantry' | 'Outside' | 'Pool' | 'Porch' | 'SewingRoom' | 'SittingRoom' | 'Stairway' | 'Yard' | 'Attic' | 'HotTub' | 'LivingRoom' | 'Sauna' | 'Workshop' | 'GuestBedroom' | 'GuestBath' | 'PowderRoom' | 'BackYard' | 'FrontYard' | 'Patio' | 'Driveway' | 'SunRoom' | 'Spa' | 'Whirlpool' | 'Shed' | 'EquipmentStorage' | 'HobbyRoom' | 'Fountain' | 'Pond' | 'ReceptionRoom' | 'BreakfastRoom' | 'Nook' | 'Garden' | 'Balcony' | 'PanicRoom' | 'Terrace' | 'Roof' | 'Toilet' | 'ToiletMain' | 'OutsideToilet' | 'ShowerRoom' | 'Study' | 'FrontGarden' | 'BackGarden' | 'Kettle' | 'Television' | 'Stove' | 'Microwave' | 'Toaster' | 'Vacuum' | 'Appliance' | 'FrontDoor' | 'BackDoor' | 'FridgeDoor' | 'MedicationCabinetDoor' | 'WardrobeDoor' | 'FrontCupboardDoor' | 'OtherDoor' | 'WaitingRoom' | 'TriageRoom' | 'DoctorsOffice' | 'PatientsPrivateRoom' | 'ConsultationRoom' | 'NurseStation' | 'Ward' | 'Corridor' | 'OperatingTheatre' | 'DentalSurgeryRoom' | 'MedicalImagingRoom' | 'DecontaminationRoom' | 'Unknown'; deviceEnabled?: boolean; alarmMask?: Partial<{ hardwareFault: boolean; softwareFault: boolean }>; disableLocalConfig?: Partial<{ factoryResetDisabled: boolean; configurationDisabled: boolean }>; @@ -319,7 +319,7 @@ export interface DoorLockClusterAttributes { userCodeTemporaryDisableTime?: number; sendPINOverTheAir?: boolean; requirePINforRFOperation?: boolean; - securityLevel?: 'network' | 'apsLinkKey'; + securityLevel?: 'network' | 'apsSecurity'; alarmMask?: Partial<{ deadboltJammed: boolean; lockResetToFactoryDefaults: boolean; reserved2: boolean; rfModulePowerCycled: boolean; tamperAlarmWrongCodeEntryLimit: boolean; tamperAlarmFrontEscutcheonRemoved: boolean; forcedDoorOpenUnderDoorLockedCondition: boolean }>; keypadOperationEventMask?: Partial<{ unknownOrManufacturerSpecificKeypadOperationEvent: boolean; lockSourceKeypad: boolean; unlockSourceKeypad: boolean; lockSourceKeypadErrorInvalidPIN: boolean; lockSourceKeypadErrorInvalidSchedule: boolean; unlockSourceKeypadErrorInvalidCode: boolean; unlockSourceKeypadErrorInvalidSchedule: boolean; nonAccessUserOperationEventSourceKeypad: boolean }>; rfOperationEventMask?: Partial<{ unknownOrManufacturerSpecificKeypadOperationEvent: boolean; lockSourceRF: boolean; unlockSourceRF: boolean; lockSourceRFErrorInvalidCode: boolean; lockSourceRFErrorInvalidSchedule: boolean; unlockSourceRFErrorInvalidCode: boolean; unlockSourceRFErrorInvalidSchedule: boolean }>; @@ -359,6 +359,8 @@ export interface DoorLockCluster extends ZCLNodeCluster { getRFIDCode(args: { userId: number }): Promise; clearRFIDCode(args: { userId: number }): Promise; clearAllRFIDCodes(): Promise; + operationEventNotification(args: { operationEventSource: number; operationEventCode: number; userId: number; pin?: Buffer; zigBeeLocalTime: number; data?: Buffer }): Promise; + programmingEventNotification(args: { programEventSource: number; programEventCode: number; userId: number; pin?: Buffer; userType: 'unrestricted' | 'yearDayScheduleUser' | 'weekDayScheduleUser' | 'masterUser' | 'nonAccessUser' | 'notSupported'; userStatus: 'available' | 'occupiedEnabled' | 'occupiedDisabled' | 'notSupported'; zigBeeLocalTime: number; data?: Buffer }): Promise; } export interface ElectricalMeasurementClusterAttributes { @@ -413,7 +415,7 @@ export interface GroupsCluster extends ZCLNodeCluster { writeAttributes(attributes: Partial): Promise; addGroup(args: { groupId: number; groupName: string }): Promise; viewGroup(args: { groupId: number }): Promise; - getGroupMembership(args: { groupIds: number }): Promise; + getGroupMembership(args: { groupIds: unknown }): Promise; removeGroup(args: { groupId: number }): Promise; removeAllGroups(): Promise; addGroupIfIdentify(args: { groupId: number; groupName: string }): Promise; @@ -428,7 +430,7 @@ export interface IasWDCluster extends ZCLNodeCluster { export interface IasZoneClusterAttributes { zoneState?: 'notEnrolled' | 'enrolled'; zoneType?: 'standardCIE' | 'motionSensor' | 'contactSwitch' | 'fireSensor' | 'waterSensor' | 'cabonMonoxideSensor' | 'personalEmergencyDevice' | 'vibrationMovementSensor' | 'remoteControl' | 'keyfob' | 'keypad' | 'standardWarningDevice' | 'glassBreakSensor' | 'securityRepeater' | 'invalidZoneType'; - zoneStatus?: unknown; + zoneStatus?: Partial<{ alarm1: boolean; alarm2: boolean; tamper: boolean; battery: boolean; supervisionReports: boolean; restoreReports: boolean; trouble: boolean; acMains: boolean; test: boolean; batteryDefect: boolean }>; iasCIEAddress?: string; zoneId?: number; } @@ -436,7 +438,7 @@ export interface IasZoneClusterAttributes { export interface IasZoneCluster extends ZCLNodeCluster { readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; writeAttributes(attributes: Partial): Promise; - zoneStatusChangeNotification(args: { zoneStatus: unknown; extendedStatus: number; zoneId: number; delay: number }): Promise; + zoneStatusChangeNotification(args: { zoneStatus: Partial<{ alarm1: boolean; alarm2: boolean; tamper: boolean; battery: boolean; supervisionReports: boolean; restoreReports: boolean; trouble: boolean; acMains: boolean; test: boolean; batteryDefect: boolean }>; extendedStatus: number; zoneId: number; delay: number }): Promise; zoneEnrollResponse(args: { enrollResponseCode: 'success' | 'notSupported' | 'noEnrollPermit' | 'tooManyZones'; zoneId: number }): Promise; zoneEnrollRequest(args: { zoneType: 'standard' | 'motionSensor' | 'contactSwitch' | 'fireSensor' | 'waterSensor' | 'carbonMonoxideSensor' | 'personalEmergencyDevice' | 'vibrationMovementSensor' | 'remoteControl' | 'keyFob' | 'keyPad' | 'standardWarningDevice' | 'glassBreakSensor' | 'securityRepeater' | 'invalid'; manufacturerCode: number }): Promise; initiateNormalOperationMode(): Promise; @@ -450,7 +452,7 @@ export interface IdentifyCluster extends ZCLNodeCluster { readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; writeAttributes(attributes: Partial): Promise; identify(args: { identifyTime: number }): Promise; - identifyQuery(args: { timeout: number }): Promise; + identifyQuery(): Promise; triggerEffect(args: { effectIdentifier: 'blink' | 'breathe' | 'okay' | 'channelChange' | 'finish' | 'stop'; effectVariant: number }): Promise; } @@ -581,11 +583,162 @@ export interface MeteringClusterAttributes { demandIntegrationPeriod?: number; numberOfDemandSubintervals?: number; demandLimitArmDuration?: number; + currentNoTierBlock1SummationDelivered?: number; + currentNoTierBlock2SummationDelivered?: number; + currentNoTierBlock3SummationDelivered?: number; + currentNoTierBlock4SummationDelivered?: number; + currentNoTierBlock5SummationDelivered?: number; + currentNoTierBlock6SummationDelivered?: number; + currentNoTierBlock7SummationDelivered?: number; + currentNoTierBlock8SummationDelivered?: number; + currentNoTierBlock9SummationDelivered?: number; + currentNoTierBlock10SummationDelivered?: number; + currentNoTierBlock11SummationDelivered?: number; + currentNoTierBlock12SummationDelivered?: number; + currentNoTierBlock13SummationDelivered?: number; + currentNoTierBlock14SummationDelivered?: number; + currentNoTierBlock15SummationDelivered?: number; + currentNoTierBlock16SummationDelivered?: number; + currentTier1Block1SummationDelivered?: number; + currentTier1Block2SummationDelivered?: number; + currentTier1Block3SummationDelivered?: number; + currentTier1Block4SummationDelivered?: number; + currentTier1Block5SummationDelivered?: number; + currentTier1Block6SummationDelivered?: number; + currentTier1Block7SummationDelivered?: number; + currentTier1Block8SummationDelivered?: number; + currentTier1Block9SummationDelivered?: number; + currentTier1Block10SummationDelivered?: number; + currentTier1Block11SummationDelivered?: number; + currentTier1Block12SummationDelivered?: number; + currentTier1Block13SummationDelivered?: number; + currentTier1Block14SummationDelivered?: number; + currentTier1Block15SummationDelivered?: number; + currentTier1Block16SummationDelivered?: number; + currentTier2Block1SummationDelivered?: number; + currentTier2Block2SummationDelivered?: number; + currentTier2Block3SummationDelivered?: number; + currentTier2Block4SummationDelivered?: number; + currentTier2Block5SummationDelivered?: number; + currentTier2Block6SummationDelivered?: number; + currentTier2Block7SummationDelivered?: number; + currentTier2Block8SummationDelivered?: number; + currentTier2Block9SummationDelivered?: number; + currentTier2Block10SummationDelivered?: number; + currentTier2Block11SummationDelivered?: number; + currentTier2Block12SummationDelivered?: number; + currentTier2Block13SummationDelivered?: number; + currentTier2Block14SummationDelivered?: number; + currentTier2Block15SummationDelivered?: number; + currentTier2Block16SummationDelivered?: number; + currentTier3Block1SummationDelivered?: number; + currentTier3Block2SummationDelivered?: number; + currentTier3Block3SummationDelivered?: number; + currentTier3Block4SummationDelivered?: number; + currentTier3Block5SummationDelivered?: number; + currentTier3Block6SummationDelivered?: number; + currentTier3Block7SummationDelivered?: number; + currentTier3Block8SummationDelivered?: number; + currentTier3Block9SummationDelivered?: number; + currentTier3Block10SummationDelivered?: number; + currentTier3Block11SummationDelivered?: number; + currentTier3Block12SummationDelivered?: number; + currentTier3Block13SummationDelivered?: number; + currentTier3Block14SummationDelivered?: number; + currentTier3Block15SummationDelivered?: number; + currentTier3Block16SummationDelivered?: number; + currentTier4Block1SummationDelivered?: number; + currentTier4Block2SummationDelivered?: number; + currentTier4Block3SummationDelivered?: number; + currentTier4Block4SummationDelivered?: number; + currentTier4Block5SummationDelivered?: number; + currentTier4Block6SummationDelivered?: number; + currentTier4Block7SummationDelivered?: number; + currentTier4Block8SummationDelivered?: number; + currentTier4Block9SummationDelivered?: number; + currentTier4Block10SummationDelivered?: number; + currentTier4Block11SummationDelivered?: number; + currentTier4Block12SummationDelivered?: number; + currentTier4Block13SummationDelivered?: number; + currentTier4Block14SummationDelivered?: number; + currentTier4Block15SummationDelivered?: number; + currentTier4Block16SummationDelivered?: number; + genericAlarmMask?: unknown; + electricityAlarmMask?: unknown; + genericFlowPressureAlarmMask?: unknown; + waterSpecificAlarmMask?: unknown; + heatAndCoolingSpecificAlarmMask?: unknown; + gasSpecificAlarmMask?: unknown; + extendedGenericAlarmMask?: unknown; + manufacturerAlarmMask?: unknown; + currentNoTierBlock1SummationReceived?: number; + currentNoTierBlock2SummationReceived?: number; + currentNoTierBlock3SummationReceived?: number; + currentNoTierBlock4SummationReceived?: number; + currentNoTierBlock5SummationReceived?: number; + currentNoTierBlock6SummationReceived?: number; + currentNoTierBlock7SummationReceived?: number; + currentNoTierBlock8SummationReceived?: number; + currentNoTierBlock9SummationReceived?: number; + currentNoTierBlock10SummationReceived?: number; + currentNoTierBlock11SummationReceived?: number; + currentNoTierBlock12SummationReceived?: number; + currentNoTierBlock13SummationReceived?: number; + currentNoTierBlock14SummationReceived?: number; + currentNoTierBlock15SummationReceived?: number; + currentNoTierBlock16SummationReceived?: number; + billToDateDelivered?: number; + billToDateTimeStampDelivered?: number; + projectedBillDelivered?: number; + projectedBillTimeStampDelivered?: number; + billDeliveredTrailingDigit?: unknown; + billToDateReceived?: number; + billToDateTimeStampReceived?: number; + projectedBillReceived?: number; + projectedBillTimeStampReceived?: number; + billReceivedTrailingDigit?: unknown; + proposedChangeSupplyImplementationTime?: number; + proposedChangeSupplyStatus?: unknown; + uncontrolledFlowThreshold?: number; + uncontrolledFlowThresholdUnitOfMeasure?: unknown; + uncontrolledFlowMultiplier?: number; + uncontrolledFlowDivisor?: number; + flowStabilisationPeriod?: number; + flowMeasurementPeriod?: number; } export interface MeteringCluster extends ZCLNodeCluster { - readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; writeAttributes(attributes: Partial): Promise; + getProfileResponse(args: { endTime: number; status: unknown; profileIntervalPeriod: unknown; numberOfPeriodsDelivered: number; intervals?: Buffer }): Promise; + requestMirror(): Promise; + removeMirror(): Promise; + requestFastPollModeResponse(args: { appliedUpdatePeriod: number; fastPollModeEndTime: number }): Promise; + scheduleSnapshotResponse(args: { issuerEventId: number; snapshotResponsePayload?: Buffer }): Promise; + takeSnapshotResponse(args: { snapshotId: number; snapshotConfirmation: unknown }): Promise; + publishSnapshot(args: { snapshotId: number; snapshotTime: number; totalSnapshotsFound: number; commandIndex: number; totalCommands: number; snapshotCause: unknown; snapshotPayloadType: unknown; snapshotPayload?: Buffer }): Promise; + getSampledDataResponse(args: { sampleId: number; sampleStartTime: number; sampleType: unknown; sampleRequestInterval: number; numberOfSamples: number; samples?: Buffer }): Promise; + configureMirror(args: { issuerEventId: number; reportingInterval: number; mirrorNotificationReporting: boolean; notificationScheme: number }): Promise; + configureNotificationScheme(args: { issuerEventId: number; notificationScheme: number; notificationFlagOrder: unknown }): Promise; + configureNotificationFlags(args: { issuerEventId: number; notificationScheme: number; notificationFlagAttributeId: number; clusterId: number; manufacturerCode: number; numberOfCommands: number; commandIds?: Buffer }): Promise; + getNotifiedMessage(args: { notificationScheme: number; notificationFlagAttributeId: number; notificationFlagsN: unknown }): Promise; + supplyStatusResponse(args: { providerId: number; issuerEventId: number; implementationDateTime: number; supplyStatus: unknown }): Promise; + startSamplingResponse(args: { sampleId: number }): Promise; + getProfile(args: { intervalChannel: unknown; endTime: number; numberOfPeriods: number }): Promise; + requestMirrorResponse(args: { endpointId: number }): Promise; + mirrorRemoved(args: { endpointId: number }): Promise; + requestFastPollMode(args: { fastPollUpdatePeriod: number; duration: number }): Promise; + scheduleSnapshot(args: { issuerEventId: number; commandIndex: number; commandCount: number; snapshotSchedulePayload?: Buffer }): Promise; + takeSnapshot(args: { snapshotCause: unknown }): Promise; + getSnapshot(args: { earliestStartTime: number; latestEndTime: number; snapshotOffset: number; snapshotCause: unknown }): Promise; + startSampling(args: { issuerEventId: number; startSamplingTime: number; sampleType: unknown; sampleRequestInterval: number; maxNumberOfSamples: number }): Promise; + getSampledData(args: { sampleId: number; earliestSampleTime: number; sampleType: unknown; numberOfSamples: number }): Promise; + mirrorReportAttributeResponse(args: { notificationScheme: number; notificationFlags?: Buffer }): Promise; + resetLoadLimitCounter(args: { providerId: number; issuerEventId: number }): Promise; + changeSupply(args: { providerId: number; issuerEventId: number; requestDateTime: number; implementationDateTime: number; proposedSupplyStatus: unknown; supplyControlBits: unknown }): Promise; + localChangeSupply(args: { proposedSupplyStatus: unknown }): Promise; + setSupplyStatus(args: { issuerEventId: number; supplyTamperState: unknown; supplyDepletionState: unknown; supplyUncontrolledFlowState: unknown; loadLimitSupplyState: unknown }): Promise; + setUncontrolledFlowThreshold(args: { providerId: number; issuerEventId: number; uncontrolledFlowThreshold: number; unitOfMeasure: unknown; multiplier: number; divisor: number; stabilisationPeriod: number; measurementPeriod: number }): Promise; } export interface MultistateInputClusterAttributes { diff --git a/scripts/generate-types.js b/scripts/generate-types.js index bb358f5..8b0244a 100644 --- a/scripts/generate-types.js +++ b/scripts/generate-types.js @@ -1,348 +1,129 @@ 'use strict'; -/* eslint-disable no-console, no-use-before-define */ +/* eslint-disable no-console, global-require */ /** * Type generation script for zigbee-clusters - * Parses cluster definitions and generates TypeScript interfaces + * Loads cluster modules directly and generates TypeScript interfaces */ const fs = require('fs'); const path = require('path'); -const CLUSTERS_DIR = path.join(__dirname, '../lib/clusters'); const OUTPUT_FILE = path.join(__dirname, '../index.d.ts'); -// Files to skip (not actual cluster definitions) -const SKIP_FILES = ['index.js']; - -/** - * Convert ZCLDataType to TypeScript type string - */ -function zclTypeToTS(typeStr) { - // Handle simple types - check these first (more specific matches) - if (typeStr.includes('ZCLDataTypes.bool')) return 'boolean'; - if (typeStr.includes('ZCLDataTypes.uint48')) return 'number'; - if (typeStr.includes('ZCLDataTypes.uint40')) return 'number'; - if (typeStr.includes('ZCLDataTypes.uint32')) return 'number'; - if (typeStr.includes('ZCLDataTypes.uint24')) return 'number'; - if (typeStr.includes('ZCLDataTypes.uint16')) return 'number'; - if (typeStr.includes('ZCLDataTypes.uint8')) return 'number'; - if (typeStr.includes('ZCLDataTypes.int32')) return 'number'; - if (typeStr.includes('ZCLDataTypes.int24')) return 'number'; - if (typeStr.includes('ZCLDataTypes.int16')) return 'number'; - if (typeStr.includes('ZCLDataTypes.int8')) return 'number'; - if (typeStr.includes('ZCLDataTypes.string')) return 'string'; - if (typeStr.includes('ZCLDataTypes.octstr')) return 'Buffer'; - if (typeStr.includes('ZCLDataTypes.data32')) return 'number'; - if (typeStr.includes('ZCLDataTypes.data24')) return 'number'; - if (typeStr.includes('ZCLDataTypes.data16')) return 'number'; - if (typeStr.includes('ZCLDataTypes.data8')) return 'number'; - if (typeStr.includes('ZCLDataTypes.EUI64')) return 'string'; - if (typeStr.includes('ZCLDataTypes.securityKey128')) return 'Buffer'; - if (typeStr.includes('ZCLDataTypes.buffer')) return 'Buffer'; - - // Handle enum8/enum16 - extract keys (multiline support) - const enumMatch = typeStr.match(/ZCLDataTypes\.enum(?:8|16)\(\{([\s\S]*?)\}\)/); - if (enumMatch) { - const enumBody = enumMatch[1]; - const keys = extractEnumKeys(enumBody); - if (keys.length > 0) { - return keys.map(k => `'${k}'`).join(' | '); - } - } - - // Handle map8/map16/map32 - extract flag names (can span multiple lines) - const mapMatch = typeStr.match(/ZCLDataTypes\.map(?:8|16|32)\(([\s\S]*?)\)/); - if (mapMatch) { - const mapArgs = mapMatch[1]; - const flags = extractMapFlags(mapArgs); - if (flags.length > 0) { - return `Partial<{ ${flags.map(f => `${f}: boolean`).join('; ')} }>`; - } - } - - // Handle Array0/Array8 - if (typeStr.includes('ZCLDataTypes.Array')) { - return 'unknown[]'; - } - - // Fallback - return 'unknown'; -} - -/** - * Extract enum keys from enum body string - */ -function extractEnumKeys(enumBody) { - const keys = []; - // Match patterns like: keyName: 0, or 'keyName': 0 - const keyPattern = /['"]?(\w+)['"]?\s*:/g; - let match = keyPattern.exec(enumBody); - while (match !== null) { - keys.push(match[1]); - match = keyPattern.exec(enumBody); - } - return keys; -} - -/** - * Extract map flag names from map arguments - */ -function extractMapFlags(mapArgs) { - const flags = []; - // Match quoted strings - const flagPattern = /['"](\w+)['"]/g; - let match = flagPattern.exec(mapArgs); - while (match !== null) { - flags.push(match[1]); - match = flagPattern.exec(mapArgs); - } - return flags; -} - /** - * Parse a cluster file and extract definitions + * Convert a ZCLDataType object to TypeScript type string + * @param {object} dataType - ZCLDataType object with shortName and args + * @returns {string} TypeScript type string */ -function parseClusterFile(filePath) { - const content = fs.readFileSync(filePath, 'utf8'); - const fileName = path.basename(filePath, '.js'); - - // Extract cluster name - const nameMatch = content.match(/static\s+get\s+NAME\(\)\s*\{\s*return\s+['"](\w+)['"]/); - if (!nameMatch) { - console.warn(`Could not find NAME in ${fileName}`); - return null; - } - const clusterName = nameMatch[1]; - - // Extract cluster ID - const idMatch = content.match(/static\s+get\s+ID\(\)\s*\{\s*return\s+(\d+)/); - const clusterId = idMatch ? parseInt(idMatch[1], 10) : null; - - // Extract ATTRIBUTES block - const attributes = parseAttributesBlock(content); - - // Extract COMMANDS block - const commands = parseCommandsBlock(content); - - return { - fileName, - clusterName, - clusterId, - attributes, - commands, - }; -} +function zclTypeToTS(dataType) { + if (!dataType || !dataType.shortName) return 'unknown'; -/** - * Strip comments from code - */ -function stripComments(code) { - // Remove multi-line comments - code = code.replace(/\/\*[\s\S]*?\*\//g, ''); - // Remove single-line comments - code = code.replace(/\/\/.*$/gm, ''); - return code; -} + const { shortName, args } = dataType; -/** - * Parse ATTRIBUTES block from file content - */ -function parseAttributesBlock(content) { - const attributes = []; + // Boolean + if (shortName === 'bool') return 'boolean'; - // Find ATTRIBUTES object - handle both const ATTRIBUTES = { and ATTRIBUTES: { - const attrMatch = content.match(/(?:const\s+)?ATTRIBUTES\s*=\s*\{/); - if (!attrMatch) return attributes; + // Numeric types + if (/^u?int\d+$/.test(shortName)) return 'number'; + if (/^data\d+$/.test(shortName)) return 'number'; - const startIdx = attrMatch.index + attrMatch[0].length; - let braceCount = 1; - let idx = startIdx; + // String types + if (shortName === 'string') return 'string'; + if (shortName === 'EUI64') return 'string'; - // Find matching closing brace - while (braceCount > 0 && idx < content.length) { - if (content[idx] === '{') braceCount++; - else if (content[idx] === '}') braceCount--; - idx++; + // Buffer types + if (shortName === 'octstr' || shortName === '_buffer' || shortName === 'seckey128') { + return 'Buffer'; } - let attrBlock = content.substring(startIdx, idx - 1); - - // Strip comments for easier parsing - attrBlock = stripComments(attrBlock); - - // Split into top-level attribute entries - const attrEntries = splitTopLevelEntries(attrBlock); - - for (const entry of attrEntries) { - // Match attribute name - may have leading whitespace/newlines after comment removal - const nameMatch = entry.match(/^\s*(\w+)\s*:/); - if (!nameMatch) continue; - - const attrName = nameMatch[1]; - - // Extract the type definition - look for type: followed by the type value - const typeMatch = entry.match(/type\s*:\s*([\s\S]+?)(?:,\s*(?:id|manufacturerId)\s*:|$)/); - if (!typeMatch) { - // Try alternative: type is last in object - const typeMatchAlt = entry.match(/type\s*:\s*([\s\S]+?)\s*[,}]?\s*$/); - if (typeMatchAlt) { - const typeStr = typeMatchAlt[1].trim().replace(/,\s*$/, ''); - const tsType = zclTypeToTS(typeStr); - attributes.push({ name: attrName, tsType }); + // Enum types - extract keys from args[0] + if (shortName === 'enum8' || shortName === 'enum16') { + if (args && args[0] && typeof args[0] === 'object') { + const keys = Object.keys(args[0]); + if (keys.length > 0) { + return keys.map(k => `'${k}'`).join(' | '); } - continue; } - - const typeStr = typeMatch[1].trim().replace(/,\s*$/, ''); - const tsType = zclTypeToTS(typeStr); - attributes.push({ name: attrName, tsType }); - } - - return attributes; -} - -/** - * Parse COMMANDS block from file content - */ -function parseCommandsBlock(content) { - const commands = []; - - const cmdMatch = content.match(/(?:const\s+)?COMMANDS\s*=\s*\{/); - if (!cmdMatch) return commands; - - const startIdx = cmdMatch.index + cmdMatch[0].length; - let braceCount = 1; - let idx = startIdx; - - while (braceCount > 0 && idx < content.length) { - if (content[idx] === '{') braceCount++; - else if (content[idx] === '}') braceCount--; - idx++; + return 'number'; } - let cmdBlock = content.substring(startIdx, idx - 1); - - // Strip comments for easier parsing - cmdBlock = stripComments(cmdBlock); - - // Parse commands - need to handle nested braces for args - // Split by top-level command definitions - const cmdEntries = splitTopLevelEntries(cmdBlock); - - for (const entry of cmdEntries) { - const nameMatch = entry.match(/^\s*(\w+)\s*:/); - if (!nameMatch) continue; - - const cmdName = nameMatch[1]; - const args = parseCommandArgs(entry); - - commands.push({ name: cmdName, args }); - } - - return commands; -} - -/** - * Split a block into top-level entries (handling nested braces and parentheses) - */ -function splitTopLevelEntries(block) { - const entries = []; - let braceCount = 0; - let parenCount = 0; - let currentEntry = ''; - let inString = false; - let stringChar = ''; - - for (let i = 0; i < block.length; i++) { - const char = block[i]; - const prevChar = i > 0 ? block[i - 1] : ''; - - // Handle strings - if ((char === '"' || char === "'") && prevChar !== '\\') { - if (!inString) { - inString = true; - stringChar = char; - } else if (char === stringChar) { - inString = false; + // Map/bitmap types - extract flag names from args + if (/^map\d+$/.test(shortName)) { + if (args && args.length > 0) { + const flags = args.filter(a => typeof a === 'string'); + if (flags.length > 0) { + return `Partial<{ ${flags.map(f => `${f}: boolean`).join('; ')} }>`; } } - - if (!inString) { - if (char === '{') braceCount++; - else if (char === '}') braceCount--; - else if (char === '(') parenCount++; - else if (char === ')') parenCount--; - else if (char === ',' && braceCount === 0 && parenCount === 0) { - if (currentEntry.trim()) { - entries.push(currentEntry.trim()); - } - currentEntry = ''; - continue; - } - } - - currentEntry += char; + return 'Partial>'; } - if (currentEntry.trim()) { - entries.push(currentEntry.trim()); + // Array types + if (shortName === 'Array0' || shortName === 'Array8' || shortName === 'Array16') { + return 'unknown[]'; } - return entries; + return 'unknown'; } /** - * Parse command arguments from a command entry + * Parse a cluster and extract its type information + * @param {Function} ClusterClass - Cluster class with static ATTRIBUTES/COMMANDS + * @returns {object} Cluster definition */ -function parseCommandArgs(cmdEntry) { - const args = []; - - // Find args block - const argsMatch = cmdEntry.match(/args\s*:\s*\{/); - if (!argsMatch) return args; - - const startIdx = argsMatch.index + argsMatch[0].length; - let braceCount = 1; - let idx = startIdx; +function parseCluster(ClusterClass) { + const clusterName = ClusterClass.NAME; + const clusterId = ClusterClass.ID; + const attributes = []; + const commands = []; - while (braceCount > 0 && idx < cmdEntry.length) { - if (cmdEntry[idx] === '{') braceCount++; - else if (cmdEntry[idx] === '}') braceCount--; - idx++; + // Parse attributes + const attrs = ClusterClass.ATTRIBUTES || {}; + for (const [name, def] of Object.entries(attrs)) { + if (def && def.type) { + attributes.push({ + name, + tsType: zclTypeToTS(def.type), + }); + } } - const argsBlock = cmdEntry.substring(startIdx, idx - 1); - - // Parse each argument - const argEntries = splitTopLevelEntries(argsBlock); - - for (const argEntry of argEntries) { - const nameMatch = argEntry.match(/^\s*(\w+)\s*:/); - if (!nameMatch) continue; - - const argName = nameMatch[1]; - const typeStr = argEntry.substring(argEntry.indexOf(':') + 1).trim(); - const tsType = zclTypeToTS(typeStr); - - args.push({ name: argName, tsType }); + // Parse commands + const cmds = ClusterClass.COMMANDS || {}; + for (const [name, def] of Object.entries(cmds)) { + const cmdArgs = []; + if (def && def.args) { + for (const [argName, argType] of Object.entries(def.args)) { + cmdArgs.push({ + name: argName, + tsType: zclTypeToTS(argType), + }); + } + } + commands.push({ name, args: cmdArgs }); } - return args; + return { + clusterName, clusterId, attributes, commands, + }; } /** * Convert cluster name to PascalCase interface name + * @param {string} clusterName + * @returns {string} */ function toInterfaceName(clusterName) { - // Handle special cases const name = clusterName.charAt(0).toUpperCase() + clusterName.slice(1); return `${name}Cluster`; } /** * Generate TypeScript interface for a cluster + * @param {object} cluster - Parsed cluster definition + * @returns {string} TypeScript interface code */ function generateClusterInterface(cluster) { const interfaceName = toInterfaceName(cluster.clusterName); @@ -371,10 +152,7 @@ function generateClusterInterface(cluster) { // Add command methods for (const cmd of cluster.commands) { if (cmd.args.length > 0) { - // Buffer arguments (octstr, securityKey128, buffer) are optional because ZCL allows - // empty octet strings (length 0). The data-types library serializes undefined/omitted - // Buffer args as empty Buffers. Example: DoorLock.lockDoor({ pinCode }) - pinCode is - // optional when the lock doesn't require PIN authentication. + // Buffer arguments are optional - ZCL allows empty octet strings const argsType = `{ ${cmd.args.map(a => `${a.name}${a.tsType === 'Buffer' ? '?' : ''}: ${a.tsType}`).join('; ')} }`; lines.push(` ${cmd.name}(args: ${argsType}): Promise;`); } else { @@ -389,6 +167,8 @@ function generateClusterInterface(cluster) { /** * Generate the full index.d.ts file + * @param {object[]} clusters - Array of parsed cluster definitions + * @returns {string} Complete TypeScript definitions file */ function generateTypesFile(clusters) { const lines = []; @@ -542,25 +322,22 @@ export interface ZCLNode { * Main entry point */ function main() { - console.log('Scanning cluster files...'); - - const files = fs.readdirSync(CLUSTERS_DIR) - .filter(f => f.endsWith('.js') && !SKIP_FILES.includes(f)); - - console.log(`Found ${files.length} cluster files`); + console.log('Loading cluster modules...'); + // Load all clusters via the index + const clustersModule = require('../lib/clusters'); const clusters = []; - for (const file of files) { - const filePath = path.join(CLUSTERS_DIR, file); - try { - const cluster = parseClusterFile(filePath); - if (cluster) { + // Get all exported cluster classes (end with 'Cluster') + for (const [name, value] of Object.entries(clustersModule)) { + if (name.endsWith('Cluster') && typeof value === 'function' && value.NAME) { + try { + const cluster = parseCluster(value); clusters.push(cluster); console.log(` ✓ ${cluster.clusterName} (${cluster.attributes.length} attrs, ${cluster.commands.length} cmds)`); + } catch (err) { + console.warn(` ✗ Failed to parse ${name}: ${err.message}`); } - } catch (err) { - console.warn(` ✗ Failed to parse ${file}: ${err.message}`); } } From 5b2a4f52c39adb5f2419f4c08b1d8745bd61bacc Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Mon, 9 Feb 2026 17:42:15 +0100 Subject: [PATCH 6/9] fix(types): improve type generation accuracy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix duplicate exports causing TS2484 compilation errors - Fix data40-64 types to return Buffer instead of number - Add missing type mappings: noData, single, double, EUI48, key128, enum4, enum32, _FixedString, _buffer8, _buffer16, map4 - Recursively resolve array element types (e.g. Array8(uint16) → number[]) - Add tsc --noEmit validation step to CI workflow --- .github/workflows/generate-types.yml | 3 +++ index.d.ts | 28 ++++++++++------------ scripts/generate-types.js | 36 ++++++++++++++++++---------- 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/.github/workflows/generate-types.yml b/.github/workflows/generate-types.yml index 454eac6..a225f32 100644 --- a/.github/workflows/generate-types.yml +++ b/.github/workflows/generate-types.yml @@ -34,6 +34,9 @@ jobs: - name: Generate TypeScript types run: npm run generate-types + - name: Validate TypeScript types compile + run: npx tsc --noEmit index.d.ts + - name: Check for changes id: check-changes run: | diff --git a/index.d.ts b/index.d.ts index 2659494..5ec4325 100644 --- a/index.d.ts +++ b/index.d.ts @@ -86,12 +86,12 @@ export interface AlarmsCluster extends ZCLNodeCluster { export interface AnalogInputClusterAttributes { description?: string; - maxPresentValue?: unknown; - minPresentValue?: unknown; + maxPresentValue?: number; + minPresentValue?: number; outOfService?: boolean; - presentValue?: unknown; + presentValue?: number; reliability?: 'noFaultDetected' | 'noSensor' | 'overRange' | 'underRange' | 'openLoop' | 'shortedLoop' | 'noOutput' | 'unreliableOther' | 'processError' | 'configurationError'; - resolution?: unknown; + resolution?: number; statusFlags?: Partial<{ inAlarm: boolean; fault: boolean; overridden: boolean; outOfService: boolean }>; applicationType?: number; } @@ -103,13 +103,13 @@ export interface AnalogInputCluster extends ZCLNodeCluster { export interface AnalogOutputClusterAttributes { description?: string; - maxPresentValue?: unknown; - minPresentValue?: unknown; + maxPresentValue?: number; + minPresentValue?: number; outOfService?: boolean; - presentValue?: unknown; + presentValue?: number; reliability?: 'noFaultDetected' | 'overRange' | 'underRange' | 'openLoop' | 'shortedLoop' | 'unreliableOther' | 'processError' | 'configurationError'; - relinquishDefault?: unknown; - resolution?: unknown; + relinquishDefault?: number; + resolution?: number; statusFlags?: Partial<{ inAlarm: boolean; fault: boolean; overridden: boolean; outOfService: boolean }>; applicationType?: number; } @@ -122,9 +122,9 @@ export interface AnalogOutputCluster extends ZCLNodeCluster { export interface AnalogValueClusterAttributes { description?: string; outOfService?: boolean; - presentValue?: unknown; + presentValue?: number; reliability?: 'noFaultDetected' | 'overRange' | 'underRange' | 'openLoop' | 'shortedLoop' | 'unreliableOther' | 'processError' | 'configurationError'; - relinquishDefault?: unknown; + relinquishDefault?: number; statusFlags?: Partial<{ inAlarm: boolean; fault: boolean; overridden: boolean; outOfService: boolean }>; applicationType?: number; } @@ -415,7 +415,7 @@ export interface GroupsCluster extends ZCLNodeCluster { writeAttributes(attributes: Partial): Promise; addGroup(args: { groupId: number; groupName: string }): Promise; viewGroup(args: { groupId: number }): Promise; - getGroupMembership(args: { groupIds: unknown }): Promise; + getGroupMembership(args: { groupIds: number[] }): Promise; removeGroup(args: { groupId: number }): Promise; removeAllGroups(): Promise; addGroupIfIdentify(args: { groupId: number; groupName: string }): Promise; @@ -1110,6 +1110,4 @@ declare module "zigbee-clusters" { export const TimeCluster: TimeCluster; export const TouchlinkCluster: TouchlinkCluster; export const WindowCoveringCluster: WindowCoveringCluster; -} - -export { ZCLNode, ZCLNodeCluster, ZCLNodeEndpoint, ClusterRegistry }; \ No newline at end of file +} \ No newline at end of file diff --git a/scripts/generate-types.js b/scripts/generate-types.js index 8b0244a..cd294bf 100644 --- a/scripts/generate-types.js +++ b/scripts/generate-types.js @@ -22,24 +22,35 @@ function zclTypeToTS(dataType) { const { shortName, args } = dataType; + // No data + if (shortName === 'noData') return 'null'; + // Boolean if (shortName === 'bool') return 'boolean'; // Numeric types if (/^u?int\d+$/.test(shortName)) return 'number'; - if (/^data\d+$/.test(shortName)) return 'number'; + // data8-32 use readUIntBE, return number + if (/^data(8|16|24|32)$/.test(shortName)) return 'number'; + // data40-64 use buf.slice, return Buffer + if (/^data(40|48|56|64)$/.test(shortName)) return 'Buffer'; + // Float types + if (shortName === 'single' || shortName === 'double') return 'number'; // String types - if (shortName === 'string') return 'string'; - if (shortName === 'EUI64') return 'string'; + if (shortName === 'string' || shortName === '_FixedString') return 'string'; + // EUI addresses return colon-separated hex strings + if (shortName === 'EUI64' || shortName === 'EUI48') return 'string'; + // key128 returns colon-separated hex string + if (shortName === 'key128') return 'string'; // Buffer types - if (shortName === 'octstr' || shortName === '_buffer' || shortName === 'seckey128') { + if (shortName === 'octstr' || shortName === '_buffer' || shortName === '_buffer8' || shortName === '_buffer16') { return 'Buffer'; } // Enum types - extract keys from args[0] - if (shortName === 'enum8' || shortName === 'enum16') { + if (/^enum(4|8|16|32)$/.test(shortName)) { if (args && args[0] && typeof args[0] === 'object') { const keys = Object.keys(args[0]); if (keys.length > 0) { @@ -50,7 +61,7 @@ function zclTypeToTS(dataType) { } // Map/bitmap types - extract flag names from args - if (/^map\d+$/.test(shortName)) { + if (/^map(4|\d+)$/.test(shortName)) { if (args && args.length > 0) { const flags = args.filter(a => typeof a === 'string'); if (flags.length > 0) { @@ -60,8 +71,13 @@ function zclTypeToTS(dataType) { return 'Partial>'; } - // Array types - if (shortName === 'Array0' || shortName === 'Array8' || shortName === 'Array16') { + // Array types (note: shortName has underscore prefix: _Array0, _Array8) + // Recursively determine element type from args[0] + if (/^_?Array(0|8|16)$/.test(shortName)) { + if (args && args[0]) { + const elementType = zclTypeToTS(args[0]); + return `${elementType}[]`; + } return 'unknown[]'; } @@ -310,10 +326,6 @@ export interface ZCLNode { } lines.push('}'); - lines.push(''); - - // Also export at top level for ESM - lines.push('export { ZCLNode, ZCLNodeCluster, ZCLNodeEndpoint, ClusterRegistry };'); return lines.join('\n'); } From 87e7fa23802f4051c6b41e97ff8dcc62c5416358 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Mon, 9 Feb 2026 17:52:07 +0100 Subject: [PATCH 7/9] refactor(types): remove redundant comments from ZCLNodeCluster interface --- index.d.ts | 24 ------------------------ scripts/generate-types.js | 24 ------------------------ 2 files changed, 48 deletions(-) diff --git a/index.d.ts b/index.d.ts index 5ec4325..68cbc9f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -15,43 +15,25 @@ type ConstructorOptions = { }; export interface ZCLNodeCluster extends EventEmitter { - /** - * Command which requests the remote cluster to report its generated commands. - */ discoverCommandsGenerated(opts?: { startValue?: number; maxResults?: number; }): Promise; - /** - * Command which requests the remote cluster to report its received commands. - */ discoverCommandsReceived(opts?: { startValue?: number; maxResults?: number; }): Promise; - /** - * Command which reads a given set of attributes from the remote cluster. - */ readAttributes( attributeNames: string[], opts?: { timeout?: number } ): Promise<{ [x: string]: unknown }>; - /** - * Command which writes a given set of attribute key-value pairs to the remote cluster. - */ writeAttributes(attributes?: object): Promise; - /** - * Command which configures attribute reporting for the given attributes on the remote cluster. - */ configureReporting(attributes?: object): Promise; - /** - * Command which retrieves the reporting configurations for the given attributes. - */ readReportingConfiguration(attributes?: (string | number)[]): Promise<{ status: string; direction: 'reported' | 'received'; @@ -63,14 +45,8 @@ export interface ZCLNodeCluster extends EventEmitter { timeoutPeriod?: number; }[]>; - /** - * Command which discovers the implemented attributes on the remote cluster. - */ discoverAttributes(): Promise<(string | number)[]>; - /** - * Command which discovers the implemented attributes with access control info. - */ discoverAttributesExtended(): Promise<{ name?: string; id: number; diff --git a/scripts/generate-types.js b/scripts/generate-types.js index cd294bf..083fc07 100644 --- a/scripts/generate-types.js +++ b/scripts/generate-types.js @@ -211,43 +211,25 @@ type ConstructorOptions = { // Base ZCLNodeCluster interface lines.push(`export interface ZCLNodeCluster extends EventEmitter { - /** - * Command which requests the remote cluster to report its generated commands. - */ discoverCommandsGenerated(opts?: { startValue?: number; maxResults?: number; }): Promise; - /** - * Command which requests the remote cluster to report its received commands. - */ discoverCommandsReceived(opts?: { startValue?: number; maxResults?: number; }): Promise; - /** - * Command which reads a given set of attributes from the remote cluster. - */ readAttributes( attributeNames: string[], opts?: { timeout?: number } ): Promise<{ [x: string]: unknown }>; - /** - * Command which writes a given set of attribute key-value pairs to the remote cluster. - */ writeAttributes(attributes?: object): Promise; - /** - * Command which configures attribute reporting for the given attributes on the remote cluster. - */ configureReporting(attributes?: object): Promise; - /** - * Command which retrieves the reporting configurations for the given attributes. - */ readReportingConfiguration(attributes?: (string | number)[]): Promise<{ status: string; direction: 'reported' | 'received'; @@ -259,14 +241,8 @@ type ConstructorOptions = { timeoutPeriod?: number; }[]>; - /** - * Command which discovers the implemented attributes on the remote cluster. - */ discoverAttributes(): Promise<(string | number)[]>; - /** - * Command which discovers the implemented attributes with access control info. - */ discoverAttributesExtended(): Promise<{ name?: string; id: number; From e946db2d54e9a9ae4145d63db0f98c44302be0ea Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Tue, 10 Feb 2026 13:03:37 +0100 Subject: [PATCH 8/9] feat(types): add typed command responses and array element types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Parse command response definitions and generate typed return values - Recursively resolve array element types (e.g. Array8(uint16) → number[]) - Commands with responses now return Promise<{...}> instead of Promise --- index.d.ts | 64 +++++++++++++++++++-------------------- scripts/generate-types.js | 22 ++++++++++++-- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/index.d.ts b/index.d.ts index 68cbc9f..ebdd4d5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -309,32 +309,32 @@ export interface DoorLockClusterAttributes { export interface DoorLockCluster extends ZCLNodeCluster { readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; writeAttributes(attributes: Partial): Promise; - lockDoor(args: { pinCode?: Buffer }): Promise; - unlockDoor(args: { pinCode?: Buffer }): Promise; - toggle(args: { pinCode?: Buffer }): Promise; - unlockWithTimeout(args: { timeout: number; pinCode?: Buffer }): Promise; - getLogRecord(args: { logIndex: number }): Promise; - setPINCode(args: { userId: number; userStatus: 'available' | 'occupiedEnabled' | 'occupiedDisabled' | 'notSupported'; userType: 'unrestricted' | 'yearDayScheduleUser' | 'weekDayScheduleUser' | 'masterUser' | 'nonAccessUser' | 'notSupported'; pinCode?: Buffer }): Promise; - getPINCode(args: { userId: number }): Promise; - clearPINCode(args: { userId: number }): Promise; - clearAllPINCodes(): Promise; - setUserStatus(args: { userId: number; userStatus: 'available' | 'occupiedEnabled' | 'occupiedDisabled' | 'notSupported' }): Promise; - getUserStatus(args: { userId: number }): Promise; - setWeekDaySchedule(args: { scheduleId: number; userId: number; daysMask: Partial<{ sunday: boolean; monday: boolean; tuesday: boolean; wednesday: boolean; thursday: boolean; friday: boolean; saturday: boolean }>; startHour: number; startMinute: number; endHour: number; endMinute: number }): Promise; - getWeekDaySchedule(args: { scheduleId: number; userId: number }): Promise; - clearWeekDaySchedule(args: { scheduleId: number; userId: number }): Promise; - setYearDaySchedule(args: { scheduleId: number; userId: number; localStartTime: number; localEndTime: number }): Promise; - getYearDaySchedule(args: { scheduleId: number; userId: number }): Promise; - clearYearDaySchedule(args: { scheduleId: number; userId: number }): Promise; - setHolidaySchedule(args: { holidayScheduleId: number; localStartTime: number; localEndTime: number; operatingModeDuringHoliday: 'normal' | 'vacation' | 'privacy' | 'noRFLockOrUnlock' | 'passage' }): Promise; - getHolidaySchedule(args: { holidayScheduleId: number }): Promise; - clearHolidaySchedule(args: { holidayScheduleId: number }): Promise; - setUserType(args: { userId: number; userType: 'unrestricted' | 'yearDayScheduleUser' | 'weekDayScheduleUser' | 'masterUser' | 'nonAccessUser' | 'notSupported' }): Promise; - getUserType(args: { userId: number }): Promise; - setRFIDCode(args: { userId: number; userStatus: 'available' | 'occupiedEnabled' | 'occupiedDisabled' | 'notSupported'; userType: 'unrestricted' | 'yearDayScheduleUser' | 'weekDayScheduleUser' | 'masterUser' | 'nonAccessUser' | 'notSupported'; rfidCode?: Buffer }): Promise; - getRFIDCode(args: { userId: number }): Promise; - clearRFIDCode(args: { userId: number }): Promise; - clearAllRFIDCodes(): Promise; + lockDoor(args: { pinCode?: Buffer }): Promise<{ status: number }>; + unlockDoor(args: { pinCode?: Buffer }): Promise<{ status: number }>; + toggle(args: { pinCode?: Buffer }): Promise<{ status: number }>; + unlockWithTimeout(args: { timeout: number; pinCode?: Buffer }): Promise<{ status: number }>; + getLogRecord(args: { logIndex: number }): Promise<{ logEntryId: number; timestamp: number; eventType: number; source: number; eventIdOrAlarmCode: number; userId: number; pin: Buffer }>; + setPINCode(args: { userId: number; userStatus: 'available' | 'occupiedEnabled' | 'occupiedDisabled' | 'notSupported'; userType: 'unrestricted' | 'yearDayScheduleUser' | 'weekDayScheduleUser' | 'masterUser' | 'nonAccessUser' | 'notSupported'; pinCode?: Buffer }): Promise<{ status: number }>; + getPINCode(args: { userId: number }): Promise<{ userId: number; userStatus: 'available' | 'occupiedEnabled' | 'occupiedDisabled' | 'notSupported'; userType: 'unrestricted' | 'yearDayScheduleUser' | 'weekDayScheduleUser' | 'masterUser' | 'nonAccessUser' | 'notSupported'; pinCode: Buffer }>; + clearPINCode(args: { userId: number }): Promise<{ status: number }>; + clearAllPINCodes(): Promise<{ status: number }>; + setUserStatus(args: { userId: number; userStatus: 'available' | 'occupiedEnabled' | 'occupiedDisabled' | 'notSupported' }): Promise<{ status: number }>; + getUserStatus(args: { userId: number }): Promise<{ userId: number; userStatus: 'available' | 'occupiedEnabled' | 'occupiedDisabled' | 'notSupported' }>; + setWeekDaySchedule(args: { scheduleId: number; userId: number; daysMask: Partial<{ sunday: boolean; monday: boolean; tuesday: boolean; wednesday: boolean; thursday: boolean; friday: boolean; saturday: boolean }>; startHour: number; startMinute: number; endHour: number; endMinute: number }): Promise<{ status: number }>; + getWeekDaySchedule(args: { scheduleId: number; userId: number }): Promise<{ scheduleId: number; userId: number; status: number; daysMask: Partial<{ sunday: boolean; monday: boolean; tuesday: boolean; wednesday: boolean; thursday: boolean; friday: boolean; saturday: boolean }>; startHour: number; startMinute: number; endHour: number; endMinute: number }>; + clearWeekDaySchedule(args: { scheduleId: number; userId: number }): Promise<{ status: number }>; + setYearDaySchedule(args: { scheduleId: number; userId: number; localStartTime: number; localEndTime: number }): Promise<{ status: number }>; + getYearDaySchedule(args: { scheduleId: number; userId: number }): Promise<{ scheduleId: number; userId: number; status: number; localStartTime: number; localEndTime: number }>; + clearYearDaySchedule(args: { scheduleId: number; userId: number }): Promise<{ status: number }>; + setHolidaySchedule(args: { holidayScheduleId: number; localStartTime: number; localEndTime: number; operatingModeDuringHoliday: 'normal' | 'vacation' | 'privacy' | 'noRFLockOrUnlock' | 'passage' }): Promise<{ status: number }>; + getHolidaySchedule(args: { holidayScheduleId: number }): Promise<{ holidayScheduleId: number; status: number; localStartTime: number; localEndTime: number; operatingMode: 'normal' | 'vacation' | 'privacy' | 'noRFLockOrUnlock' | 'passage' }>; + clearHolidaySchedule(args: { holidayScheduleId: number }): Promise<{ status: number }>; + setUserType(args: { userId: number; userType: 'unrestricted' | 'yearDayScheduleUser' | 'weekDayScheduleUser' | 'masterUser' | 'nonAccessUser' | 'notSupported' }): Promise<{ status: number }>; + getUserType(args: { userId: number }): Promise<{ userId: number; userType: 'unrestricted' | 'yearDayScheduleUser' | 'weekDayScheduleUser' | 'masterUser' | 'nonAccessUser' | 'notSupported' }>; + setRFIDCode(args: { userId: number; userStatus: 'available' | 'occupiedEnabled' | 'occupiedDisabled' | 'notSupported'; userType: 'unrestricted' | 'yearDayScheduleUser' | 'weekDayScheduleUser' | 'masterUser' | 'nonAccessUser' | 'notSupported'; rfidCode?: Buffer }): Promise<{ status: number }>; + getRFIDCode(args: { userId: number }): Promise<{ userId: number; userStatus: 'available' | 'occupiedEnabled' | 'occupiedDisabled' | 'notSupported'; userType: 'unrestricted' | 'yearDayScheduleUser' | 'weekDayScheduleUser' | 'masterUser' | 'nonAccessUser' | 'notSupported'; rfidCode: Buffer }>; + clearRFIDCode(args: { userId: number }): Promise<{ status: number }>; + clearAllRFIDCodes(): Promise<{ status: number }>; operationEventNotification(args: { operationEventSource: number; operationEventCode: number; userId: number; pin?: Buffer; zigBeeLocalTime: number; data?: Buffer }): Promise; programmingEventNotification(args: { programEventSource: number; programEventCode: number; userId: number; pin?: Buffer; userType: 'unrestricted' | 'yearDayScheduleUser' | 'weekDayScheduleUser' | 'masterUser' | 'nonAccessUser' | 'notSupported'; userStatus: 'available' | 'occupiedEnabled' | 'occupiedDisabled' | 'notSupported'; zigBeeLocalTime: number; data?: Buffer }): Promise; } @@ -389,10 +389,10 @@ export interface GroupsClusterAttributes { export interface GroupsCluster extends ZCLNodeCluster { readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; writeAttributes(attributes: Partial): Promise; - addGroup(args: { groupId: number; groupName: string }): Promise; - viewGroup(args: { groupId: number }): Promise; - getGroupMembership(args: { groupIds: number[] }): Promise; - removeGroup(args: { groupId: number }): Promise; + addGroup(args: { groupId: number; groupName: string }): 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 }>; + viewGroup(args: { groupId: number }): 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 }>; + getGroupMembership(args: { groupIds: number[] }): Promise<{ capacity: number; groups: number[] }>; + removeGroup(args: { groupId: number }): 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 }>; removeAllGroups(): Promise; addGroupIfIdentify(args: { groupId: number; groupName: string }): Promise; } @@ -428,7 +428,7 @@ export interface IdentifyCluster extends ZCLNodeCluster { readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; writeAttributes(attributes: Partial): Promise; identify(args: { identifyTime: number }): Promise; - identifyQuery(): Promise; + identifyQuery(): Promise<{ timeout: number }>; triggerEffect(args: { effectIdentifier: 'blink' | 'breathe' | 'okay' | 'channelChange' | 'finish' | 'stop'; effectVariant: number }): Promise; } @@ -928,7 +928,7 @@ export interface TimeCluster extends ZCLNodeCluster { } export interface TouchlinkCluster extends ZCLNodeCluster { - getGroups(args: { startIdx: number }): Promise; + getGroups(args: { startIdx: number }): Promise<{ total: number; startIndex: number; groups: unknown[] }>; } export interface WindowCoveringClusterAttributes { diff --git a/scripts/generate-types.js b/scripts/generate-types.js index 083fc07..4b7d8d1 100644 --- a/scripts/generate-types.js +++ b/scripts/generate-types.js @@ -110,6 +110,7 @@ function parseCluster(ClusterClass) { const cmds = ClusterClass.COMMANDS || {}; for (const [name, def] of Object.entries(cmds)) { const cmdArgs = []; + const responseArgs = []; if (def && def.args) { for (const [argName, argType] of Object.entries(def.args)) { cmdArgs.push({ @@ -118,7 +119,16 @@ function parseCluster(ClusterClass) { }); } } - commands.push({ name, args: cmdArgs }); + // Parse response type if present + if (def && def.response && def.response.args) { + for (const [argName, argType] of Object.entries(def.response.args)) { + responseArgs.push({ + name: argName, + tsType: zclTypeToTS(argType), + }); + } + } + commands.push({ name, args: cmdArgs, responseArgs }); } return { @@ -167,12 +177,18 @@ function generateClusterInterface(cluster) { // Add command methods for (const cmd of cluster.commands) { + // Determine return type based on response args + let returnType = 'void'; + if (cmd.responseArgs && cmd.responseArgs.length > 0) { + returnType = `{ ${cmd.responseArgs.map(a => `${a.name}: ${a.tsType}`).join('; ')} }`; + } + if (cmd.args.length > 0) { // Buffer arguments are optional - ZCL allows empty octet strings const argsType = `{ ${cmd.args.map(a => `${a.name}${a.tsType === 'Buffer' ? '?' : ''}: ${a.tsType}`).join('; ')} }`; - lines.push(` ${cmd.name}(args: ${argsType}): Promise;`); + lines.push(` ${cmd.name}(args: ${argsType}): Promise<${returnType}>;`); } else { - lines.push(` ${cmd.name}(): Promise;`); + lines.push(` ${cmd.name}(): Promise<${returnType}>;`); } } From 299d48b644d5894a713bb86972711fb9aceef5bf Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Tue, 10 Feb 2026 13:07:55 +0100 Subject: [PATCH 9/9] fix(types): make args optional when all properties are optional - Commands with only Buffer args now have optional args parameter - e.g. lockDoor(args?: { pinCode?: Buffer }) instead of requiring {} - Add test verifying lockDoor() works without arguments --- index.d.ts | 6 +++--- scripts/generate-types.js | 4 +++- test/doorLock.js | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index ebdd4d5..3eb6ec8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -309,9 +309,9 @@ export interface DoorLockClusterAttributes { export interface DoorLockCluster extends ZCLNodeCluster { readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; writeAttributes(attributes: Partial): Promise; - lockDoor(args: { pinCode?: Buffer }): Promise<{ status: number }>; - unlockDoor(args: { pinCode?: Buffer }): Promise<{ status: number }>; - toggle(args: { pinCode?: Buffer }): Promise<{ status: number }>; + lockDoor(args?: { pinCode?: Buffer }): Promise<{ status: number }>; + unlockDoor(args?: { pinCode?: Buffer }): Promise<{ status: number }>; + toggle(args?: { pinCode?: Buffer }): Promise<{ status: number }>; unlockWithTimeout(args: { timeout: number; pinCode?: Buffer }): Promise<{ status: number }>; getLogRecord(args: { logIndex: number }): Promise<{ logEntryId: number; timestamp: number; eventType: number; source: number; eventIdOrAlarmCode: number; userId: number; pin: Buffer }>; setPINCode(args: { userId: number; userStatus: 'available' | 'occupiedEnabled' | 'occupiedDisabled' | 'notSupported'; userType: 'unrestricted' | 'yearDayScheduleUser' | 'weekDayScheduleUser' | 'masterUser' | 'nonAccessUser' | 'notSupported'; pinCode?: Buffer }): Promise<{ status: number }>; diff --git a/scripts/generate-types.js b/scripts/generate-types.js index 4b7d8d1..a99aa37 100644 --- a/scripts/generate-types.js +++ b/scripts/generate-types.js @@ -185,8 +185,10 @@ function generateClusterInterface(cluster) { if (cmd.args.length > 0) { // Buffer arguments are optional - ZCL allows empty octet strings + const allArgsOptional = cmd.args.every(a => a.tsType === 'Buffer'); const argsType = `{ ${cmd.args.map(a => `${a.name}${a.tsType === 'Buffer' ? '?' : ''}: ${a.tsType}`).join('; ')} }`; - lines.push(` ${cmd.name}(args: ${argsType}): Promise<${returnType}>;`); + // If all args are optional, make the entire args object optional + lines.push(` ${cmd.name}(args${allArgsOptional ? '?' : ''}: ${argsType}): Promise<${returnType}>;`); } else { lines.push(` ${cmd.name}(): Promise<${returnType}>;`); } diff --git a/test/doorLock.js b/test/doorLock.js index c14ff0d..4c9c9ee 100644 --- a/test/doorLock.js +++ b/test/doorLock.js @@ -33,6 +33,31 @@ describe('Door Lock', function() { assert.deepStrictEqual(receivedData.pinCode, Buffer.from([0x31, 0x32, 0x33, 0x34])); }); + it('should receive lockDoor without arguments', async function() { + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, + inputClusters: [DoorLockCluster.ID], + }], + }); + + let called = false; + node.endpoints[1].bind('doorLock', new (class extends BoundCluster { + + async lockDoor(data) { + called = true; + // pinCode should be empty buffer when not provided + assert.deepStrictEqual(data.pinCode, Buffer.from([])); + } + + })()); + + // Call without any arguments - should not throw + await node.endpoints[1].clusters.doorLock.lockDoor(); + assert.strictEqual(called, true); + }); + it('should receive unlockDoor', async function() { const node = createMockNode({ loopback: true,