From 0ebb07e2e62bae2d26885fe870dc326fed07fc46 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Mon, 26 Jan 2026 19:22:44 +0100 Subject: [PATCH 1/3] 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 49b5b1461d5e313e9bc2b5c387e5ddbe0efa566a Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Mon, 26 Jan 2026 21:50:19 +0100 Subject: [PATCH 2/3] 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 8c0be67895c5bde0cb0f61b2c18af4b17ed537a0 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Tue, 27 Jan 2026 09:40:03 +0100 Subject: [PATCH 3/3] 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 {