diff --git a/.github/workflows/generate-types.yml b/.github/workflows/generate-types.yml new file mode 100644 index 0000000..a225f32 --- /dev/null +++ b/.github/workflows/generate-types.yml @@ -0,0 +1,63 @@ +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: Validate TypeScript types compile + run: npx tsc --noEmit index.d.ts + + - 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 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7a0e5a4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,451 @@ +# AGENTS.md + +This file provides guidance to AI agents when working with code in this repository. + +## Commands + +```bash +npm test # Run all tests (mocha) +npm run lint # ESLint (extends athom config) +npm run build # Generate JSDoc documentation +``` + +Run single test file: +```bash +npx mocha test/onOff.js +``` + +## Architecture + +Zigbee Cluster Library (ZCL) implementation for Node.js, designed for Homey's Zigbee stack. + +### Core Classes + +- **ZCLNode** (`lib/Node.js`) - Entry point. Wraps Homey's ZigBeeNode, manages endpoints, routes incoming frames. +- **Endpoint** (`lib/Endpoint.js`) - Represents device endpoint. Contains `clusters` (client) and `bindings` (server). Routes frames to appropriate cluster. +- **Cluster** (`lib/Cluster.js`) - Base class for all clusters. Handles frame parsing, command execution, attribute operations. Commands auto-generate methods via `_addPrototypeMethods`. +- **BoundCluster** (`lib/BoundCluster.js`) - Server-side cluster for receiving commands from remote nodes. + +### Data Flow + +``` +ZCLNode.handleFrame() → Endpoint.handleFrame() → Cluster/BoundCluster.handleFrame() +Cluster.sendCommand() → Endpoint.sendFrame() → ZCLNode.sendFrame() +``` + +### Cluster Implementation Pattern + +Each cluster in `lib/clusters/` follows: +1. Define `ATTRIBUTES` object with `{id, type}` using `ZCLDataTypes` +2. Define `COMMANDS` object with `{id, args?}` +3. Extend `Cluster` with static getters: `ID`, `NAME`, `ATTRIBUTES`, `COMMANDS` +4. Call `Cluster.addCluster(MyCluster)` to register + +Example: `lib/clusters/onOff.js` + +### Key Types + +- `ZCLDataTypes` - Primitive types (uint8, bool, enum8, etc.) from `@athombv/data-types` +- `ZCLStruct` - Composite types for command arguments +- `CLUSTER` constant - Maps cluster names to `{ID, NAME, ATTRIBUTES, COMMANDS}` + +### Cluster ID References + +Prefer `Cluster.ID` over hardcoded numbers: +```javascript +// Good +const OnOffCluster = require('../lib/clusters/onOff'); +inputClusters: [OnOffCluster.ID] + +// Avoid +inputClusters: [6] +inputClusters: [0x0006] +``` + +### Test Pattern + +Tests use mock nodes from `test/util/mockNode.js`: + +**Single node with loopback** (command → same node's BoundCluster): +```javascript +const { createMockNode } = require('./util'); +const OnOffCluster = require('../lib/clusters/onOff'); + +const node = createMockNode({ + loopback: true, + endpoints: [{ endpointId: 1, inputClusters: [OnOffCluster.ID] }], +}); + +node.endpoints[1].bind('onOff', new (class extends BoundCluster { + async setOn() { /* handle command */ } +})()); + +await node.endpoints[1].clusters.onOff.setOn(); // loops back to BoundCluster +``` + +**Connected node pair** (node A sends → node B receives): +```javascript +const { createConnectedNodePair } = require('./util'); + +const [sender, receiver] = createConnectedNodePair( + { endpoints: [{ endpointId: 1, inputClusters: [6] }] }, + { endpoints: [{ endpointId: 1, inputClusters: [6] }] }, +); + +receiver.endpoints[1].bind('onOff', new BoundCluster()); +await sender.endpoints[1].clusters.onOff.toggle(); +``` + +**Server-to-client notifications** (device → controller) still require manual frames: +```javascript +const { ZCLStandardHeader } = require('../lib/zclFrames'); + +node.endpoints[1].clusters.iasZone.onZoneStatusChangeNotification = data => { ... }; + +const frame = new ZCLStandardHeader(); +frame.cmdId = IASZoneCluster.COMMANDS.zoneStatusChangeNotification.id; +frame.frameControl.directionToClient = true; +frame.frameControl.clusterSpecific = true; +frame.data = Buffer.from([...]); + +node.handleFrame(1, IASZoneCluster.ID, frame.toBuffer(), {}); +``` + +**Preset mock devices** for common sensor types: +```javascript +const { MOCK_DEVICES } = require('./util'); + +const sensor = MOCK_DEVICES.motionSensor(); +const boundCluster = sensor.endpoints[1].bindings.iasZone; +``` + +## Key Files + +- `index.js` - Public exports +- `lib/clusters/index.js` - All cluster exports + `CLUSTER` constant +- `lib/zclTypes.js` - ZCL data types +- `lib/zclFrames.js` - ZCL frame parsing/serialization + +--- + +## Adding/Updating Cluster Definitions + +Reusable guide for adding new clusters or updating existing cluster definitions. + +### Source Reference + +- **Spec PDF**: `docs/zigbee-cluster-specification-r8.pdf` +- **Extract text**: `pdftotext docs/zigbee-cluster-specification-r8.pdf -` +- **Cluster files**: `lib/clusters/*.js` + +--- + +### File Structure Template + +```javascript +'use strict'; + +const Cluster = require('../Cluster'); +const { ZCLDataTypes } = require('../zclTypes'); + +// Reusable enum definitions (if needed) +const EXAMPLE_ENUM = ZCLDataTypes.enum8({ + value1: 0, + value2: 1, +}); + +// ============================================================================ +// Server Attributes +// ============================================================================ +const ATTRIBUTES = { + // Section Name (0x0000 - 0x000F) + + // Description from spec, copied 1-on-1. + // Multi-line if needed, wrapped at 100 chars. + attrName: { id: 0x0000, type: ZCLDataTypes.uint8 }, // Mandatory +}; + +// ============================================================================ +// Commands +// ============================================================================ +const COMMANDS = { + // --- Client to Server Commands --- + + // Description from spec. + commandName: { // Mandatory + id: 0x0000, + args: { + argName: ZCLDataTypes.uint8, + }, + response: { + id: 0x0000, + args: { status: ZCLDataTypes.uint8 }, + }, + }, + + // --- Server to Client Commands --- + + // Description from spec. + notificationName: { // Optional + id: 0x0020, // 32 + direction: Cluster.DIRECTION_SERVER_TO_CLIENT, + args: { + argName: ZCLDataTypes.uint8, + }, + }, +}; + +class ExampleCluster extends Cluster { + static get ID() { + return 0x0000; // Add decimal comment if > 9, e.g.: return 0x0101; // 257 + } + + static get NAME() { + return 'example'; + } + + static get ATTRIBUTES() { + return ATTRIBUTES; + } + + static get COMMANDS() { + return COMMANDS; + } +} + +Cluster.addCluster(ExampleCluster); +module.exports = ExampleCluster; +``` + +--- + +### Comment Format Rules + +1. **Description placement**: ABOVE the attribute/command +2. **M/O marker placement**: + - Single-line: at END of line (`attrName: { ... }, // Mandatory`) + - Multi-line: on OPENING brace (`attrName: { // Mandatory`) + - NEVER on closing brace +3. **Copy exactly**: Text from spec, 1-on-1 +4. **Skip if >5 sentences**: Skip and only refer to section in spec +5. **Line wrap**: Respect 100 char limit (ESLint) +6. **M/O source for attrs**: Server-side column in spec table +7. **M/O source for cmds**: + - Server "receives" → server-side M/O + - Server "generates" → client-side M/O +8. **Conditional (M*)**: Use `// Conditional¹` with footnote for attrs/cmds marked "M*" in spec + - Reason on new line below: `// ¹ Reason here` + - Shared reasons use same footnote number + +#### Example + +```javascript +currentLevel: { id: 0x0000, type: ZCLDataTypes.uint8 }, // Mandatory +pirOccupiedToUnoccupiedDelay: { id: 0x0010, type: ZCLDataTypes.uint16 }, // 16, Conditional¹ +pirUnoccupiedToOccupiedDelay: { id: 0x0011, type: ZCLDataTypes.uint16 }, // 17, Conditional¹ +// ¹ PIR sensor type supported +``` + +--- + +### Attribute Definition Rules + +| Field | Format | Notes | +|-------|--------|-------| +| `id` | Hex (`0x0000`) | Always 4-digit format (0x0000); add decimal comment if > 9 | +| `type` | `ZCLDataTypes.*` | See type reference below | +| M/O | Inline comment | `// Mandatory`, `// Optional`, or `// Conditional` | + +#### Hex with Decimal Comments + +For hex values > 9 (where hex differs from decimal), add decimal in comment: +```javascript +id: 0x000A, // 10 +id: 0x0010, // 16 +id: 0x0100, // 256 +``` + +For values 0-9, no decimal comment needed: +```javascript +id: 0x0000, +id: 0x0009, +``` + +For multi-line definitions, decimal goes on the `id:` line, M/O on opening brace: +```javascript +operatingMode: { // Optional + id: 0x0025, // 37 + type: ZCLDataTypes.enum8({ + normal: 0, + vacation: 1, + }), +}, +``` + +#### Section Comments + +Group attrs by function with section headers: +```javascript +// Section Name (0x0000 - 0x000F) +attr1: { ... }, +attr2: { ... }, + +// Another Section (0x0010 - 0x001F) +attr3: { ... }, +``` + +--- + +### Command Definition Rules + +| Field | Required | Notes | +|-------|----------|-------| +| `id` | Yes | Always 4-digit hex format (0x0000); add decimal comment if > 9 | +| `args` | If has params | Object with typed fields | +| `response` | If expects response | Has own `id` and `args` | +| `direction` | For server→client | `Cluster.DIRECTION_SERVER_TO_CLIENT` | + +#### Command Sections + +```javascript +// --- Client to Server Commands --- +lockDoor: { id: 0x0000, ... }, // Mandatory + +// --- Server to Client Commands --- +operationEventNotification: { // Optional + id: 0x0020, // 32 + direction: Cluster.DIRECTION_SERVER_TO_CLIENT, + ... +}, +``` + +#### Command Direction Rules + +Focus on **client→server commands** with inline `response:` when applicable: +```javascript +lockDoor: { + id: 0x0000, + args: { pinCode: ZCLDataTypes.octstr }, + response: { + id: 0x0000, + args: { status: ZCLDataTypes.uint8 }, + }, +}, +``` + +**Server→client commands** (events/notifications) should be evaluated per case: +- Implement if commonly needed (e.g., `operationEventNotification` for door locks) +- Skip obscure or rarely-used notifications unless specifically requested +- These require `direction: Cluster.DIRECTION_SERVER_TO_CLIENT` + +--- + +### ZCLDataTypes Reference + +#### Primitives +- `bool`, `uint8`, `uint16`, `uint24`, `uint32`, `uint48`, `uint64` +- `int8`, `int16`, `int24`, `int32` +- `string`, `octstr` + +#### Enums +```javascript +ZCLDataTypes.enum8({ + valueName: 0, + anotherValue: 1, +}) +``` + +#### Bitmaps +```javascript +ZCLDataTypes.map8('bit0', 'bit1', 'bit2') +ZCLDataTypes.map16('bit0', 'bit1', ...) +ZCLDataTypes.map64(...) +``` + +#### Arrays +```javascript +ZCLDataTypes.Array0(ZCLDataTypes.uint8) +ZCLDataTypes.Array8(...) +``` + +#### Reusable Enums +Define at module level if used multiple times: +```javascript +const USER_STATUS_ENUM = ZCLDataTypes.enum8({ + available: 0, + occupied: 1, +}); + +// Then use in commands: +args: { userStatus: USER_STATUS_ENUM } +``` + +#### Reusable Bitmaps +Same pattern for bitmaps used multiple times: +```javascript +const ALARM_MASK = ZCLDataTypes.map8( + 'generalHardwareFault', + 'generalSoftwareFault', + 'reserved2', + 'reserved3', +); + +// Then use in attributes/commands: +alarmMask: { id: 0x0010, type: ALARM_MASK }, +``` + +--- + +### Workflow: Adding/Updating a Cluster + +#### 1. Extract Spec Section +```bash +pdftotext docs/zigbee-cluster-specification-r8.pdf - | grep -A 500 "X.Y.Z Cluster Name" +``` + +#### 2. Identify Elements +From spec tables, extract: +- Cluster ID and Name +- All attributes (ID, name, type, M/O) +- All commands (ID, name, args, direction, M/O) +- Descriptions (≤5 sentences) + +#### 3. Create/Update File +- Use template above +- Follow naming: `lib/clusters/clusterName.js` +- Export in `lib/clusters/index.js` + +#### 4. Validate +```bash +npm run lint +npm test +npm run build +``` + +--- + +### Checklist for Each Cluster + +- [ ] Cluster ID correct (hex in class, matches spec) +- [ ] Cluster NAME matches spec (camelCase) +- [ ] All mandatory attrs present with `// Mandatory` +- [ ] All mandatory cmds present with `// Mandatory` +- [ ] Conditional attrs/cmds marked `// Conditional` (M* in spec) +- [ ] Optional attrs/cmds marked `// Optional` +- [ ] Descriptions copied from spec (≤5 sentences) +- [ ] Section comments group related attrs +- [ ] Client/server cmd sections separated +- [ ] Server→client cmds have `direction` field +- [ ] Responses defined where applicable +- [ ] Reusable enums/bitmaps extracted if used 2+ times +- [ ] Hex IDs used consistently (with decimal comments if > 9) +- [ ] Lint passes +- [ ] Tests pass + +--- + +### Reference Examples + +- **Best documented**: `lib/clusters/doorLock.js` +- **Simple attrs only**: `lib/clusters/metering.js` +- **Color/enum heavy**: `lib/clusters/colorControl.js` 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: diff --git a/docs/zigbee-cluster-specification-r8.pdf b/docs/zigbee-cluster-specification-r8.pdf new file mode 100644 index 0000000..e2d869d Binary files /dev/null and b/docs/zigbee-cluster-specification-r8.pdf differ diff --git a/index.d.ts b/index.d.ts index 90d3e58..3eb6ec8 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,1016 @@ type ConstructorOptions = { endpointDescriptors: EndpointDescriptor[]; sendFrame: (endpointId: number, clusterId: number, frame: Buffer) => Promise; }; -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} - */ - discoverCommandsGenerated({ - startValue, - maxResults, - }?: { + +export interface ZCLNodeCluster extends EventEmitter { + 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} - */ - 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; - }>; - /** - * 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; - } - >; - /** - * 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} - */ + opts?: { timeout?: number } + ): Promise<{ [x: string]: unknown }>; + + writeAttributes(attributes?: object): Promise; + 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. - */ - readReportingConfiguration(attributes?: any[]): Promise< - { - status: any; - 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; - /** - * 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. - */ - discoverAttributesExtended(): Promise; -} - -interface BasicCluster extends ZCLNodeCluster { + + readReportingConfiguration(attributes?: (string | number)[]): Promise<{ + status: string; + direction: 'reported' | 'received'; + attributeId: number; + attributeDataType?: number; + minInterval?: number; + maxInterval?: number; + minChange?: number; + timeoutPeriod?: number; + }[]>; + + discoverAttributes(): Promise<(string | number)[]>; + + 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?: number; + minPresentValue?: number; + outOfService?: boolean; + presentValue?: number; + reliability?: 'noFaultDetected' | 'noSensor' | 'overRange' | 'underRange' | 'openLoop' | 'shortedLoop' | 'noOutput' | 'unreliableOther' | 'processError' | 'configurationError'; + resolution?: number; + 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?: number; + minPresentValue?: number; + outOfService?: boolean; + presentValue?: number; + reliability?: 'noFaultDetected' | 'overRange' | 'underRange' | 'openLoop' | 'shortedLoop' | 'unreliableOther' | 'processError' | 'configurationError'; + relinquishDefault?: number; + resolution?: number; + 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; +} + +export interface AnalogValueClusterAttributes { + description?: string; + outOfService?: boolean; + presentValue?: number; + reliability?: 'noFaultDetected' | 'overRange' | 'underRange' | 'openLoop' | 'shortedLoop' | 'unreliableOther' | 'processError' | 'configurationError'; + relinquishDefault?: number; + 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' | 'mains' | 'mains3phase' | 'battery' | 'dc' | 'emergencyMains' | 'emergencyTransfer'; + appProfileVersion?: number; + locationDesc?: string; + 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 }>; + 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' | '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 }>; + 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<{ 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; +} + +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<{ 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; +} + +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?: 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; +} + +export interface IasZoneCluster extends ZCLNodeCluster { + readAttributes(attributeNames: K[], opts?: { timeout?: number }): Promise>; + writeAttributes(attributes: Partial): 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; +} + +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(): Promise<{ timeout: number }>; + 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; + 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>; + 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 { + 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<{ total: number; startIndex: number; groups: unknown[] }>; +} + +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 +1033,57 @@ 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; +} \ No newline at end of file diff --git a/lib/clusters/doorLock.js b/lib/clusters/doorLock.js index 8d100b4..831fa88 100644 --- a/lib/clusters/doorLock.js +++ b/lib/clusters/doorLock.js @@ -1,16 +1,628 @@ 'use strict'; const Cluster = require('../Cluster'); +const { ZCLDataTypes } = require('../zclTypes'); const ATTRIBUTES = { + // Lock Information (0x0000 - 0x000F) + lockState: { // Mandatory + id: 0x0000, + type: ZCLDataTypes.enum8({ + notFullyLocked: 0, + locked: 1, + unlocked: 2, + undefined: 255, + }), + }, + lockType: { // Mandatory + id: 0x0001, + type: ZCLDataTypes.enum8({ + deadBolt: 0, + magnetic: 1, + other: 2, + mortise: 3, + rim: 4, + latchBolt: 5, + cylindricalLock: 6, + tubularLock: 7, + interconnectedLock: 8, + deadLatch: 9, + doorFurniture: 10, + }), + }, + actuatorEnabled: { id: 0x0002, type: ZCLDataTypes.bool }, // Mandatory + doorState: { // Optional + id: 0x0003, + type: ZCLDataTypes.enum8({ + open: 0, + closed: 1, + errorJammed: 2, + errorForcedOpen: 3, + errorUnspecified: 4, + undefined: 255, + }), + }, + doorOpenEvents: { id: 0x0004, type: ZCLDataTypes.uint32 }, // Optional + doorClosedEvents: { id: 0x0005, type: ZCLDataTypes.uint32 }, // Optional + openPeriod: { id: 0x0006, type: ZCLDataTypes.uint16 }, // Optional + + // User/PIN/RFID Configuration (0x0010 - 0x001F) + numberOfLogRecordsSupported: { id: 0x0010, type: ZCLDataTypes.uint16 }, // 16, Optional + numberOfTotalUsersSupported: { id: 0x0011, type: ZCLDataTypes.uint16 }, // 17, Optional + numberOfPINUsersSupported: { id: 0x0012, type: ZCLDataTypes.uint16 }, // 18, Conditional¹ + numberOfRFIDUsersSupported: { id: 0x0013, type: ZCLDataTypes.uint16 }, // 19, Conditional² + numberOfWeekDaySchedulesSupportedPerUser: { // Conditional³ + id: 0x0014, // 20 + type: ZCLDataTypes.uint8, + }, + numberOfYearDaySchedulesSupportedPerUser: { // Conditional⁴ + id: 0x0015, // 21 + type: ZCLDataTypes.uint8, + }, + numberOfHolidaySchedulesSupported: { id: 0x0016, type: ZCLDataTypes.uint8 }, // 22, Conditional⁵ + maxPINCodeLength: { id: 0x0017, type: ZCLDataTypes.uint8 }, // 23, Conditional¹ + minPINCodeLength: { id: 0x0018, type: ZCLDataTypes.uint8 }, // 24, Conditional¹ + maxRFIDCodeLength: { id: 0x0019, type: ZCLDataTypes.uint8 }, // 25, Conditional² + minRFIDCodeLength: { id: 0x001A, type: ZCLDataTypes.uint8 }, // 26, Conditional² + // ¹ PIN credential support + // ² RFID credential support + // ³ Week Day Schedule support + // ⁴ Year Day Schedule support + // ⁵ Holiday Schedule support + + // Operational Settings (0x0020 - 0x002F) + enableLogging: { id: 0x0020, type: ZCLDataTypes.bool }, // 32, Optional + language: { id: 0x0021, type: ZCLDataTypes.string }, // 33, Optional + ledSettings: { id: 0x0022, type: ZCLDataTypes.uint8 }, // 34, Optional + autoRelockTime: { id: 0x0023, type: ZCLDataTypes.uint32 }, // 35, Optional + soundVolume: { id: 0x0024, type: ZCLDataTypes.uint8 }, // 36, Optional + operatingMode: { // Optional + id: 0x0025, // 37 + type: ZCLDataTypes.enum8({ + normal: 0, + vacation: 1, + privacy: 2, + noRFLockOrUnlock: 3, + passage: 4, + }), + }, + supportedOperatingModes: { // Mandatory + id: 0x0026, // 38 + type: ZCLDataTypes.map16( + 'normal', + 'vacation', + 'privacy', + 'noRFLockOrUnlock', + 'passage', + ), + }, + defaultConfigurationRegister: { // Optional + id: 0x0027, // 39 + type: ZCLDataTypes.map16( + 'enableLocalProgramming', + 'keypadInterfaceDefaultAccess', + 'rfInterfaceDefaultAccess', + 'reserved3', + 'reserved4', + 'soundEnabled', + 'autoRelockTimeSet', + 'ledSettingsSet', + ), + }, + enableLocalProgramming: { id: 0x0028, type: ZCLDataTypes.bool }, // 40, Optional + enableOneTouchLocking: { id: 0x0029, type: ZCLDataTypes.bool }, // 41, Optional + enableInsideStatusLED: { id: 0x002A, type: ZCLDataTypes.bool }, // 42, Optional + enablePrivacyModeButton: { id: 0x002B, type: ZCLDataTypes.bool }, // 43, Optional + + // Security Settings (0x0030 - 0x003F) + wrongCodeEntryLimit: { id: 0x0030, type: ZCLDataTypes.uint8 }, // 48, Optional + userCodeTemporaryDisableTime: { id: 0x0031, type: ZCLDataTypes.uint8 }, // 49, Optional + sendPINOverTheAir: { id: 0x0032, type: ZCLDataTypes.bool }, // 50, Optional + requirePINforRFOperation: { id: 0x0033, type: ZCLDataTypes.bool }, // 51, Optional + securityLevel: { // Optional + id: 0x0034, // 52 + type: ZCLDataTypes.enum8({ + network: 0, + apsSecurity: 1, + }), + }, + + // Alarm and Event Masks (0x0040 - 0x004F) + alarmMask: { // Optional + id: 0x0040, // 64 + type: ZCLDataTypes.map16( + 'deadboltJammed', + 'lockResetToFactoryDefaults', + 'reserved2', + 'rfModulePowerCycled', + 'tamperAlarmWrongCodeEntryLimit', + 'tamperAlarmFrontEscutcheonRemoved', + 'forcedDoorOpenUnderDoorLockedCondition', + ), + }, + keypadOperationEventMask: { // Optional + id: 0x0041, // 65 + type: ZCLDataTypes.map16( + 'unknownOrManufacturerSpecificKeypadOperationEvent', + 'lockSourceKeypad', + 'unlockSourceKeypad', + 'lockSourceKeypadErrorInvalidPIN', + 'lockSourceKeypadErrorInvalidSchedule', + 'unlockSourceKeypadErrorInvalidCode', + 'unlockSourceKeypadErrorInvalidSchedule', + 'nonAccessUserOperationEventSourceKeypad', + ), + }, + rfOperationEventMask: { // Optional + id: 0x0042, // 66 + type: ZCLDataTypes.map16( + 'unknownOrManufacturerSpecificKeypadOperationEvent', + 'lockSourceRF', + 'unlockSourceRF', + 'lockSourceRFErrorInvalidCode', + 'lockSourceRFErrorInvalidSchedule', + 'unlockSourceRFErrorInvalidCode', + 'unlockSourceRFErrorInvalidSchedule', + ), + }, + manualOperationEventMask: { // Optional + id: 0x0043, // 67 + type: ZCLDataTypes.map16( + 'unknownOrManufacturerSpecificManualOperationEvent', + 'thumbturnLock', + 'thumbturnUnlock', + 'oneTouchLock', + 'keyLock', + 'keyUnlock', + 'autoLock', + 'scheduleLock', + 'scheduleUnlock', + 'manualLock', + 'manualUnlock', + ), + }, + rfidOperationEventMask: { // Optional + id: 0x0044, // 68 + type: ZCLDataTypes.map16( + 'unknownOrManufacturerSpecificKeypadOperationEvent', + 'lockSourceRFID', + 'unlockSourceRFID', + 'lockSourceRFIDErrorInvalidRFIDID', + 'lockSourceRFIDErrorInvalidSchedule', + 'unlockSourceRFIDErrorInvalidRFIDID', + 'unlockSourceRFIDErrorInvalidSchedule', + ), + }, + keypadProgrammingEventMask: { // Optional + id: 0x0045, // 69 + type: ZCLDataTypes.map16( + 'unknownOrManufacturerSpecificKeypadProgrammingEvent', + 'masterCodeChanged', + 'pinCodeAdded', + 'pinCodeDeleted', + 'pinCodeChanged', + ), + }, + rfProgrammingEventMask: { // Optional + id: 0x0046, // 70 + type: ZCLDataTypes.map16( + 'unknownOrManufacturerSpecificRFProgrammingEvent', + 'reserved1', + 'pinCodeAdded', + 'pinCodeDeleted', + 'pinCodeChanged', + 'rfidCodeAdded', + 'rfidCodeDeleted', + ), + }, + rfidProgrammingEventMask: { // Optional + id: 0x0047, // 71 + type: ZCLDataTypes.map16( + 'unknownOrManufacturerSpecificRFIDProgrammingEvent', + 'rfidCodeAdded', + 'rfidCodeDeleted', + ), + }, }; -const COMMANDS = {}; +// Reusable enum definitions +const USER_STATUS_ENUM = ZCLDataTypes.enum8({ + available: 0, + occupiedEnabled: 1, + occupiedDisabled: 3, + notSupported: 255, +}); + +const USER_TYPE_ENUM = ZCLDataTypes.enum8({ + unrestricted: 0, + yearDayScheduleUser: 1, + weekDayScheduleUser: 2, + masterUser: 3, + nonAccessUser: 4, + notSupported: 255, +}); + +const OPERATING_MODE_ENUM = ZCLDataTypes.enum8({ + normal: 0, + vacation: 1, + privacy: 2, + noRFLockOrUnlock: 3, + passage: 4, +}); + +const COMMANDS = { + // --- Client to Server Commands --- + + // Lock/Unlock Commands + lockDoor: { // Mandatory + id: 0x0000, + args: { + pinCode: ZCLDataTypes.octstr, + }, + response: { + id: 0x0000, + args: { status: ZCLDataTypes.uint8 }, + }, + }, + unlockDoor: { // Mandatory + id: 0x0001, + args: { + pinCode: ZCLDataTypes.octstr, + }, + response: { + id: 0x0001, + args: { status: ZCLDataTypes.uint8 }, + }, + }, + toggle: { // Optional + id: 0x0002, + args: { + pinCode: ZCLDataTypes.octstr, + }, + response: { + id: 0x0002, + args: { status: ZCLDataTypes.uint8 }, + }, + }, + unlockWithTimeout: { // Optional + id: 0x0003, + args: { + timeout: ZCLDataTypes.uint16, + pinCode: ZCLDataTypes.octstr, + }, + response: { + id: 0x0003, + args: { status: ZCLDataTypes.uint8 }, + }, + }, + + // Logging Commands + getLogRecord: { // Optional + id: 0x0004, + args: { + logIndex: ZCLDataTypes.uint16, + }, + response: { + id: 0x0004, + args: { + logEntryId: ZCLDataTypes.uint16, + timestamp: ZCLDataTypes.uint32, + eventType: ZCLDataTypes.uint8, + source: ZCLDataTypes.uint8, + eventIdOrAlarmCode: ZCLDataTypes.uint8, + userId: ZCLDataTypes.uint16, + pin: ZCLDataTypes.octstr, + }, + }, + }, + + // PIN Code Commands + setPINCode: { // Optional + id: 0x0005, + args: { + userId: ZCLDataTypes.uint16, + userStatus: USER_STATUS_ENUM, + userType: USER_TYPE_ENUM, + pinCode: ZCLDataTypes.octstr, + }, + response: { + id: 0x0005, + args: { status: ZCLDataTypes.uint8 }, + }, + }, + getPINCode: { // Optional + id: 0x0006, + args: { + userId: ZCLDataTypes.uint16, + }, + response: { + id: 0x0006, + args: { + userId: ZCLDataTypes.uint16, + userStatus: USER_STATUS_ENUM, + userType: USER_TYPE_ENUM, + pinCode: ZCLDataTypes.octstr, + }, + }, + }, + clearPINCode: { // Optional + id: 0x0007, + args: { + userId: ZCLDataTypes.uint16, + }, + response: { + id: 0x0007, + args: { status: ZCLDataTypes.uint8 }, + }, + }, + clearAllPINCodes: { // Optional + id: 0x0008, + response: { + id: 0x0008, + args: { status: ZCLDataTypes.uint8 }, + }, + }, + setUserStatus: { // Optional + id: 0x0009, + args: { + userId: ZCLDataTypes.uint16, + userStatus: USER_STATUS_ENUM, + }, + response: { + id: 0x0009, + args: { status: ZCLDataTypes.uint8 }, + }, + }, + getUserStatus: { // Optional + id: 0x000A, // 10 + args: { + userId: ZCLDataTypes.uint16, + }, + response: { + id: 0x000A, // 10 + args: { + userId: ZCLDataTypes.uint16, + userStatus: USER_STATUS_ENUM, + }, + }, + }, + + // Schedule Commands + setWeekDaySchedule: { // Optional + id: 0x000B, // 11 + args: { + scheduleId: ZCLDataTypes.uint8, + userId: ZCLDataTypes.uint16, + daysMask: ZCLDataTypes.map8('sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'), + startHour: ZCLDataTypes.uint8, + startMinute: ZCLDataTypes.uint8, + endHour: ZCLDataTypes.uint8, + endMinute: ZCLDataTypes.uint8, + }, + response: { + id: 0x000B, // 11 + args: { status: ZCLDataTypes.uint8 }, + }, + }, + getWeekDaySchedule: { // Optional + id: 0x000C, // 12 + args: { + scheduleId: ZCLDataTypes.uint8, + userId: ZCLDataTypes.uint16, + }, + response: { + id: 0x000C, // 12 + args: { + scheduleId: ZCLDataTypes.uint8, + userId: ZCLDataTypes.uint16, + status: ZCLDataTypes.uint8, + daysMask: ZCLDataTypes.map8('sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'), + startHour: ZCLDataTypes.uint8, + startMinute: ZCLDataTypes.uint8, + endHour: ZCLDataTypes.uint8, + endMinute: ZCLDataTypes.uint8, + }, + }, + }, + clearWeekDaySchedule: { // Optional + id: 0x000D, // 13 + args: { + scheduleId: ZCLDataTypes.uint8, + userId: ZCLDataTypes.uint16, + }, + response: { + id: 0x000D, // 13 + args: { status: ZCLDataTypes.uint8 }, + }, + }, + setYearDaySchedule: { // Optional + id: 0x000E, // 14 + args: { + scheduleId: ZCLDataTypes.uint8, + userId: ZCLDataTypes.uint16, + localStartTime: ZCLDataTypes.uint32, + localEndTime: ZCLDataTypes.uint32, + }, + response: { + id: 0x000E, // 14 + args: { status: ZCLDataTypes.uint8 }, + }, + }, + getYearDaySchedule: { // Optional + id: 0x000F, // 15 + args: { + scheduleId: ZCLDataTypes.uint8, + userId: ZCLDataTypes.uint16, + }, + response: { + id: 0x000F, // 15 + args: { + scheduleId: ZCLDataTypes.uint8, + userId: ZCLDataTypes.uint16, + status: ZCLDataTypes.uint8, + localStartTime: ZCLDataTypes.uint32, + localEndTime: ZCLDataTypes.uint32, + }, + }, + }, + clearYearDaySchedule: { // Optional + id: 0x0010, // 16 + args: { + scheduleId: ZCLDataTypes.uint8, + userId: ZCLDataTypes.uint16, + }, + response: { + id: 0x0010, // 16 + args: { status: ZCLDataTypes.uint8 }, + }, + }, + setHolidaySchedule: { // Optional + id: 0x0011, // 17 + args: { + holidayScheduleId: ZCLDataTypes.uint8, + localStartTime: ZCLDataTypes.uint32, + localEndTime: ZCLDataTypes.uint32, + operatingModeDuringHoliday: OPERATING_MODE_ENUM, + }, + response: { + id: 0x0011, // 17 + args: { status: ZCLDataTypes.uint8 }, + }, + }, + getHolidaySchedule: { // Optional + id: 0x0012, // 18 + args: { + holidayScheduleId: ZCLDataTypes.uint8, + }, + response: { + id: 0x0012, // 18 + args: { + holidayScheduleId: ZCLDataTypes.uint8, + status: ZCLDataTypes.uint8, + localStartTime: ZCLDataTypes.uint32, + localEndTime: ZCLDataTypes.uint32, + operatingMode: OPERATING_MODE_ENUM, + }, + }, + }, + clearHolidaySchedule: { // Optional + id: 0x0013, // 19 + args: { + holidayScheduleId: ZCLDataTypes.uint8, + }, + response: { + id: 0x0013, // 19 + args: { status: ZCLDataTypes.uint8 }, + }, + }, + + // User Type Commands + setUserType: { // Optional + id: 0x0014, // 20 + args: { + userId: ZCLDataTypes.uint16, + userType: USER_TYPE_ENUM, + }, + response: { + id: 0x0014, // 20 + args: { status: ZCLDataTypes.uint8 }, + }, + }, + getUserType: { // Optional + id: 0x0015, // 21 + args: { + userId: ZCLDataTypes.uint16, + }, + response: { + id: 0x0015, // 21 + args: { + userId: ZCLDataTypes.uint16, + userType: USER_TYPE_ENUM, + }, + }, + }, + + // RFID Code Commands + setRFIDCode: { // Optional + id: 0x0016, // 22 + args: { + userId: ZCLDataTypes.uint16, + userStatus: USER_STATUS_ENUM, + userType: USER_TYPE_ENUM, + rfidCode: ZCLDataTypes.octstr, + }, + response: { + id: 0x0016, // 22 + args: { status: ZCLDataTypes.uint8 }, + }, + }, + getRFIDCode: { // Optional + id: 0x0017, // 23 + args: { + userId: ZCLDataTypes.uint16, + }, + response: { + id: 0x0017, // 23 + args: { + userId: ZCLDataTypes.uint16, + userStatus: USER_STATUS_ENUM, + userType: USER_TYPE_ENUM, + rfidCode: ZCLDataTypes.octstr, + }, + }, + }, + clearRFIDCode: { // Optional + id: 0x0018, // 24 + args: { + userId: ZCLDataTypes.uint16, + }, + response: { + id: 0x0018, // 24 + args: { status: ZCLDataTypes.uint8 }, + }, + }, + clearAllRFIDCodes: { // Optional + id: 0x0019, // 25 + response: { + id: 0x0019, // 25 + args: { status: ZCLDataTypes.uint8 }, + }, + }, + + // --- Server to Client Commands --- + + operationEventNotification: { // Optional + id: 0x0020, // 32 + direction: Cluster.DIRECTION_SERVER_TO_CLIENT, + args: { + operationEventSource: ZCLDataTypes.uint8, + operationEventCode: ZCLDataTypes.uint8, + userId: ZCLDataTypes.uint16, + pin: ZCLDataTypes.octstr, + zigBeeLocalTime: ZCLDataTypes.uint32, + data: ZCLDataTypes.octstr, + }, + }, + programmingEventNotification: { // Optional + id: 0x0021, // 33 + direction: Cluster.DIRECTION_SERVER_TO_CLIENT, + args: { + programEventSource: ZCLDataTypes.uint8, + programEventCode: ZCLDataTypes.uint8, + userId: ZCLDataTypes.uint16, + pin: ZCLDataTypes.octstr, + userType: USER_TYPE_ENUM, + userStatus: USER_STATUS_ENUM, + zigBeeLocalTime: ZCLDataTypes.uint32, + data: ZCLDataTypes.octstr, + }, + }, +}; class DoorLockCluster extends Cluster { static get ID() { - return 257; + return 0x0101; // 257 } static get NAME() { diff --git a/lib/clusters/metering.js b/lib/clusters/metering.js index 6ae4d58..f788f15 100644 --- a/lib/clusters/metering.js +++ b/lib/clusters/metering.js @@ -4,14 +4,250 @@ const Cluster = require('../Cluster'); const { ZCLDataTypes } = require('../zclTypes'); const ATTRIBUTES = { - currentSummationDelivered: { id: 0, type: ZCLDataTypes.uint48 }, - currentSummationReceived: { id: 1, type: ZCLDataTypes.uint48 }, - currentMaxDemandDelivered: { id: 2, type: ZCLDataTypes.uint48 }, - currentMaxDemandReceived: { id: 3, type: ZCLDataTypes.uint48 }, - powerFactor: { id: 6, type: ZCLDataTypes.int8 }, - multiplier: { id: 769, type: ZCLDataTypes.uint24 }, - divisor: { id: 770, type: ZCLDataTypes.uint24 }, - instantaneousDemand: { id: 1024, type: ZCLDataTypes.int24 }, + // Reading Information Set (0x0000 - 0x000F) + currentSummationDelivered: { id: 0x0000, type: ZCLDataTypes.uint48 }, // Mandatory + currentSummationReceived: { id: 0x0001, type: ZCLDataTypes.uint48 }, // Optional + currentMaxDemandDelivered: { id: 0x0002, type: ZCLDataTypes.uint48 }, // Optional + currentMaxDemandReceived: { id: 0x0003, type: ZCLDataTypes.uint48 }, // Optional + dftSummation: { id: 0x0004, type: ZCLDataTypes.uint48 }, // Optional + dailyFreezeTime: { id: 0x0005, type: ZCLDataTypes.uint16 }, // Optional + powerFactor: { id: 0x0006, type: ZCLDataTypes.int8 }, // Optional + readingSnapShotTime: { id: 0x0007, type: ZCLDataTypes.uint32 }, // Optional + currentMaxDemandDeliveredTime: { id: 0x0008, type: ZCLDataTypes.uint32 }, // Optional + currentMaxDemandReceivedTime: { id: 0x0009, type: ZCLDataTypes.uint32 }, // Optional + defaultUpdatePeriod: { id: 0x000A, type: ZCLDataTypes.uint8 }, // 10, Optional + fastPollUpdatePeriod: { id: 0x000B, type: ZCLDataTypes.uint8 }, // 11, Optional + currentBlockPeriodConsumptionDelivered: { id: 0x000C, type: ZCLDataTypes.uint48 }, // 12, Optional + dailyConsumptionTarget: { id: 0x000D, type: ZCLDataTypes.uint24 }, // 13, Optional + currentBlock: { id: 0x000E, type: ZCLDataTypes.enum8 }, // 14, Optional + profileIntervalPeriod: { id: 0x000F, type: ZCLDataTypes.enum8 }, // 15, Optional + + // Summation TOU Information Set (0x0100 - 0x01FF) + currentTier1SummationDelivered: { id: 0x0100, type: ZCLDataTypes.uint48 }, // 256, Optional + currentTier1SummationReceived: { id: 0x0101, type: ZCLDataTypes.uint48 }, // 257, Optional + currentTier2SummationDelivered: { id: 0x0102, type: ZCLDataTypes.uint48 }, // 258, Optional + currentTier2SummationReceived: { id: 0x0103, type: ZCLDataTypes.uint48 }, // 259, Optional + currentTier3SummationDelivered: { id: 0x0104, type: ZCLDataTypes.uint48 }, // 260, Optional + currentTier3SummationReceived: { id: 0x0105, type: ZCLDataTypes.uint48 }, // 261, Optional + currentTier4SummationDelivered: { id: 0x0106, type: ZCLDataTypes.uint48 }, // 262, Optional + currentTier4SummationReceived: { id: 0x0107, type: ZCLDataTypes.uint48 }, // 263, Optional + + // Meter Status (0x0200 - 0x02FF) + status: { id: 0x0200, type: ZCLDataTypes.map8 }, // 512, Optional + remainingBatteryLife: { id: 0x0201, type: ZCLDataTypes.uint8 }, // 513, Optional + hoursInOperation: { id: 0x0202, type: ZCLDataTypes.uint24 }, // 514, Optional + hoursInFault: { id: 0x0203, type: ZCLDataTypes.uint24 }, // 515, Optional + extendedStatus: { id: 0x0204, type: ZCLDataTypes.map64 }, // 516, Optional + + // Formatting Set (0x0300 - 0x03FF) + unitOfMeasure: { id: 0x0300, type: ZCLDataTypes.enum8 }, // 768, Mandatory + multiplier: { id: 0x0301, type: ZCLDataTypes.uint24 }, // 769, Optional + divisor: { id: 0x0302, type: ZCLDataTypes.uint24 }, // 770, Optional + summationFormatting: { id: 0x0303, type: ZCLDataTypes.map8 }, // 771, Mandatory + demandFormatting: { id: 0x0304, type: ZCLDataTypes.map8 }, // 772, Optional + historicalConsumptionFormatting: { id: 0x0305, type: ZCLDataTypes.map8 }, // 773, Optional + meteringDeviceType: { id: 0x0306, type: ZCLDataTypes.map8 }, // 774, Mandatory + siteId: { id: 0x0307, type: ZCLDataTypes.octstr }, // 775, Optional + meterSerialNumber: { id: 0x0308, type: ZCLDataTypes.octstr }, // 776, Optional + energyCarrierUnitOfMeasure: { id: 0x0309, type: ZCLDataTypes.enum8 }, // 777, Optional + energyCarrierSummationFormatting: { id: 0x030A, type: ZCLDataTypes.map8 }, // 778, Optional + energyCarrierDemandFormatting: { id: 0x030B, type: ZCLDataTypes.map8 }, // 779, Optional + temperatureUnitOfMeasure: { id: 0x030C, type: ZCLDataTypes.enum8 }, // 780, Optional + temperatureFormatting: { id: 0x030D, type: ZCLDataTypes.map8 }, // 781, Optional + moduleSerialNumber: { id: 0x030E, type: ZCLDataTypes.octstr }, // 782, Optional + operatingTariffLabelDelivered: { id: 0x030F, type: ZCLDataTypes.octstr }, // 783, Optional + operatingTariffLabelReceived: { id: 0x0310, type: ZCLDataTypes.octstr }, // 784, Optional + customerIdNumber: { id: 0x0311, type: ZCLDataTypes.octstr }, // 785, Optional + alternativeUnitOfMeasure: { id: 0x0312, type: ZCLDataTypes.enum8 }, // 786, Optional + alternativeDemandFormatting: { id: 0x0313, type: ZCLDataTypes.map8 }, // 787, Optional + alternativeConsumptionFormatting: { id: 0x0314, type: ZCLDataTypes.map8 }, // 788, Optional + + // Historical Consumption (0x0400 - 0x04FF) + instantaneousDemand: { id: 0x0400, type: ZCLDataTypes.int24 }, // 1024, Optional + currentDayConsumptionDelivered: { id: 0x0401, type: ZCLDataTypes.uint24 }, // 1025, Optional + currentDayConsumptionReceived: { id: 0x0402, type: ZCLDataTypes.uint24 }, // 1026, Optional + previousDayConsumptionDelivered: { id: 0x0403, type: ZCLDataTypes.uint24 }, // 1027, Optional + previousDayConsumptionReceived: { id: 0x0404, type: ZCLDataTypes.uint24 }, // 1028, Optional + currentPartialProfileIntervalStartTimeDelivered: { // Optional + id: 0x0405, // 1029 + type: ZCLDataTypes.uint32, + }, + currentPartialProfileIntervalStartTimeReceived: { // Optional + id: 0x0406, // 1030 + type: ZCLDataTypes.uint32, + }, + currentPartialProfileIntervalValueDelivered: { // Optional + id: 0x0407, // 1031 + type: ZCLDataTypes.uint24, + }, + currentPartialProfileIntervalValueReceived: { // Optional + id: 0x0408, // 1032 + type: ZCLDataTypes.uint24, + }, + currentDayMaxPressure: { id: 0x0409, type: ZCLDataTypes.uint48 }, // 1033, Optional + currentDayMinPressure: { id: 0x040A, type: ZCLDataTypes.uint48 }, // 1034, Optional + previousDayMaxPressure: { id: 0x040B, type: ZCLDataTypes.uint48 }, // 1035, Optional + previousDayMinPressure: { id: 0x040C, type: ZCLDataTypes.uint48 }, // 1036, Optional + currentDayMaxDemand: { id: 0x040D, type: ZCLDataTypes.int24 }, // 1037, Optional + previousDayMaxDemand: { id: 0x040E, type: ZCLDataTypes.int24 }, // 1038, Optional + currentMonthMaxDemand: { id: 0x040F, type: ZCLDataTypes.int24 }, // 1039, Optional + currentYearMaxDemand: { id: 0x0410, type: ZCLDataTypes.int24 }, // 1040, Optional + currentDayMaxEnergyCarrierDemand: { id: 0x0411, type: ZCLDataTypes.int24 }, // 1041, Optional + previousDayMaxEnergyCarrierDemand: { id: 0x0412, type: ZCLDataTypes.int24 }, // 1042, Optional + currentMonthMaxEnergyCarrierDemand: { id: 0x0413, type: ZCLDataTypes.int24 }, // 1043, Optional + currentMonthMinEnergyCarrierDemand: { id: 0x0414, type: ZCLDataTypes.int24 }, // 1044, Optional + currentYearMaxEnergyCarrierDemand: { id: 0x0415, type: ZCLDataTypes.int24 }, // 1045, Optional + currentYearMinEnergyCarrierDemand: { id: 0x0416, type: ZCLDataTypes.int24 }, // 1046, Optional + + // Load Profile Configuration (0x0500 - 0x05FF) + maxNumberOfPeriodsDelivered: { id: 0x0500, type: ZCLDataTypes.uint8 }, // 1280, Optional + + // Supply Limit (0x0600 - 0x06FF) + currentDemandDelivered: { id: 0x0600, type: ZCLDataTypes.uint24 }, // 1536, Optional + demandLimit: { id: 0x0601, type: ZCLDataTypes.uint24 }, // 1537, Optional + demandIntegrationPeriod: { id: 0x0602, type: ZCLDataTypes.uint8 }, // 1538, Optional + numberOfDemandSubintervals: { id: 0x0603, type: ZCLDataTypes.uint8 }, // 1539, Optional + demandLimitArmDuration: { id: 0x0604, type: ZCLDataTypes.uint16 }, // 1540, Optional + + // Block Information Delivered (0x0700 - 0x07FF) + // All attributes in this section are Optional + currentNoTierBlock1SummationDelivered: { id: 0x0700, type: ZCLDataTypes.uint48 }, + currentNoTierBlock2SummationDelivered: { id: 0x0701, type: ZCLDataTypes.uint48 }, + currentNoTierBlock3SummationDelivered: { id: 0x0702, type: ZCLDataTypes.uint48 }, + currentNoTierBlock4SummationDelivered: { id: 0x0703, type: ZCLDataTypes.uint48 }, + currentNoTierBlock5SummationDelivered: { id: 0x0704, type: ZCLDataTypes.uint48 }, + currentNoTierBlock6SummationDelivered: { id: 0x0705, type: ZCLDataTypes.uint48 }, + currentNoTierBlock7SummationDelivered: { id: 0x0706, type: ZCLDataTypes.uint48 }, + currentNoTierBlock8SummationDelivered: { id: 0x0707, type: ZCLDataTypes.uint48 }, + currentNoTierBlock9SummationDelivered: { id: 0x0708, type: ZCLDataTypes.uint48 }, + currentNoTierBlock10SummationDelivered: { id: 0x0709, type: ZCLDataTypes.uint48 }, + currentNoTierBlock11SummationDelivered: { id: 0x070A, type: ZCLDataTypes.uint48 }, + currentNoTierBlock12SummationDelivered: { id: 0x070B, type: ZCLDataTypes.uint48 }, + currentNoTierBlock13SummationDelivered: { id: 0x070C, type: ZCLDataTypes.uint48 }, + currentNoTierBlock14SummationDelivered: { id: 0x070D, type: ZCLDataTypes.uint48 }, + currentNoTierBlock15SummationDelivered: { id: 0x070E, type: ZCLDataTypes.uint48 }, + currentNoTierBlock16SummationDelivered: { id: 0x070F, type: ZCLDataTypes.uint48 }, + currentTier1Block1SummationDelivered: { id: 0x0710, type: ZCLDataTypes.uint48 }, + currentTier1Block2SummationDelivered: { id: 0x0711, type: ZCLDataTypes.uint48 }, + currentTier1Block3SummationDelivered: { id: 0x0712, type: ZCLDataTypes.uint48 }, + currentTier1Block4SummationDelivered: { id: 0x0713, type: ZCLDataTypes.uint48 }, + currentTier1Block5SummationDelivered: { id: 0x0714, type: ZCLDataTypes.uint48 }, + currentTier1Block6SummationDelivered: { id: 0x0715, type: ZCLDataTypes.uint48 }, + currentTier1Block7SummationDelivered: { id: 0x0716, type: ZCLDataTypes.uint48 }, + currentTier1Block8SummationDelivered: { id: 0x0717, type: ZCLDataTypes.uint48 }, + currentTier1Block9SummationDelivered: { id: 0x0718, type: ZCLDataTypes.uint48 }, + currentTier1Block10SummationDelivered: { id: 0x0719, type: ZCLDataTypes.uint48 }, + currentTier1Block11SummationDelivered: { id: 0x071A, type: ZCLDataTypes.uint48 }, + currentTier1Block12SummationDelivered: { id: 0x071B, type: ZCLDataTypes.uint48 }, + currentTier1Block13SummationDelivered: { id: 0x071C, type: ZCLDataTypes.uint48 }, + currentTier1Block14SummationDelivered: { id: 0x071D, type: ZCLDataTypes.uint48 }, + currentTier1Block15SummationDelivered: { id: 0x071E, type: ZCLDataTypes.uint48 }, + currentTier1Block16SummationDelivered: { id: 0x071F, type: ZCLDataTypes.uint48 }, + currentTier2Block1SummationDelivered: { id: 0x0720, type: ZCLDataTypes.uint48 }, + currentTier2Block2SummationDelivered: { id: 0x0721, type: ZCLDataTypes.uint48 }, + currentTier2Block3SummationDelivered: { id: 0x0722, type: ZCLDataTypes.uint48 }, + currentTier2Block4SummationDelivered: { id: 0x0723, type: ZCLDataTypes.uint48 }, + currentTier2Block5SummationDelivered: { id: 0x0724, type: ZCLDataTypes.uint48 }, + currentTier2Block6SummationDelivered: { id: 0x0725, type: ZCLDataTypes.uint48 }, + currentTier2Block7SummationDelivered: { id: 0x0726, type: ZCLDataTypes.uint48 }, + currentTier2Block8SummationDelivered: { id: 0x0727, type: ZCLDataTypes.uint48 }, + currentTier2Block9SummationDelivered: { id: 0x0728, type: ZCLDataTypes.uint48 }, + currentTier2Block10SummationDelivered: { id: 0x0729, type: ZCLDataTypes.uint48 }, + currentTier2Block11SummationDelivered: { id: 0x072A, type: ZCLDataTypes.uint48 }, + currentTier2Block12SummationDelivered: { id: 0x072B, type: ZCLDataTypes.uint48 }, + currentTier2Block13SummationDelivered: { id: 0x072C, type: ZCLDataTypes.uint48 }, + currentTier2Block14SummationDelivered: { id: 0x072D, type: ZCLDataTypes.uint48 }, + currentTier2Block15SummationDelivered: { id: 0x072E, type: ZCLDataTypes.uint48 }, + currentTier2Block16SummationDelivered: { id: 0x072F, type: ZCLDataTypes.uint48 }, + currentTier3Block1SummationDelivered: { id: 0x0730, type: ZCLDataTypes.uint48 }, + currentTier3Block2SummationDelivered: { id: 0x0731, type: ZCLDataTypes.uint48 }, + currentTier3Block3SummationDelivered: { id: 0x0732, type: ZCLDataTypes.uint48 }, + currentTier3Block4SummationDelivered: { id: 0x0733, type: ZCLDataTypes.uint48 }, + currentTier3Block5SummationDelivered: { id: 0x0734, type: ZCLDataTypes.uint48 }, + currentTier3Block6SummationDelivered: { id: 0x0735, type: ZCLDataTypes.uint48 }, + currentTier3Block7SummationDelivered: { id: 0x0736, type: ZCLDataTypes.uint48 }, + currentTier3Block8SummationDelivered: { id: 0x0737, type: ZCLDataTypes.uint48 }, + currentTier3Block9SummationDelivered: { id: 0x0738, type: ZCLDataTypes.uint48 }, + currentTier3Block10SummationDelivered: { id: 0x0739, type: ZCLDataTypes.uint48 }, + currentTier3Block11SummationDelivered: { id: 0x073A, type: ZCLDataTypes.uint48 }, + currentTier3Block12SummationDelivered: { id: 0x073B, type: ZCLDataTypes.uint48 }, + currentTier3Block13SummationDelivered: { id: 0x073C, type: ZCLDataTypes.uint48 }, + currentTier3Block14SummationDelivered: { id: 0x073D, type: ZCLDataTypes.uint48 }, + currentTier3Block15SummationDelivered: { id: 0x073E, type: ZCLDataTypes.uint48 }, + currentTier3Block16SummationDelivered: { id: 0x073F, type: ZCLDataTypes.uint48 }, + currentTier4Block1SummationDelivered: { id: 0x0740, type: ZCLDataTypes.uint48 }, + currentTier4Block2SummationDelivered: { id: 0x0741, type: ZCLDataTypes.uint48 }, + currentTier4Block3SummationDelivered: { id: 0x0742, type: ZCLDataTypes.uint48 }, + currentTier4Block4SummationDelivered: { id: 0x0743, type: ZCLDataTypes.uint48 }, + currentTier4Block5SummationDelivered: { id: 0x0744, type: ZCLDataTypes.uint48 }, + currentTier4Block6SummationDelivered: { id: 0x0745, type: ZCLDataTypes.uint48 }, + currentTier4Block7SummationDelivered: { id: 0x0746, type: ZCLDataTypes.uint48 }, + currentTier4Block8SummationDelivered: { id: 0x0747, type: ZCLDataTypes.uint48 }, + currentTier4Block9SummationDelivered: { id: 0x0748, type: ZCLDataTypes.uint48 }, + currentTier4Block10SummationDelivered: { id: 0x0749, type: ZCLDataTypes.uint48 }, + currentTier4Block11SummationDelivered: { id: 0x074A, type: ZCLDataTypes.uint48 }, + currentTier4Block12SummationDelivered: { id: 0x074B, type: ZCLDataTypes.uint48 }, + currentTier4Block13SummationDelivered: { id: 0x074C, type: ZCLDataTypes.uint48 }, + currentTier4Block14SummationDelivered: { id: 0x074D, type: ZCLDataTypes.uint48 }, + currentTier4Block15SummationDelivered: { id: 0x074E, type: ZCLDataTypes.uint48 }, + currentTier4Block16SummationDelivered: { id: 0x074F, type: ZCLDataTypes.uint48 }, + + // Alarms (0x0800 - 0x08FF) + genericAlarmMask: { id: 0x0800, type: ZCLDataTypes.map16 }, // 2048, Optional + electricityAlarmMask: { id: 0x0801, type: ZCLDataTypes.map32 }, // 2049, Optional + genericFlowPressureAlarmMask: { id: 0x0802, type: ZCLDataTypes.map16 }, // 2050, Optional + waterSpecificAlarmMask: { id: 0x0803, type: ZCLDataTypes.map16 }, // 2051, Optional + heatAndCoolingSpecificAlarmMask: { id: 0x0804, type: ZCLDataTypes.map16 }, // 2052, Optional + gasSpecificAlarmMask: { id: 0x0805, type: ZCLDataTypes.map16 }, // 2053, Optional + extendedGenericAlarmMask: { id: 0x0806, type: ZCLDataTypes.map48 }, // 2054, Optional + manufacturerAlarmMask: { id: 0x0807, type: ZCLDataTypes.map16 }, // 2055, Optional + + // Block Information Received (0x0900 - 0x09FF) + // All attributes in this section are Optional + currentNoTierBlock1SummationReceived: { id: 0x0900, type: ZCLDataTypes.uint48 }, + currentNoTierBlock2SummationReceived: { id: 0x0901, type: ZCLDataTypes.uint48 }, + currentNoTierBlock3SummationReceived: { id: 0x0902, type: ZCLDataTypes.uint48 }, + currentNoTierBlock4SummationReceived: { id: 0x0903, type: ZCLDataTypes.uint48 }, + currentNoTierBlock5SummationReceived: { id: 0x0904, type: ZCLDataTypes.uint48 }, + currentNoTierBlock6SummationReceived: { id: 0x0905, type: ZCLDataTypes.uint48 }, + currentNoTierBlock7SummationReceived: { id: 0x0906, type: ZCLDataTypes.uint48 }, + currentNoTierBlock8SummationReceived: { id: 0x0907, type: ZCLDataTypes.uint48 }, + currentNoTierBlock9SummationReceived: { id: 0x0908, type: ZCLDataTypes.uint48 }, + currentNoTierBlock10SummationReceived: { id: 0x0909, type: ZCLDataTypes.uint48 }, + currentNoTierBlock11SummationReceived: { id: 0x090A, type: ZCLDataTypes.uint48 }, + currentNoTierBlock12SummationReceived: { id: 0x090B, type: ZCLDataTypes.uint48 }, + currentNoTierBlock13SummationReceived: { id: 0x090C, type: ZCLDataTypes.uint48 }, + currentNoTierBlock14SummationReceived: { id: 0x090D, type: ZCLDataTypes.uint48 }, + currentNoTierBlock15SummationReceived: { id: 0x090E, type: ZCLDataTypes.uint48 }, + currentNoTierBlock16SummationReceived: { id: 0x090F, type: ZCLDataTypes.uint48 }, + + // Meter Billing (0x0A00 - 0x0AFF) + billToDateDelivered: { id: 0x0A00, type: ZCLDataTypes.uint32 }, // 2560, Optional + billToDateTimeStampDelivered: { id: 0x0A01, type: ZCLDataTypes.uint32 }, // 2561, Optional + projectedBillDelivered: { id: 0x0A02, type: ZCLDataTypes.uint32 }, // 2562, Optional + projectedBillTimeStampDelivered: { id: 0x0A03, type: ZCLDataTypes.uint32 }, // 2563, Optional + billDeliveredTrailingDigit: { id: 0x0A04, type: ZCLDataTypes.map8 }, // 2564, Optional + billToDateReceived: { id: 0x0A10, type: ZCLDataTypes.uint32 }, // 2576, Optional + billToDateTimeStampReceived: { id: 0x0A11, type: ZCLDataTypes.uint32 }, // 2577, Optional + projectedBillReceived: { id: 0x0A12, type: ZCLDataTypes.uint32 }, // 2578, Optional + projectedBillTimeStampReceived: { id: 0x0A13, type: ZCLDataTypes.uint32 }, // 2579, Optional + billReceivedTrailingDigit: { id: 0x0A14, type: ZCLDataTypes.map8 }, // 2580, Optional + + // Supply Control (0x0B00 - 0x0BFF) + proposedChangeSupplyImplementationTime: { // Optional + id: 0x0B00, // 2816 + type: ZCLDataTypes.uint32, + }, + proposedChangeSupplyStatus: { id: 0x0B01, type: ZCLDataTypes.enum8 }, // 2817, Optional + uncontrolledFlowThreshold: { id: 0x0B10, type: ZCLDataTypes.uint16 }, // 2832, Optional + uncontrolledFlowThresholdUnitOfMeasure: { // Optional + id: 0x0B11, // 2833 + type: ZCLDataTypes.enum8, + }, + uncontrolledFlowMultiplier: { id: 0x0B12, type: ZCLDataTypes.uint16 }, // 2834, Optional + uncontrolledFlowDivisor: { id: 0x0B13, type: ZCLDataTypes.uint16 }, // 2835, Optional + flowStabilisationPeriod: { id: 0x0B14, type: ZCLDataTypes.uint8 }, // 2836, Optional + flowMeasurementPeriod: { id: 0x0B15, type: ZCLDataTypes.uint16 }, // 2837, Optional }; const COMMANDS = {}; @@ -19,7 +255,7 @@ const COMMANDS = {}; class MeteringCluster extends Cluster { static get ID() { - return 1794; // 0x0702 + return 0x0702; // 1794 } static get NAME() { diff --git a/lib/clusters/occupancySensing.js b/lib/clusters/occupancySensing.js index ca6e902..9a0aecc 100644 --- a/lib/clusters/occupancySensing.js +++ b/lib/clusters/occupancySensing.js @@ -4,21 +4,57 @@ const Cluster = require('../Cluster'); const { ZCLDataTypes } = require('../zclTypes'); const ATTRIBUTES = { - occupancy: { id: 0, type: ZCLDataTypes.map8('occupied') }, // TODO: verify this bitmap - occupancySensorType: { - id: 1, + // Occupancy Sensor Information (0x0000 - 0x000F) + occupancy: { id: 0x0000, type: ZCLDataTypes.map8('occupied') }, // Mandatory + occupancySensorType: { // Mandatory + id: 0x0001, type: ZCLDataTypes.enum8({ - pir: 0, // 0x00 PIR - ultrasonic: 1, // 0x01 Ultrasonic - pirAndUltrasonic: 2, // 0x02 PIR and ultrasonic + pir: 0, + ultrasonic: 1, + pirAndUltrasonic: 2, + physicalContact: 3, }), }, - pirOccupiedToUnoccupiedDelay: { id: 16, type: ZCLDataTypes.uint16 }, - pirUnoccupiedToOccupiedDelay: { id: 17, type: ZCLDataTypes.uint16 }, - pirUnoccupiedToOccupiedThreshold: { id: 18, type: ZCLDataTypes.uint8 }, - ultrasonicOccupiedToUnoccupiedDelay: { id: 32, type: ZCLDataTypes.uint16 }, - ultrasonicUnoccupiedToOccupiedDelay: { id: 33, type: ZCLDataTypes.uint16 }, - ultrasonicUnoccupiedToOccupiedThreshold: { id: 34, type: ZCLDataTypes.uint8 }, + occupancySensorTypeBitmap: { // Mandatory + id: 0x0002, + type: ZCLDataTypes.map8('pir', 'ultrasonic', 'physicalContact'), + }, + + // PIR Configuration (0x0010 - 0x001F) + pirOccupiedToUnoccupiedDelay: { id: 0x0010, type: ZCLDataTypes.uint16 }, // 16, Conditional¹ + pirUnoccupiedToOccupiedDelay: { id: 0x0011, type: ZCLDataTypes.uint16 }, // 17, Conditional¹ + pirUnoccupiedToOccupiedThreshold: { id: 0x0012, type: ZCLDataTypes.uint8 }, // 18, Conditional¹ + // ¹ PIR sensor type supported + + // Ultrasonic Configuration (0x0020 - 0x002F) + ultrasonicOccupiedToUnoccupiedDelay: { // 32, Conditional² + id: 0x0020, + type: ZCLDataTypes.uint16, + }, + ultrasonicUnoccupiedToOccupiedDelay: { // 33, Conditional² + id: 0x0021, + type: ZCLDataTypes.uint16, + }, + ultrasonicUnoccupiedToOccupiedThreshold: { // 34, Conditional² + id: 0x0022, + type: ZCLDataTypes.uint8, + }, + // ² Ultrasonic sensor type supported + + // Physical Contact Configuration (0x0030 - 0x003F) + physicalContactOccupiedToUnoccupiedDelay: { // 48, Conditional³ + id: 0x0030, + type: ZCLDataTypes.uint16, + }, + physicalContactUnoccupiedToOccupiedDelay: { // 49, Conditional³ + id: 0x0031, + type: ZCLDataTypes.uint16, + }, + physicalContactUnoccupiedToOccupiedThreshold: { // 50, Conditional³ + id: 0x0032, + type: ZCLDataTypes.uint8, + }, + // ³ Physical Contact sensor type supported }; const COMMANDS = {}; @@ -26,7 +62,7 @@ const COMMANDS = {}; class OccupancySensing extends Cluster { static get ID() { - return 1030; // 0x0406 + return 0x0406; // 1030 } static get NAME() { diff --git a/lib/clusters/windowCovering.js b/lib/clusters/windowCovering.js index 2856279..3e88aa9 100644 --- a/lib/clusters/windowCovering.js +++ b/lib/clusters/windowCovering.js @@ -4,8 +4,9 @@ const Cluster = require('../Cluster'); const { ZCLDataTypes } = require('../zclTypes'); const ATTRIBUTES = { - windowCoveringType: { - id: 0, + // Window Covering Information (0x0000 - 0x000F) + windowCoveringType: { // Mandatory + id: 0x0000, type: ZCLDataTypes.enum8({ rollershade: 0, rollershade2Motor: 1, @@ -19,41 +20,75 @@ const ATTRIBUTES = { projectorScreen: 9, }), }, - physicalClosedLimitLift: { id: 1, type: ZCLDataTypes.uint16 }, - physicalClosedLimitTilt: { id: 2, type: ZCLDataTypes.uint16 }, - currentPositionLift: { id: 3, type: ZCLDataTypes.uint16 }, - currentPositionTilt: { id: 4, type: ZCLDataTypes.uint16 }, - numberofActuationsLift: { id: 5, type: ZCLDataTypes.uint16 }, - numberofActuationsTilt: { id: 6, type: ZCLDataTypes.uint16 }, - configStatus: { id: 7, type: ZCLDataTypes.map8('operational', 'online', 'reversalLiftCommands', 'controlLift', 'controlTilt', 'encoderLift', 'encoderTilt', 'reserved') }, - currentPositionLiftPercentage: { id: 8, type: ZCLDataTypes.uint8 }, - currentPositionTiltPercentage: { id: 9, type: ZCLDataTypes.uint8 }, + physicalClosedLimitLift: { id: 0x0001, type: ZCLDataTypes.uint16 }, // Optional + physicalClosedLimitTilt: { id: 0x0002, type: ZCLDataTypes.uint16 }, // Optional + currentPositionLift: { id: 0x0003, type: ZCLDataTypes.uint16 }, // Optional + currentPositionTilt: { id: 0x0004, type: ZCLDataTypes.uint16 }, // Optional + numberofActuationsLift: { id: 0x0005, type: ZCLDataTypes.uint16 }, // Optional + numberofActuationsTilt: { id: 0x0006, type: ZCLDataTypes.uint16 }, // Optional + configStatus: { // Mandatory + id: 0x0007, + type: ZCLDataTypes.map8( + 'operational', + 'online', + 'reversalLiftCommands', + 'controlLift', + 'controlTilt', + 'encoderLift', + 'encoderTilt', + 'reserved', + ), + }, + currentPositionLiftPercentage: { id: 0x0008, type: ZCLDataTypes.uint8 }, // Optional + currentPositionTiltPercentage: { id: 0x0009, type: ZCLDataTypes.uint8 }, // Optional + + // Settings (0x0010 - 0x001F) + installedOpenLimitLift: { id: 0x0010, type: ZCLDataTypes.uint16 }, // 16, Conditional¹ + installedClosedLimitLift: { id: 0x0011, type: ZCLDataTypes.uint16 }, // 17, Conditional¹ + installedOpenLimitTilt: { id: 0x0012, type: ZCLDataTypes.uint16 }, // 18, Conditional¹ + installedClosedLimitTilt: { id: 0x0013, type: ZCLDataTypes.uint16 }, // 19, Conditional¹ + // ¹ Closed Loop control and Lift/Tilt actions supported + velocityLift: { id: 0x0014, type: ZCLDataTypes.uint16 }, // 20, Optional + accelerationTimeLift: { id: 0x0015, type: ZCLDataTypes.uint16 }, // 21, Optional + decelerationTimeLift: { id: 0x0016, type: ZCLDataTypes.uint16 }, // 22, Optional + mode: { // Mandatory + id: 0x0017, // 23 + type: ZCLDataTypes.map8( + 'motorDirectionReversed', + 'calibrationMode', + 'maintenanceMode', + 'ledFeedback', + ), + }, + intermediateSetpointsLift: { id: 0x0018, type: ZCLDataTypes.octstr }, // 24, Optional + intermediateSetpointsTilt: { id: 0x0019, type: ZCLDataTypes.octstr }, // 25, Optional }; const COMMANDS = { - upOpen: { id: 0 }, - downClose: { id: 1 }, - stop: { id: 2 }, - goToLiftValue: { - id: 4, + // --- Client to Server Commands --- + upOpen: { id: 0x0000 }, // Mandatory + downClose: { id: 0x0001 }, // Mandatory + stop: { id: 0x0002 }, // Mandatory + goToLiftValue: { // Optional + id: 0x0004, args: { liftValue: ZCLDataTypes.uint16, }, }, - goToLiftPercentage: { - id: 5, + goToLiftPercentage: { // Optional + id: 0x0005, args: { percentageLiftValue: ZCLDataTypes.uint8, }, }, - goToTiltValue: { - id: 7, + goToTiltValue: { // Optional + id: 0x0007, args: { tiltValue: ZCLDataTypes.uint16, }, }, - goToTiltPercentage: { - id: 8, + goToTiltPercentage: { // Optional + id: 0x0008, args: { percentageTiltValue: ZCLDataTypes.uint8, }, @@ -63,7 +98,7 @@ const COMMANDS = { class WindowCovering extends Cluster { static get ID() { - return 258; // 0x0102 + return 0x0102; // 258 } static get NAME() { 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..a99aa37 --- /dev/null +++ b/scripts/generate-types.js @@ -0,0 +1,360 @@ +'use strict'; + +/* eslint-disable no-console, global-require */ + +/** + * Type generation script for zigbee-clusters + * Loads cluster modules directly and generates TypeScript interfaces + */ + +const fs = require('fs'); +const path = require('path'); + +const OUTPUT_FILE = path.join(__dirname, '../index.d.ts'); + +/** + * Convert a ZCLDataType object to TypeScript type string + * @param {object} dataType - ZCLDataType object with shortName and args + * @returns {string} TypeScript type string + */ +function zclTypeToTS(dataType) { + if (!dataType || !dataType.shortName) return 'unknown'; + + 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'; + // 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' || 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 === '_buffer8' || shortName === '_buffer16') { + return 'Buffer'; + } + + // Enum types - extract keys from args[0] + 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) { + return keys.map(k => `'${k}'`).join(' | '); + } + } + return 'number'; + } + + // Map/bitmap types - extract flag names from args + if (/^map(4|\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('; ')} }>`; + } + } + return 'Partial>'; + } + + // 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[]'; + } + + return 'unknown'; +} + +/** + * Parse a cluster and extract its type information + * @param {Function} ClusterClass - Cluster class with static ATTRIBUTES/COMMANDS + * @returns {object} Cluster definition + */ +function parseCluster(ClusterClass) { + const clusterName = ClusterClass.NAME; + const clusterId = ClusterClass.ID; + const attributes = []; + const commands = []; + + // 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), + }); + } + } + + // Parse commands + 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({ + name: argName, + tsType: zclTypeToTS(argType), + }); + } + } + // 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 { + clusterName, clusterId, attributes, commands, + }; +} + +/** + * Convert cluster name to PascalCase interface name + * @param {string} clusterName + * @returns {string} + */ +function toInterfaceName(clusterName) { + 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); + 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) { + // 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 allArgsOptional = cmd.args.every(a => a.tsType === 'Buffer'); + const argsType = `{ ${cmd.args.map(a => `${a.name}${a.tsType === 'Buffer' ? '?' : ''}: ${a.tsType}`).join('; ')} }`; + // 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}>;`); + } + } + + lines.push('}'); + + return lines.join('\n'); +} + +/** + * 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 = []; + + // 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 { + discoverCommandsGenerated(opts?: { + startValue?: number; + maxResults?: number; + }): Promise; + + discoverCommandsReceived(opts?: { + startValue?: number; + maxResults?: number; + }): Promise; + + readAttributes( + attributeNames: string[], + opts?: { timeout?: number } + ): Promise<{ [x: string]: unknown }>; + + writeAttributes(attributes?: object): Promise; + + configureReporting(attributes?: object): Promise; + + readReportingConfiguration(attributes?: (string | number)[]): Promise<{ + status: string; + direction: 'reported' | 'received'; + attributeId: number; + attributeDataType?: number; + minInterval?: number; + maxInterval?: number; + minChange?: number; + timeoutPeriod?: number; + }[]>; + + discoverAttributes(): Promise<(string | number)[]>; + + 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('}'); + + return lines.join('\n'); +} + +/** + * Main entry point + */ +function main() { + console.log('Loading cluster modules...'); + + // Load all clusters via the index + const clustersModule = require('../lib/clusters'); + const clusters = []; + + // 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}`); + } + } + } + + // 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(); diff --git a/test/colorControl.js b/test/colorControl.js new file mode 100644 index 0000000..0f170ec --- /dev/null +++ b/test/colorControl.js @@ -0,0 +1,94 @@ +// eslint-disable-next-line max-classes-per-file,lines-around-directive +'use strict'; + +const assert = require('assert'); +const BoundCluster = require('../lib/BoundCluster'); +const ColorControlCluster = require('../lib/clusters/colorControl'); +const { createMockNode } = require('./util'); + +describe('Color Control', function() { + it('should receive moveToColor', async function() { + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, + inputClusters: [ColorControlCluster.ID], + }], + }); + + let receivedData = null; + node.endpoints[1].bind('colorControl', new (class extends BoundCluster { + + async moveToColor(data) { + receivedData = data; + } + + })()); + + await node.endpoints[1].clusters.colorControl.moveToColor({ + colorX: 0x5000, + colorY: 0x3000, + transitionTime: 20, + }); + + assert.strictEqual(receivedData.colorX, 0x5000); + assert.strictEqual(receivedData.colorY, 0x3000); + assert.strictEqual(receivedData.transitionTime, 20); + }); + + it('should receive moveToColorTemperature', async function() { + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, + inputClusters: [ColorControlCluster.ID], + }], + }); + + let receivedData = null; + node.endpoints[1].bind('colorControl', new (class extends BoundCluster { + + async moveToColorTemperature(data) { + receivedData = data; + } + + })()); + + await node.endpoints[1].clusters.colorControl.moveToColorTemperature({ + colorTemperature: 370, + transitionTime: 15, + }); + + assert.strictEqual(receivedData.colorTemperature, 370); + assert.strictEqual(receivedData.transitionTime, 15); + }); + + it('should receive moveToHueAndSaturation', async function() { + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, + inputClusters: [ColorControlCluster.ID], + }], + }); + + let receivedData = null; + node.endpoints[1].bind('colorControl', new (class extends BoundCluster { + + async moveToHueAndSaturation(data) { + receivedData = data; + } + + })()); + + await node.endpoints[1].clusters.colorControl.moveToHueAndSaturation({ + hue: 180, + saturation: 200, + transitionTime: 10, + }); + + assert.strictEqual(receivedData.hue, 180); + assert.strictEqual(receivedData.saturation, 200); + assert.strictEqual(receivedData.transitionTime, 10); + }); +}); diff --git a/test/doorLock.js b/test/doorLock.js new file mode 100644 index 0000000..4c9c9ee --- /dev/null +++ b/test/doorLock.js @@ -0,0 +1,187 @@ +// eslint-disable-next-line max-classes-per-file,lines-around-directive +'use strict'; + +const assert = require('assert'); +const BoundCluster = require('../lib/BoundCluster'); +const DoorLockCluster = require('../lib/clusters/doorLock'); +const { createMockNode } = require('./util'); +const { ZCLStandardHeader } = require('../lib/zclFrames'); + +describe('Door Lock', function() { + it('should receive lockDoor', async function() { + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, + inputClusters: [DoorLockCluster.ID], + }], + }); + + let receivedData = null; + node.endpoints[1].bind('doorLock', new (class extends BoundCluster { + + async lockDoor(data) { + receivedData = data; + } + + })()); + + await node.endpoints[1].clusters.doorLock.lockDoor({ + pinCode: Buffer.from([0x31, 0x32, 0x33, 0x34]), + }); + + 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, + endpoints: [{ + endpointId: 1, + inputClusters: [DoorLockCluster.ID], + }], + }); + + let receivedData = null; + node.endpoints[1].bind('doorLock', new (class extends BoundCluster { + + async unlockDoor(data) { + receivedData = data; + } + + })()); + + await node.endpoints[1].clusters.doorLock.unlockDoor({ + pinCode: Buffer.from([0x30, 0x30, 0x30, 0x30]), + }); + + assert.deepStrictEqual(receivedData.pinCode, Buffer.from([0x30, 0x30, 0x30, 0x30])); + }); + + it('should receive setPINCode', async function() { + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, + inputClusters: [DoorLockCluster.ID], + }], + }); + + let receivedData = null; + node.endpoints[1].bind('doorLock', new (class extends BoundCluster { + + async setPINCode(data) { + receivedData = data; + } + + })()); + + await node.endpoints[1].clusters.doorLock.setPINCode({ + userId: 1, + userStatus: 'occupiedEnabled', + userType: 'unrestricted', + pinCode: Buffer.from([0x35, 0x36, 0x37, 0x38]), + }); + + assert.strictEqual(receivedData.userId, 1); + assert.strictEqual(receivedData.userStatus, 'occupiedEnabled'); + assert.strictEqual(receivedData.userType, 'unrestricted'); + assert.deepStrictEqual(receivedData.pinCode, Buffer.from([0x35, 0x36, 0x37, 0x38])); + }); + + // Server-to-client notifications require manual frame construction + // since they are sent BY the device TO the controller + it('should receive operationEventNotification', function(done) { + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, + inputClusters: [DoorLockCluster.ID], + }], + }); + + node.endpoints[1].clusters.doorLock.onOperationEventNotification = data => { + assert.strictEqual(data.operationEventSource, 1); // Keypad + assert.strictEqual(data.operationEventCode, 2); // Unlock + assert.strictEqual(data.userId, 3); + assert.deepStrictEqual(data.pin, Buffer.from([0x31, 0x32, 0x33, 0x34])); + assert.strictEqual(data.zigBeeLocalTime, 0x12345678); + assert.deepStrictEqual(data.data, Buffer.from([])); + done(); + }; + + const frame = new ZCLStandardHeader(); + frame.cmdId = DoorLockCluster.COMMANDS.operationEventNotification.id; + frame.frameControl.directionToClient = true; + frame.frameControl.clusterSpecific = true; + frame.data = Buffer.from([ + 0x01, 0x02, 0x03, 0x00, + 0x04, 0x31, 0x32, 0x33, 0x34, + 0x78, 0x56, 0x34, 0x12, + 0x00, + ]); + + node.handleFrame(1, DoorLockCluster.ID, frame.toBuffer(), {}); + }); + + it('should receive programmingEventNotification', function(done) { + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, + inputClusters: [DoorLockCluster.ID], + }], + }); + + node.endpoints[1].clusters.doorLock.onProgrammingEventNotification = data => { + assert.strictEqual(data.programEventSource, 2); // RF + assert.strictEqual(data.programEventCode, 3); // PIN added + assert.strictEqual(data.userId, 7); + assert.deepStrictEqual(data.pin, Buffer.from([0x35, 0x36, 0x37, 0x38])); + assert.strictEqual(data.userType, 'unrestricted'); + assert.strictEqual(data.userStatus, 'occupiedEnabled'); + assert.strictEqual(data.zigBeeLocalTime, 0xAABBCCDD); + assert.deepStrictEqual(data.data, Buffer.from([])); + done(); + }; + + const frame = new ZCLStandardHeader(); + frame.cmdId = DoorLockCluster.COMMANDS.programmingEventNotification.id; + frame.frameControl.directionToClient = true; + frame.frameControl.clusterSpecific = true; + frame.data = Buffer.from([ + 0x02, 0x03, 0x07, 0x00, + 0x04, 0x35, 0x36, 0x37, 0x38, + 0x00, 0x01, + 0xDD, 0xCC, 0xBB, 0xAA, + 0x00, + ]); + + node.handleFrame(1, DoorLockCluster.ID, frame.toBuffer(), {}); + }); +}); diff --git a/test/iasZone.js b/test/iasZone.js index 48ed5f2..b797e6b 100644 --- a/test/iasZone.js +++ b/test/iasZone.js @@ -4,83 +4,74 @@ const assert = require('assert'); const BoundCluster = require('../lib/BoundCluster'); const IASZoneCluster = require('../lib/clusters/iasZone'); -const Node = require('../lib/Node'); +const { createMockNode, MOCK_DEVICES } = require('./util'); const { ZCLStandardHeader } = require('../lib/zclFrames'); -const endpointId = 1; - describe('IAS Zone', function() { + // Server-to-client notification - requires manual frame it('should receive onZoneEnrollRequest', function(done) { - const node = new Node({ - sendFrame: () => null, - endpointDescriptors: [{ - endpointId, + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, inputClusters: [IASZoneCluster.ID], }], }); - // Listen for incoming zoneEnrollRequest command - node.endpoints[endpointId].clusters.iasZone.onZoneEnrollRequest = data => { + node.endpoints[1].clusters.iasZone.onZoneEnrollRequest = data => { assert.strictEqual(data.zoneType, 'keyPad'); assert.strictEqual(data.manufacturerCode, 4117); done(); }; - // Create zoneEnrollRequest command const frame = new ZCLStandardHeader(); frame.cmdId = IASZoneCluster.COMMANDS.zoneEnrollRequest.id; frame.frameControl.directionToClient = true; frame.frameControl.clusterSpecific = true; frame.data = Buffer.from([0x1d, 0x02, 0x15, 0x10]); - // Feed frame to node - node.handleFrame(endpointId, IASZoneCluster.ID, frame.toBuffer(), {}); + node.handleFrame(1, IASZoneCluster.ID, frame.toBuffer(), {}); }); - it('should send zoneEnrollResponse', function(done) { - const node = new Node({ - sendFrame: () => null, - endpointDescriptors: [{ - endpointId, + // Client-to-server command - can use loopback + it('should send zoneEnrollResponse', async function() { + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, inputClusters: [IASZoneCluster.ID], }], }); - // Listen for incoming zoneEnrollRequest command on bound cluster - node.endpoints[endpointId].bind( - IASZoneCluster.NAME, - new (class extends BoundCluster { + let receivedData = null; + node.endpoints[1].bind('iasZone', new (class extends BoundCluster { - async zoneEnrollResponse(data) { - assert.strictEqual(data.enrollResponseCode, 'success'); - assert.strictEqual(data.zoneId, 10); - done(); - } + async zoneEnrollResponse(data) { + receivedData = data; + } - })(), - ); - // Zone enroll response - const frame = new ZCLStandardHeader(); - frame.cmdId = IASZoneCluster.COMMANDS.zoneEnrollResponse.id; - frame.frameControl.directionToClient = false; - frame.frameControl.clusterSpecific = true; - frame.data = Buffer.from([0x00, 0x0a]); + })()); - // Feed frame to node - node.handleFrame(endpointId, IASZoneCluster.ID, frame.toBuffer(), {}); + await node.endpoints[1].clusters.iasZone.zoneEnrollResponse({ + enrollResponseCode: 'success', + zoneId: 10, + }); + + assert.strictEqual(receivedData.enrollResponseCode, 'success'); + assert.strictEqual(receivedData.zoneId, 10); }); + // Server-to-client notification - requires manual frame it('should receive zoneStatusChangeNotification', function(done) { - const node = new Node({ - sendFrame: () => null, - endpointDescriptors: [{ - endpointId, + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, inputClusters: [IASZoneCluster.ID], }], }); - // Listen for incoming zoneEnrollRequest command - node.endpoints[endpointId].clusters.iasZone.onZoneStatusChangeNotification = data => { + node.endpoints[1].clusters.iasZone.onZoneStatusChangeNotification = data => { assert.strictEqual(data.zoneStatus.supervisionReports, true); assert.strictEqual(data.extendedStatus, 0); assert.strictEqual(data.zoneId, 10); @@ -88,14 +79,44 @@ describe('IAS Zone', function() { done(); }; - // Create zoneStatusChangeNotification command const frame = new ZCLStandardHeader(); frame.cmdId = IASZoneCluster.COMMANDS.zoneStatusChangeNotification.id; frame.frameControl.directionToClient = true; frame.frameControl.clusterSpecific = true; frame.data = Buffer.from([0x10, 0x00, 0x00, 0x0A, 0x6C, 0x00]); - // Feed frame to node - node.handleFrame(endpointId, IASZoneCluster.ID, frame.toBuffer(), {}); + node.handleFrame(1, IASZoneCluster.ID, frame.toBuffer(), {}); + }); + + describe('Mock Device Factory', function() { + it('should create a motion sensor with correct zone type', function() { + const sensor = MOCK_DEVICES.motionSensor(); + const boundCluster = sensor.endpoints[1].bindings.iasZone; + assert.strictEqual(boundCluster.zoneType, 0x000D, 'Should be motion sensor type'); + assert.strictEqual(boundCluster.zoneState, 1, 'Should be enrolled'); + }); + + it('should create a contact sensor with correct zone type', function() { + const sensor = MOCK_DEVICES.contactSensor(); + const boundCluster = sensor.endpoints[1].bindings.iasZone; + assert.strictEqual(boundCluster.zoneType, 0x0015, 'Should be contact switch type'); + assert.strictEqual(boundCluster.zoneState, 1, 'Should be enrolled'); + }); + + it('should allow attribute overrides', function() { + const sensor = MOCK_DEVICES.motionSensor({ + iasZone: { zoneStatus: 0x0001 }, // Alarm1 active + }); + const boundCluster = sensor.endpoints[1].bindings.iasZone; + assert.strictEqual(boundCluster.zoneStatus, 0x0001, 'Should have alarm1 bit set'); + }); + + it('should create temp/humidity sensor with measurement values', function() { + const sensor = MOCK_DEVICES.tempHumiditySensor(); + const tempCluster = sensor.endpoints[1].bindings.temperatureMeasurement; + const humCluster = sensor.endpoints[1].bindings.relativeHumidity; + assert.strictEqual(tempCluster.measuredValue, 2150, 'Should be 21.50°C raw'); + assert.strictEqual(humCluster.measuredValue, 6500, 'Should be 65.00% raw'); + }); }); }); diff --git a/test/levelControl.js b/test/levelControl.js new file mode 100644 index 0000000..4272860 --- /dev/null +++ b/test/levelControl.js @@ -0,0 +1,87 @@ +// eslint-disable-next-line max-classes-per-file,lines-around-directive +'use strict'; + +const assert = require('assert'); +const BoundCluster = require('../lib/BoundCluster'); +const LevelControlCluster = require('../lib/clusters/levelControl'); +const { createMockNode } = require('./util'); + +describe('Level Control', function() { + it('should receive moveToLevel', async function() { + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, + inputClusters: [LevelControlCluster.ID], + }], + }); + + let receivedData = null; + node.endpoints[1].bind('levelControl', new (class extends BoundCluster { + + async moveToLevel(data) { + receivedData = data; + } + + })()); + + await node.endpoints[1].clusters.levelControl.moveToLevel({ + level: 128, + transitionTime: 10, + }); + + assert.strictEqual(receivedData.level, 128); + assert.strictEqual(receivedData.transitionTime, 10); + }); + + it('should receive step', async function() { + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, + inputClusters: [LevelControlCluster.ID], + }], + }); + + let receivedData = null; + node.endpoints[1].bind('levelControl', new (class extends BoundCluster { + + async step(data) { + receivedData = data; + } + + })()); + + await node.endpoints[1].clusters.levelControl.step({ + mode: 'up', + stepSize: 50, + transitionTime: 5, + }); + + assert.strictEqual(receivedData.mode, 'up'); + assert.strictEqual(receivedData.stepSize, 50); + assert.strictEqual(receivedData.transitionTime, 5); + }); + + it('should receive stop', async function() { + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, + inputClusters: [LevelControlCluster.ID], + }], + }); + + let called = false; + node.endpoints[1].bind('levelControl', new (class extends BoundCluster { + + async stop() { + called = true; + } + + })()); + + await node.endpoints[1].clusters.levelControl.stop(); + assert.strictEqual(called, true); + }); +}); diff --git a/test/onOff.js b/test/onOff.js new file mode 100644 index 0000000..8241c5b --- /dev/null +++ b/test/onOff.js @@ -0,0 +1,104 @@ +// eslint-disable-next-line max-classes-per-file,lines-around-directive +'use strict'; + +const assert = require('assert'); +const BoundCluster = require('../lib/BoundCluster'); +const OnOffCluster = require('../lib/clusters/onOff'); +const { createMockNode } = require('./util'); + +describe('On/Off', function() { + it('should receive setOn', async function() { + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, + inputClusters: [OnOffCluster.ID], + }], + }); + + let called = false; + node.endpoints[1].bind('onOff', new (class extends BoundCluster { + + async setOn() { + called = true; + } + + })()); + + await node.endpoints[1].clusters.onOff.setOn(); + assert.strictEqual(called, true); + }); + + it('should receive setOff', async function() { + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, + inputClusters: [OnOffCluster.ID], + }], + }); + + let called = false; + node.endpoints[1].bind('onOff', new (class extends BoundCluster { + + async setOff() { + called = true; + } + + })()); + + await node.endpoints[1].clusters.onOff.setOff(); + assert.strictEqual(called, true); + }); + + it('should receive toggle', async function() { + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, + inputClusters: [OnOffCluster.ID], + }], + }); + + let called = false; + node.endpoints[1].bind('onOff', new (class extends BoundCluster { + + async toggle() { + called = true; + } + + })()); + + await node.endpoints[1].clusters.onOff.toggle(); + assert.strictEqual(called, true); + }); + + it('should receive onWithTimedOff', async function() { + const node = createMockNode({ + loopback: true, + endpoints: [{ + endpointId: 1, + inputClusters: [OnOffCluster.ID], + }], + }); + + let receivedData = null; + node.endpoints[1].bind('onOff', new (class extends BoundCluster { + + async onWithTimedOff(data) { + receivedData = data; + } + + })()); + + await node.endpoints[1].clusters.onOff.onWithTimedOff({ + onOffControl: 0x01, + onTime: 100, + offWaitTime: 50, + }); + + assert.strictEqual(receivedData.onOffControl, 0x01); + assert.strictEqual(receivedData.onTime, 100); + assert.strictEqual(receivedData.offWaitTime, 50); + }); +}); diff --git a/test/testClusterResponseTimeout.js b/test/testClusterResponseTimeout.js index bbacde8..bfa4e50 100644 --- a/test/testClusterResponseTimeout.js +++ b/test/testClusterResponseTimeout.js @@ -6,7 +6,7 @@ const sinon = require('sinon'); const Node = require('../lib/Node'); const BoundCluster = require('../lib/BoundCluster'); -require('../lib/clusters/onOff'); +const OnOffCluster = require('../lib/clusters/onOff'); const sandbox = sinon.createSandbox(); @@ -23,7 +23,7 @@ describe('Cluster Response Timeout', function() { endpointDescriptors: [ { endpointId: 1, - inputClusters: [6], + inputClusters: [OnOffCluster.ID], }, ], }); @@ -36,7 +36,7 @@ describe('Cluster Response Timeout', function() { endpointDescriptors: [ { endpointId: 1, - inputClusters: [6], + inputClusters: [OnOffCluster.ID], }, ], }); diff --git a/test/testMfgSpecificCommands.js b/test/testMfgSpecificCommands.js index 113c2ab..5c3a96e 100644 --- a/test/testMfgSpecificCommands.js +++ b/test/testMfgSpecificCommands.js @@ -75,14 +75,15 @@ class IkeaSceneCluster extends SceneCluster { } Cluster.addCluster(IkeaSceneCluster); -const { loopbackNode } = require('./util'); +const { createMockNode } = require('./util'); -const node = loopbackNode([ - { +const node = createMockNode({ + loopback: true, + endpoints: [{ endpointId: 1, - inputClusters: [5], - }, -]); + inputClusters: [SceneCluster.ID], + }], +}); class IkeaBoundCluster extends BoundCluster { diff --git a/test/testNode.js b/test/testNode.js index f06ae5a..7cfc540 100644 --- a/test/testNode.js +++ b/test/testNode.js @@ -4,56 +4,50 @@ const assert = require('assert'); const sinon = require('sinon'); +const { createMockNode, createConnectedNodePair } = require('./util'); let { debug } = require('./util'); -const Node = require('../lib/Node'); const BoundCluster = require('../lib/BoundCluster'); const Cluster = require('../lib/Cluster'); const { ZCLDataTypes } = require('../lib/zclTypes'); const Endpoint = require('../lib/Endpoint'); -require('../lib/clusters/basic'); -require('../lib/clusters/onOff'); +const BasicCluster = require('../lib/clusters/basic'); +const OnOffCluster = require('../lib/clusters/onOff'); debug = debug.extend('test-node'); const sandbox = sinon.createSandbox(); describe('Node', function() { - let loopbackNode; + let mockNode; let receivingNode; let sendingNode; before(function() { - // eslint-disable-next-line global-require - const { loopbackNode: loopbackNodeBuilder } = require('./util'); - loopbackNode = loopbackNodeBuilder([ - { + mockNode = createMockNode({ + loopback: true, + endpoints: [{ endpointId: 1, - inputClusters: [0], + inputClusters: [BasicCluster.ID], + }], + }); + + [sendingNode, receivingNode] = createConnectedNodePair( + { + endpoints: [{ + endpointId: 1, + inputClusters: [BasicCluster.ID, OnOffCluster.ID], + }], }, - ]); - sendingNode = new Node({ - // Forward frames to receiving node - sendFrame: (...args) => receivingNode.handleFrame(...args), - endpointDescriptors: [ - { + { + endpoints: [{ endpointId: 1, - inputClusters: [0, 6], - }, - ], - }); + inputClusters: [BasicCluster.ID, OnOffCluster.ID], + }], + }, + ); // Override log id for sending node const sendingNodeLog = (...args) => `sending-node:${args.join(':')}`; sendingNode.getLogId = sendingNodeLog; sendingNode.endpoints[1].getLogId = sendingNodeLog; - receivingNode = new Node({ - // Forward frames to sending node - sendFrame: (...args) => sendingNode.handleFrame(...args), - endpointDescriptors: [ - { - endpointId: 1, - inputClusters: [0, 6], - }, - ], - }); // Override log id for receiving node const receivingNodeLog = (...args) => `receiving-node:${args.join(':')}`; receivingNode.getLogId = receivingNodeLog; @@ -64,7 +58,7 @@ describe('Node', function() { }); it('should fail for unbound cluster', async function() { try { - await loopbackNode.endpoints[1].clusters['basic'].configureReporting({ + await mockNode.endpoints[1].clusters['basic'].configureReporting({ zclVersion: { minInterval: 1234, maxInterval: 4321, @@ -77,10 +71,10 @@ describe('Node', function() { }); it('should fail for unimplemented command', async function() { - loopbackNode.endpoints[1].bind('basic', new BoundCluster()); + mockNode.endpoints[1].bind('basic', new BoundCluster()); try { - await loopbackNode.endpoints[1].clusters['basic'].factoryReset(); + await mockNode.endpoints[1].clusters['basic'].factoryReset(); } catch (e) { return; } @@ -88,7 +82,7 @@ describe('Node', function() { }); it('should invoke command', async function() { - loopbackNode.endpoints[1].bind('basic', new class extends BoundCluster { + mockNode.endpoints[1].bind('basic', new class extends BoundCluster { async factoryReset() { debug('factory reset'); @@ -96,11 +90,11 @@ describe('Node', function() { }()); - await loopbackNode.endpoints[1].clusters['basic'].factoryReset(); + await mockNode.endpoints[1].clusters['basic'].factoryReset(); }); it('should configure reporting', async function() { - loopbackNode.endpoints[1].bind('basic', new class extends BoundCluster { + mockNode.endpoints[1].bind('basic', new class extends BoundCluster { async configureReporting({ reports }) { assert.equal(reports.length, 1, 'exactly 1 report'); @@ -108,7 +102,7 @@ describe('Node', function() { }()); - await loopbackNode.endpoints[1].clusters['basic'].configureReporting({ + await mockNode.endpoints[1].clusters['basic'].configureReporting({ zclVersion: { minInterval: 10, maxInterval: 4321, @@ -117,7 +111,7 @@ describe('Node', function() { }); it('should read attributes', async function() { - loopbackNode.endpoints[1].bind('basic', new class extends BoundCluster { + mockNode.endpoints[1].bind('basic', new class extends BoundCluster { constructor() { super(); @@ -134,7 +128,7 @@ describe('Node', function() { }()); - const res = await loopbackNode.endpoints[1].clusters['basic'].readAttributes(['modelId', 'manufacturerName', 'clusterRevision', 'zclVersion', 'dateCode']); + const res = await mockNode.endpoints[1].clusters['basic'].readAttributes(['modelId', 'manufacturerName', 'clusterRevision', 'zclVersion', 'dateCode']); assert.equal(res.modelId, 'test', 'modelId should be test'); assert.equal(res.manufacturerName, 'Athom', 'manufacturerName should be test'); assert.equal(res.clusterRevision, 1, 'clusterRevision should be test'); @@ -243,7 +237,7 @@ describe('Node', function() { }); it('should write attributes', async function() { - loopbackNode.endpoints[1].bind('basic', new class extends BoundCluster { + mockNode.endpoints[1].bind('basic', new class extends BoundCluster { get modelId() { return 'test'; @@ -259,13 +253,13 @@ describe('Node', function() { }()); - await loopbackNode.endpoints[1].clusters['basic'].writeAttributes({ + await mockNode.endpoints[1].clusters['basic'].writeAttributes({ modelId: 'test1', }); }); it('should discover attributes', async function() { - loopbackNode.endpoints[1].bind('basic', new class extends BoundCluster { + mockNode.endpoints[1].bind('basic', new class extends BoundCluster { get modelId() { return 'test'; @@ -281,21 +275,21 @@ describe('Node', function() { }()); - const attrs = await loopbackNode.endpoints[1].clusters['basic'].discoverAttributes(); + const attrs = await mockNode.endpoints[1].clusters['basic'].discoverAttributes(); ['modelId', 'manufacturerName'].forEach(a => { assert(attrs.includes(a), `${a} is missing`); }); }); it('should discover received commands', async function() { - loopbackNode.endpoints[1].bind('basic', new class extends BoundCluster { + mockNode.endpoints[1].bind('basic', new class extends BoundCluster { async factoryReset() { debug('factory reset'); } }()); - const cmds = await loopbackNode.endpoints[1].clusters['basic'].discoverCommandsReceived(); + const cmds = await mockNode.endpoints[1].clusters['basic'].discoverCommandsReceived(); assert(cmds.includes('factoryReset')); }); diff --git a/test/util/index.js b/test/util/index.js index 2331cc3..dadbea9 100644 --- a/test/util/index.js +++ b/test/util/index.js @@ -4,20 +4,18 @@ let { debug } = require('../../lib/util'); debug = debug.extend('test'); -const Node = require('../../lib/Node'); - -const debugUtil = debug.extend('util'); - -const loopbackNode = config => { - const remotenode = { - sendFrame: (...args) => remotenode.handleFrame(...args), - bind: debugUtil.bind(debugUtil, 'binding: ep %d, cluster %d '), - endpointDescriptors: config, - }; - return new Node(remotenode); -}; +const { + createMockNode, + createConnectedNodePair, + createBoundClusterWithAttributes, + MOCK_DEVICES, +} = require('./mockNode'); module.exports = { debug, - loopbackNode, + // Mock node utilities + createMockNode, + createConnectedNodePair, + createBoundClusterWithAttributes, + MOCK_DEVICES, }; diff --git a/test/util/mockNode.js b/test/util/mockNode.js new file mode 100644 index 0000000..a303794 --- /dev/null +++ b/test/util/mockNode.js @@ -0,0 +1,316 @@ +'use strict'; + +const Node = require('../../lib/Node'); +const BoundCluster = require('../../lib/BoundCluster'); + +// Load clusters and get their IDs +const BasicCluster = require('../../lib/clusters/basic'); +const PowerConfigurationCluster = require('../../lib/clusters/powerConfiguration'); +const IASZoneCluster = require('../../lib/clusters/iasZone'); +const TemperatureMeasurementCluster = require('../../lib/clusters/temperatureMeasurement'); +const RelativeHumidityCluster = require('../../lib/clusters/relativeHumidity'); +const OnOffCluster = require('../../lib/clusters/onOff'); +const MeteringCluster = require('../../lib/clusters/metering'); +const ElectricalMeasurementCluster = require('../../lib/clusters/electricalMeasurement'); + +/** + * Creates a BoundCluster with getter properties for each attribute. + * + * @param {Object} attributes - Attribute name/value pairs + * @returns {BoundCluster} Configured BoundCluster + */ +function createBoundClusterWithAttributes(attributes) { + const ClusterClass = class extends BoundCluster { + + constructor() { + super(); + // Store mutable attributes + this._attributes = { ...attributes }; + } + + }; + + // Define getters/setters for each attribute + Object.keys(attributes).forEach(attrName => { + Object.defineProperty(ClusterClass.prototype, attrName, { + get() { + return this._attributes[attrName]; + }, + set(value) { + this._attributes[attrName] = value; + }, + enumerable: true, + configurable: true, + }); + }); + + return new ClusterClass(); +} + +/** + * Creates a mock device Node with configurable cluster attributes. + * + * @example + * const mockMotionSensor = createMockDevice({ + * endpoints: [{ + * endpointId: 1, + * inputClusters: [0x0000, 0x0001, 0x0500], + * clusters: { + * iasZone: { + * zoneType: 0x000D, // Motion sensor + * zoneState: 1, // Enrolled + * zoneStatus: 0, + * }, + * powerConfiguration: { + * batteryPercentageRemaining: 180, // 90% + * }, + * }, + * }], + * }); + * + * @param {Object} config - Device configuration + * @param {Array} config.endpoints - Endpoint configurations + * @param {number} config.endpoints[].endpointId - Endpoint ID + * @param {number[]} config.endpoints[].inputClusters - Input cluster IDs + * @param {number[]} [config.endpoints[].outputClusters] - Output cluster IDs + * @param {Object} [config.endpoints[].clusters] - Cluster attribute values keyed by cluster name + * @param {boolean} [config.loopback] - If true, frames are looped back to the node's handleFrame + * @returns {Node} Configured mock Node + */ +function createMockNode(config) { + const endpointDescriptors = config.endpoints.map(ep => ({ + endpointId: ep.endpointId, + inputClusters: ep.inputClusters || [], + outputClusters: ep.outputClusters || [], + })); + + // Create node reference first so sendFrame can reference it + let node; + const mockNode = { + sendFrame: config.loopback + ? (...args) => node.handleFrame(...args) + : () => Promise.resolve(), + endpointDescriptors, + }; + + node = new Node(mockNode); + + // Bind clusters with preset attribute values + config.endpoints.forEach(ep => { + if (!ep.clusters) return; + + Object.entries(ep.clusters).forEach(([clusterName, attributes]) => { + const boundCluster = createBoundClusterWithAttributes(attributes); + node.endpoints[ep.endpointId].bind(clusterName, boundCluster); + }); + }); + + return node; +} + +/** + * Creates a pair of connected mock nodes that can send frames to each other. + * + * @param {Object} config1 - Configuration for first node + * @param {Object} config2 - Configuration for second node + * @returns {[Node, Node]} Tuple of connected nodes + */ +function createConnectedNodePair(config1, config2) { + const endpointDescriptors1 = config1.endpoints.map(ep => ({ + endpointId: ep.endpointId, + inputClusters: ep.inputClusters || [], + outputClusters: ep.outputClusters || [], + })); + + const endpointDescriptors2 = config2.endpoints.map(ep => ({ + endpointId: ep.endpointId, + inputClusters: ep.inputClusters || [], + outputClusters: ep.outputClusters || [], + })); + + let node1; + let node2; + + const mockNode1 = { + sendFrame: (...args) => node2.handleFrame(...args), + endpointDescriptors: endpointDescriptors1, + }; + + const mockNode2 = { + sendFrame: (...args) => node1.handleFrame(...args), + endpointDescriptors: endpointDescriptors2, + }; + + node1 = new Node(mockNode1); + node2 = new Node(mockNode2); + + // Bind clusters for node1 + config1.endpoints.forEach(ep => { + if (!ep.clusters) return; + Object.entries(ep.clusters).forEach(([clusterName, attributes]) => { + const boundCluster = createBoundClusterWithAttributes(attributes); + node1.endpoints[ep.endpointId].bind(clusterName, boundCluster); + }); + }); + + // Bind clusters for node2 + config2.endpoints.forEach(ep => { + if (!ep.clusters) return; + Object.entries(ep.clusters).forEach(([clusterName, attributes]) => { + const boundCluster = createBoundClusterWithAttributes(attributes); + node2.endpoints[ep.endpointId].bind(clusterName, boundCluster); + }); + }); + + return [node1, node2]; +} + +/** + * Preset device configurations for common device types. + */ +const MOCK_DEVICES = { + /** + * IAS Zone Motion Sensor + */ + motionSensor: (overrides = {}) => createMockNode({ + endpoints: [{ + endpointId: 1, + inputClusters: [BasicCluster.ID, PowerConfigurationCluster.ID, IASZoneCluster.ID], + clusters: { + basic: { + zclVersion: 3, + manufacturerName: 'MockDevice', + modelId: 'MockMotionSensor', + ...overrides.basic, + }, + powerConfiguration: { + batteryPercentageRemaining: 200, // 100% + ...overrides.powerConfiguration, + }, + iasZone: { + zoneState: 1, // Enrolled + zoneType: 0x000D, // Motion sensor + zoneStatus: 0, + iasCIEAddress: '0x0000000000000000', + zoneId: 1, + ...overrides.iasZone, + }, + }, + }], + }), + + /** + * IAS Zone Contact Sensor (door/window) + */ + contactSensor: (overrides = {}) => createMockNode({ + endpoints: [{ + endpointId: 1, + inputClusters: [BasicCluster.ID, PowerConfigurationCluster.ID, IASZoneCluster.ID], + clusters: { + basic: { + zclVersion: 3, + manufacturerName: 'MockDevice', + modelId: 'MockContactSensor', + ...overrides.basic, + }, + powerConfiguration: { + batteryPercentageRemaining: 200, + ...overrides.powerConfiguration, + }, + iasZone: { + zoneState: 1, + zoneType: 0x0015, // Contact switch + zoneStatus: 0, + iasCIEAddress: '0x0000000000000000', + zoneId: 1, + ...overrides.iasZone, + }, + }, + }], + }), + + /** + * Temperature + Humidity Sensor + */ + tempHumiditySensor: (overrides = {}) => createMockNode({ + endpoints: [{ + endpointId: 1, + inputClusters: [ + BasicCluster.ID, + PowerConfigurationCluster.ID, + TemperatureMeasurementCluster.ID, + RelativeHumidityCluster.ID, + ], + clusters: { + basic: { + zclVersion: 3, + manufacturerName: 'MockDevice', + modelId: 'MockTempHumidity', + ...overrides.basic, + }, + powerConfiguration: { + batteryPercentageRemaining: 200, + ...overrides.powerConfiguration, + }, + temperatureMeasurement: { + measuredValue: 2150, // 21.50°C + minMeasuredValue: -4000, + maxMeasuredValue: 8500, + ...overrides.temperatureMeasurement, + }, + relativeHumidity: { + measuredValue: 6500, // 65.00% + minMeasuredValue: 0, + maxMeasuredValue: 10000, + ...overrides.relativeHumidity, + }, + }, + }], + }), + + /** + * Smart Plug with Power Metering + */ + smartPlug: (overrides = {}) => createMockNode({ + endpoints: [{ + endpointId: 1, + inputClusters: [ + BasicCluster.ID, + OnOffCluster.ID, + MeteringCluster.ID, + ElectricalMeasurementCluster.ID, + ], + clusters: { + basic: { + zclVersion: 3, + manufacturerName: 'MockDevice', + modelId: 'MockSmartPlug', + ...overrides.basic, + }, + onOff: { + onOff: false, + ...overrides.onOff, + }, + metering: { + currentSummationDelivered: 12345678, + multiplier: 1, + divisor: 1000, + ...overrides.metering, + }, + electricalMeasurement: { + activePower: 1500, // 150.0W + rmsVoltage: 2300, // 230.0V + rmsCurrent: 652, // 0.652A + ...overrides.electricalMeasurement, + }, + }, + }], + }), +}; + +module.exports = { + createMockNode, + createConnectedNodePair, + createBoundClusterWithAttributes, + MOCK_DEVICES, +};