zigbee-clusters 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,360 @@
1
+ 'use strict';
2
+
3
+ /* eslint-disable no-console, global-require */
4
+
5
+ /**
6
+ * Type generation script for zigbee-clusters
7
+ * Loads cluster modules directly and generates TypeScript interfaces
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const OUTPUT_FILE = path.join(__dirname, '../index.d.ts');
14
+
15
+ /**
16
+ * Convert a ZCLDataType object to TypeScript type string
17
+ * @param {object} dataType - ZCLDataType object with shortName and args
18
+ * @returns {string} TypeScript type string
19
+ */
20
+ function zclTypeToTS(dataType) {
21
+ if (!dataType || !dataType.shortName) return 'unknown';
22
+
23
+ const { shortName, args } = dataType;
24
+
25
+ // No data
26
+ if (shortName === 'noData') return 'null';
27
+
28
+ // Boolean
29
+ if (shortName === 'bool') return 'boolean';
30
+
31
+ // Numeric types
32
+ if (/^u?int\d+$/.test(shortName)) return 'number';
33
+ // data8-32 use readUIntBE, return number
34
+ if (/^data(8|16|24|32)$/.test(shortName)) return 'number';
35
+ // data40-64 use buf.slice, return Buffer
36
+ if (/^data(40|48|56|64)$/.test(shortName)) return 'Buffer';
37
+ // Float types
38
+ if (shortName === 'single' || shortName === 'double') return 'number';
39
+
40
+ // String types
41
+ if (shortName === 'string' || shortName === '_FixedString') return 'string';
42
+ // EUI addresses return colon-separated hex strings
43
+ if (shortName === 'EUI64' || shortName === 'EUI48') return 'string';
44
+ // key128 returns colon-separated hex string
45
+ if (shortName === 'key128') return 'string';
46
+
47
+ // Buffer types
48
+ if (shortName === 'octstr' || shortName === '_buffer' || shortName === '_buffer8' || shortName === '_buffer16') {
49
+ return 'Buffer';
50
+ }
51
+
52
+ // Enum types - extract keys from args[0]
53
+ if (/^enum(4|8|16|32)$/.test(shortName)) {
54
+ if (args && args[0] && typeof args[0] === 'object') {
55
+ const keys = Object.keys(args[0]);
56
+ if (keys.length > 0) {
57
+ return keys.map(k => `'${k}'`).join(' | ');
58
+ }
59
+ }
60
+ return 'number';
61
+ }
62
+
63
+ // Map/bitmap types - extract flag names from args
64
+ if (/^map(4|\d+)$/.test(shortName)) {
65
+ if (args && args.length > 0) {
66
+ const flags = args.filter(a => typeof a === 'string');
67
+ if (flags.length > 0) {
68
+ return `Partial<{ ${flags.map(f => `${f}: boolean`).join('; ')} }>`;
69
+ }
70
+ }
71
+ return 'Partial<Record<string, boolean>>';
72
+ }
73
+
74
+ // Array types (note: shortName has underscore prefix: _Array0, _Array8)
75
+ // Recursively determine element type from args[0]
76
+ if (/^_?Array(0|8|16)$/.test(shortName)) {
77
+ if (args && args[0]) {
78
+ const elementType = zclTypeToTS(args[0]);
79
+ return `${elementType}[]`;
80
+ }
81
+ return 'unknown[]';
82
+ }
83
+
84
+ return 'unknown';
85
+ }
86
+
87
+ /**
88
+ * Parse a cluster and extract its type information
89
+ * @param {Function} ClusterClass - Cluster class with static ATTRIBUTES/COMMANDS
90
+ * @returns {object} Cluster definition
91
+ */
92
+ function parseCluster(ClusterClass) {
93
+ const clusterName = ClusterClass.NAME;
94
+ const clusterId = ClusterClass.ID;
95
+ const attributes = [];
96
+ const commands = [];
97
+
98
+ // Parse attributes
99
+ const attrs = ClusterClass.ATTRIBUTES || {};
100
+ for (const [name, def] of Object.entries(attrs)) {
101
+ if (def && def.type) {
102
+ attributes.push({
103
+ name,
104
+ tsType: zclTypeToTS(def.type),
105
+ });
106
+ }
107
+ }
108
+
109
+ // Parse commands
110
+ const cmds = ClusterClass.COMMANDS || {};
111
+ for (const [name, def] of Object.entries(cmds)) {
112
+ const cmdArgs = [];
113
+ const responseArgs = [];
114
+ if (def && def.args) {
115
+ for (const [argName, argType] of Object.entries(def.args)) {
116
+ cmdArgs.push({
117
+ name: argName,
118
+ tsType: zclTypeToTS(argType),
119
+ });
120
+ }
121
+ }
122
+ // Parse response type if present
123
+ if (def && def.response && def.response.args) {
124
+ for (const [argName, argType] of Object.entries(def.response.args)) {
125
+ responseArgs.push({
126
+ name: argName,
127
+ tsType: zclTypeToTS(argType),
128
+ });
129
+ }
130
+ }
131
+ commands.push({ name, args: cmdArgs, responseArgs });
132
+ }
133
+
134
+ return {
135
+ clusterName, clusterId, attributes, commands,
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Convert cluster name to PascalCase interface name
141
+ * @param {string} clusterName
142
+ * @returns {string}
143
+ */
144
+ function toInterfaceName(clusterName) {
145
+ const name = clusterName.charAt(0).toUpperCase() + clusterName.slice(1);
146
+ return `${name}Cluster`;
147
+ }
148
+
149
+ /**
150
+ * Generate TypeScript interface for a cluster
151
+ * @param {object} cluster - Parsed cluster definition
152
+ * @returns {string} TypeScript interface code
153
+ */
154
+ function generateClusterInterface(cluster) {
155
+ const interfaceName = toInterfaceName(cluster.clusterName);
156
+ const lines = [];
157
+
158
+ // Generate attributes interface
159
+ if (cluster.attributes.length > 0) {
160
+ lines.push(`export interface ${interfaceName}Attributes {`);
161
+ for (const attr of cluster.attributes) {
162
+ lines.push(` ${attr.name}?: ${attr.tsType};`);
163
+ }
164
+ lines.push('}');
165
+ lines.push('');
166
+ }
167
+
168
+ // Generate cluster interface
169
+ lines.push(`export interface ${interfaceName} extends ZCLNodeCluster {`);
170
+
171
+ // Add typed readAttributes if we have attributes
172
+ if (cluster.attributes.length > 0) {
173
+ const attrNames = cluster.attributes.map(a => `'${a.name}'`).join(' | ');
174
+ lines.push(` readAttributes<K extends ${attrNames}>(attributeNames: K[], opts?: { timeout?: number }): Promise<Pick<${interfaceName}Attributes, K>>;`);
175
+ lines.push(` writeAttributes(attributes: Partial<${interfaceName}Attributes>): Promise<void>;`);
176
+ }
177
+
178
+ // Add command methods
179
+ for (const cmd of cluster.commands) {
180
+ // Determine return type based on response args
181
+ let returnType = 'void';
182
+ if (cmd.responseArgs && cmd.responseArgs.length > 0) {
183
+ returnType = `{ ${cmd.responseArgs.map(a => `${a.name}: ${a.tsType}`).join('; ')} }`;
184
+ }
185
+
186
+ if (cmd.args.length > 0) {
187
+ // Buffer arguments are optional - ZCL allows empty octet strings
188
+ const allArgsOptional = cmd.args.every(a => a.tsType === 'Buffer');
189
+ const argsType = `{ ${cmd.args.map(a => `${a.name}${a.tsType === 'Buffer' ? '?' : ''}: ${a.tsType}`).join('; ')} }`;
190
+ // If all args are optional, make the entire args object optional
191
+ lines.push(` ${cmd.name}(args${allArgsOptional ? '?' : ''}: ${argsType}): Promise<${returnType}>;`);
192
+ } else {
193
+ lines.push(` ${cmd.name}(): Promise<${returnType}>;`);
194
+ }
195
+ }
196
+
197
+ lines.push('}');
198
+
199
+ return lines.join('\n');
200
+ }
201
+
202
+ /**
203
+ * Generate the full index.d.ts file
204
+ * @param {object[]} clusters - Array of parsed cluster definitions
205
+ * @returns {string} Complete TypeScript definitions file
206
+ */
207
+ function generateTypesFile(clusters) {
208
+ const lines = [];
209
+
210
+ // Header
211
+ lines.push('// Auto-generated TypeScript definitions for zigbee-clusters');
212
+ lines.push('// Generated by scripts/generate-types.js');
213
+ lines.push('');
214
+ lines.push('import * as EventEmitter from "events";');
215
+ lines.push('');
216
+
217
+ // Base types
218
+ lines.push(`type EndpointDescriptor = {
219
+ endpointId: number;
220
+ inputClusters: number[];
221
+ outputClusters: number[];
222
+ };
223
+
224
+ type ConstructorOptions = {
225
+ endpointDescriptors: EndpointDescriptor[];
226
+ sendFrame: (endpointId: number, clusterId: number, frame: Buffer) => Promise<void>;
227
+ };
228
+ `);
229
+
230
+ // Base ZCLNodeCluster interface
231
+ lines.push(`export interface ZCLNodeCluster extends EventEmitter {
232
+ discoverCommandsGenerated(opts?: {
233
+ startValue?: number;
234
+ maxResults?: number;
235
+ }): Promise<number[]>;
236
+
237
+ discoverCommandsReceived(opts?: {
238
+ startValue?: number;
239
+ maxResults?: number;
240
+ }): Promise<number[]>;
241
+
242
+ readAttributes(
243
+ attributeNames: string[],
244
+ opts?: { timeout?: number }
245
+ ): Promise<{ [x: string]: unknown }>;
246
+
247
+ writeAttributes(attributes?: object): Promise<void>;
248
+
249
+ configureReporting(attributes?: object): Promise<void>;
250
+
251
+ readReportingConfiguration(attributes?: (string | number)[]): Promise<{
252
+ status: string;
253
+ direction: 'reported' | 'received';
254
+ attributeId: number;
255
+ attributeDataType?: number;
256
+ minInterval?: number;
257
+ maxInterval?: number;
258
+ minChange?: number;
259
+ timeoutPeriod?: number;
260
+ }[]>;
261
+
262
+ discoverAttributes(): Promise<(string | number)[]>;
263
+
264
+ discoverAttributesExtended(): Promise<{
265
+ name?: string;
266
+ id: number;
267
+ acl: { readable: boolean; writable: boolean; reportable: boolean };
268
+ }[]>;
269
+ }
270
+ `);
271
+
272
+ // Generate individual cluster interfaces
273
+ for (const cluster of clusters) {
274
+ lines.push(generateClusterInterface(cluster));
275
+ lines.push('');
276
+ }
277
+
278
+ // Generate cluster registry type
279
+ lines.push('/** Type-safe cluster registry */');
280
+ lines.push('export interface ClusterRegistry {');
281
+ for (const cluster of clusters) {
282
+ const interfaceName = toInterfaceName(cluster.clusterName);
283
+ lines.push(` ${cluster.clusterName}?: ${interfaceName};`);
284
+ }
285
+ lines.push('}');
286
+ lines.push('');
287
+
288
+ // Generate endpoint type
289
+ lines.push(`export type ZCLNodeEndpoint = {
290
+ clusters: ClusterRegistry & {
291
+ [clusterName: string]: ZCLNodeCluster | undefined;
292
+ };
293
+ };
294
+
295
+ export interface ZCLNode {
296
+ endpoints: { [endpointId: number | string]: ZCLNodeEndpoint };
297
+ handleFrame: (
298
+ endpointId: number,
299
+ clusterId: number,
300
+ frame: Buffer,
301
+ meta?: unknown
302
+ ) => Promise<void>;
303
+ }
304
+ `);
305
+
306
+ // Module declaration for CommonJS compatibility
307
+ lines.push(`declare module "zigbee-clusters" {
308
+ export const ZCLNode: {
309
+ new (options: ConstructorOptions): ZCLNode;
310
+ };
311
+ export const CLUSTER: {
312
+ [key: string]: { ID: number; NAME: string; ATTRIBUTES: unknown; COMMANDS: unknown };
313
+ };
314
+ export { ZCLNodeCluster };`);
315
+
316
+ // Export all cluster classes
317
+ for (const cluster of clusters) {
318
+ const interfaceName = toInterfaceName(cluster.clusterName);
319
+ lines.push(` export const ${interfaceName}: ${interfaceName};`);
320
+ }
321
+
322
+ lines.push('}');
323
+
324
+ return lines.join('\n');
325
+ }
326
+
327
+ /**
328
+ * Main entry point
329
+ */
330
+ function main() {
331
+ console.log('Loading cluster modules...');
332
+
333
+ // Load all clusters via the index
334
+ const clustersModule = require('../lib/clusters');
335
+ const clusters = [];
336
+
337
+ // Get all exported cluster classes (end with 'Cluster')
338
+ for (const [name, value] of Object.entries(clustersModule)) {
339
+ if (name.endsWith('Cluster') && typeof value === 'function' && value.NAME) {
340
+ try {
341
+ const cluster = parseCluster(value);
342
+ clusters.push(cluster);
343
+ console.log(` ✓ ${cluster.clusterName} (${cluster.attributes.length} attrs, ${cluster.commands.length} cmds)`);
344
+ } catch (err) {
345
+ console.warn(` ✗ Failed to parse ${name}: ${err.message}`);
346
+ }
347
+ }
348
+ }
349
+
350
+ // Sort clusters alphabetically
351
+ clusters.sort((a, b) => a.clusterName.localeCompare(b.clusterName));
352
+
353
+ console.log(`\nGenerating ${OUTPUT_FILE}...`);
354
+ const output = generateTypesFile(clusters);
355
+ fs.writeFileSync(OUTPUT_FILE, output);
356
+
357
+ console.log(`Done! Generated types for ${clusters.length} clusters.`);
358
+ }
359
+
360
+ main();