homebridge-smartika 1.0.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,977 @@
1
+ 'use strict';
2
+
3
+ // Protocol constants
4
+ const START_MARK_REQUEST = 0xFE00;
5
+ const START_MARK_RESPONSE = 0xFE01;
6
+ const END_MARK = 0x00FF;
7
+
8
+ // Broadcast address for all devices
9
+ const DEVICE_ID_BROADCAST = 0xFFFF;
10
+
11
+ // Command IDs (from hub simulator)
12
+ const CMD = {
13
+ // Main device commands
14
+ DEVICE_SWITCH: 0x0000, // Turn device on/off
15
+ DEVICE_DISCOVERY: 0x0001, // Discover devices on network
16
+ DEVICE_STATUS: 0x0002, // Get device status
17
+ LIGHT_DIM: 0x0004, // Set light brightness (0-255)
18
+ LIGHT_TEMPERATURE: 0x0005, // Set light color temperature (0-255)
19
+ FAN_CONTROL: 0x0006, // Control fan speed
20
+ LIGHT_DIM_BATCH: 0x0008, // Set brightness for multiple devices
21
+ LIGHT_TEMPERATURE_BATCH: 0x0009, // Set temperature for multiple devices
22
+
23
+ // System commands
24
+ GATEWAY_ID: 0x0010, // Fetch hub UUID (returns "artika" + hub_id)
25
+ PING: 0x0101, // Keep-alive
26
+ CREDENTIALS: 0x0103, // Authentication (used by hub->bridge)
27
+ JOIN_ENABLE: 0x0104, // Enable device pairing
28
+ JOIN_DISABLE: 0x0105, // Disable device pairing
29
+ FIRMWARE_VERSION: 0x0106, // Get firmware version
30
+
31
+ // Database commands
32
+ DB_LIST_DEVICE: 0x0200, // List registered device IDs
33
+ DB_ADD_DEVICE: 0x0201, // Add device to database
34
+ DB_REMOVE_DEVICE: 0x0202, // Remove device from database
35
+ DB_LIST_DEVICE_FULL: 0x0203, // List registered devices with full info
36
+
37
+ // Group commands
38
+ GROUP_LIST: 0x0400, // List all groups
39
+ GROUP_CREATE: 0x0401, // Create a new group
40
+ GROUP_UPDATE: 0x0402, // Update group members
41
+ GROUP_READ: 0x0403, // Read group members
42
+ GROUP_DELETE: 0x0404, // Delete groups
43
+ };
44
+
45
+ // Hub communication port
46
+ const HUB_PORT = 1234;
47
+
48
+ // Device types
49
+ const DEVICE_TYPES = {
50
+ // Real devices
51
+ 0x00000001: 'Champagne Track',
52
+ 0x00000005: 'Ceiling Fan',
53
+ 0x00000007: 'Smart Plug',
54
+ 0x00000008: 'Mini Wall Washer',
55
+ 0x00000009: 'Glowbox',
56
+ 0x0000000B: 'Recessed Lighting',
57
+ 0x0000000D: 'Water Leakage Sensor',
58
+ 0x00001001: 'Pendant 1',
59
+ 0x00001002: 'Pendant 2',
60
+ 0x00001003: 'Pendant 3',
61
+ 0x00001004: 'Pendant 4',
62
+ 0x00001005: 'Pendant 5',
63
+ 0x00001006: 'Smart Bulb',
64
+ 0x00001007: 'Spotlight',
65
+ 0x00001008: 'Sandwich Light 1',
66
+ 0x00001009: 'Sandwich Light 2',
67
+ 0x0000100A: 'Sandwich Light 3',
68
+ 0x00002001: 'Thermostat',
69
+ 0x00002002: 'Smart Heater',
70
+ // Virtual devices (groups)
71
+ 0x40000001: 'Virtual Light',
72
+ 0x40000003: 'Virtual Fan',
73
+ 0x40000004: 'Virtual Plug',
74
+ 0x40002002: 'Virtual Heater',
75
+ // Remote controls
76
+ 0x80000002: 'Remote Control Light',
77
+ 0x80000004: 'Remote Control Heater',
78
+ 0x80000006: 'Remote Control Fan',
79
+ 0x80000008: 'Programmable Remote',
80
+ };
81
+
82
+ // Device categories for HomeKit mapping
83
+ const DEVICE_CATEGORY = {
84
+ LIGHT: 'light',
85
+ FAN: 'fan',
86
+ PLUG: 'plug',
87
+ THERMOSTAT: 'thermostat',
88
+ SENSOR: 'sensor',
89
+ REMOTE: 'remote',
90
+ };
91
+
92
+ // Map device types to categories
93
+ const DEVICE_TYPE_CATEGORY = {
94
+ 0x00000001: DEVICE_CATEGORY.LIGHT, // Champagne Track
95
+ 0x00000005: DEVICE_CATEGORY.FAN, // Ceiling Fan
96
+ 0x00000007: DEVICE_CATEGORY.PLUG, // Smart Plug
97
+ 0x00000008: DEVICE_CATEGORY.LIGHT, // Mini Wall Washer
98
+ 0x00000009: DEVICE_CATEGORY.LIGHT, // Glowbox
99
+ 0x0000000B: DEVICE_CATEGORY.LIGHT, // Recessed Lighting
100
+ 0x0000000D: DEVICE_CATEGORY.SENSOR, // Water Leakage Sensor
101
+ 0x00001001: DEVICE_CATEGORY.LIGHT, // Pendant 1
102
+ 0x00001002: DEVICE_CATEGORY.LIGHT, // Pendant 2
103
+ 0x00001003: DEVICE_CATEGORY.LIGHT, // Pendant 3
104
+ 0x00001004: DEVICE_CATEGORY.LIGHT, // Pendant 4
105
+ 0x00001005: DEVICE_CATEGORY.LIGHT, // Pendant 5
106
+ 0x00001006: DEVICE_CATEGORY.LIGHT, // Smart Bulb
107
+ 0x00001007: DEVICE_CATEGORY.LIGHT, // Spotlight
108
+ 0x00001008: DEVICE_CATEGORY.LIGHT, // Sandwich Light 1
109
+ 0x00001009: DEVICE_CATEGORY.LIGHT, // Sandwich Light 2
110
+ 0x0000100A: DEVICE_CATEGORY.LIGHT, // Sandwich Light 3
111
+ 0x00002001: DEVICE_CATEGORY.THERMOSTAT, // Thermostat
112
+ 0x00002002: DEVICE_CATEGORY.THERMOSTAT, // Smart Heater
113
+ 0x40000001: DEVICE_CATEGORY.LIGHT, // Virtual Light
114
+ 0x40000003: DEVICE_CATEGORY.FAN, // Virtual Fan
115
+ 0x40000004: DEVICE_CATEGORY.PLUG, // Virtual Plug
116
+ 0x40002002: DEVICE_CATEGORY.THERMOSTAT, // Virtual Heater
117
+ 0x80000002: DEVICE_CATEGORY.REMOTE, // Remote Control Light
118
+ 0x80000004: DEVICE_CATEGORY.REMOTE, // Remote Control Heater
119
+ 0x80000006: DEVICE_CATEGORY.REMOTE, // Remote Control Fan
120
+ 0x80000008: DEVICE_CATEGORY.REMOTE, // Programmable Remote
121
+ };
122
+
123
+ // ============================================================================
124
+ // Core Protocol Functions
125
+ // ============================================================================
126
+
127
+ /**
128
+ * Compute XOR checksum of buffer
129
+ * @param {Buffer} data
130
+ * @returns {number}
131
+ */
132
+ function computeChecksum(data) {
133
+ let fcs = 0;
134
+ for (let i = 0; i < data.length; i++) {
135
+ fcs ^= data[i];
136
+ }
137
+ return fcs;
138
+ }
139
+
140
+ /**
141
+ * Create a command packet
142
+ * @param {number} cmdId - Command ID
143
+ * @param {Buffer} data - Command data (optional)
144
+ * @param {number} listLen - List length (optional)
145
+ * @param {boolean} isRequest - True for request, false for response
146
+ * @returns {Buffer}
147
+ */
148
+ function createPacket(cmdId, data = Buffer.alloc(0), listLen = 0, isRequest = true) {
149
+ const startMark = isRequest ? START_MARK_REQUEST : START_MARK_RESPONSE;
150
+ const dataLen = data.length;
151
+
152
+ // Build the FCS data (cmd + len + listLen + data)
153
+ const fcsData = Buffer.alloc(6 + dataLen);
154
+ fcsData.writeUInt16BE(cmdId, 0);
155
+ fcsData.writeUInt16BE(dataLen, 2);
156
+ fcsData.writeUInt16BE(listLen, 4);
157
+ data.copy(fcsData, 6);
158
+
159
+ const fcs = computeChecksum(fcsData);
160
+
161
+ // Build full packet
162
+ const packet = Buffer.alloc(2 + fcsData.length + 1 + 2);
163
+ packet.writeUInt16BE(startMark, 0);
164
+ fcsData.copy(packet, 2);
165
+ packet.writeUInt8(fcs, 2 + fcsData.length);
166
+ packet.writeUInt16BE(END_MARK, 2 + fcsData.length + 1);
167
+
168
+ return packet;
169
+ }
170
+
171
+ /**
172
+ * Parse a response packet
173
+ * @param {Buffer} packet
174
+ * @returns {Object} - { cmdId, dataLen, listLen, data, isRequest }
175
+ */
176
+ function parsePacket(packet) {
177
+ if (packet.length < 11) {
178
+ throw new Error('Packet too short');
179
+ }
180
+
181
+ const startMark = packet.readUInt16BE(0);
182
+ const isRequest = startMark === START_MARK_REQUEST;
183
+
184
+ if (startMark !== START_MARK_REQUEST && startMark !== START_MARK_RESPONSE) {
185
+ throw new Error(`Invalid start mark: 0x${startMark.toString(16)}`);
186
+ }
187
+
188
+ const cmdId = packet.readUInt16BE(2);
189
+ const dataLen = packet.readUInt16BE(4);
190
+ const listLen = packet.readUInt16BE(6);
191
+
192
+ if (packet.length < 8 + dataLen + 3) {
193
+ throw new Error('Packet data incomplete');
194
+ }
195
+
196
+ const data = packet.subarray(8, 8 + dataLen);
197
+ const fcs = packet.readUInt8(8 + dataLen);
198
+ const endMark = packet.readUInt16BE(8 + dataLen + 1);
199
+
200
+ if (endMark !== END_MARK) {
201
+ throw new Error(`Invalid end mark: 0x${endMark.toString(16)}`);
202
+ }
203
+
204
+ // Verify checksum
205
+ const fcsData = packet.subarray(2, 8 + dataLen);
206
+ const expectedFcs = computeChecksum(fcsData);
207
+ if (fcs !== expectedFcs) {
208
+ throw new Error(`Checksum mismatch: got 0x${fcs.toString(16)}, expected 0x${expectedFcs.toString(16)}`);
209
+ }
210
+
211
+ return { cmdId, dataLen, listLen, data, isRequest };
212
+ }
213
+
214
+ // ============================================================================
215
+ // System Commands
216
+ // ============================================================================
217
+
218
+ /**
219
+ * Create gateway ID request (fetch hub UUID)
220
+ * @returns {Buffer}
221
+ */
222
+ function createGatewayIdRequest() {
223
+ return createPacket(CMD.GATEWAY_ID, Buffer.alloc(0), 0, true);
224
+ }
225
+
226
+ /**
227
+ * Parse gateway ID response
228
+ * @param {Buffer} packet
229
+ * @returns {Object} - { prefix, hubId, hubIdHex }
230
+ */
231
+ function parseGatewayIdResponse(packet) {
232
+ const { cmdId, data } = parsePacket(packet);
233
+ if (cmdId !== CMD.GATEWAY_ID) {
234
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
235
+ }
236
+
237
+ // Response format: "artika" (6 bytes) + hub_id (6 bytes) = 12 bytes
238
+ if (data.length < 12) {
239
+ throw new Error(`Gateway ID response too short: ${data.length} bytes`);
240
+ }
241
+
242
+ const prefix = data.subarray(0, 6).toString('ascii');
243
+ const hubId = data.subarray(6, 12);
244
+
245
+ return {
246
+ prefix,
247
+ hubId,
248
+ hubIdHex: hubId.toString('hex').toUpperCase(),
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Create ping request
254
+ * @returns {Buffer}
255
+ */
256
+ function createPingRequest() {
257
+ return createPacket(CMD.PING, Buffer.alloc(0), 0, true);
258
+ }
259
+
260
+ /**
261
+ * Parse ping response
262
+ * @param {Buffer} packet
263
+ * @returns {Object} - { alarmSet }
264
+ */
265
+ function parsePingResponse(packet) {
266
+ const { cmdId, data } = parsePacket(packet);
267
+ if (cmdId !== CMD.PING) {
268
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
269
+ }
270
+ return {
271
+ alarmSet: data.length > 0 && data[0] !== 0,
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Create credentials request
277
+ * @param {Buffer} hubId - 6 bytes hub MAC address
278
+ * @returns {Buffer}
279
+ */
280
+ function createCredentialsRequest(hubId) {
281
+ const prefix = Buffer.from('artika', 'ascii');
282
+ const data = Buffer.concat([prefix, hubId.subarray(0, 6)]);
283
+ return createPacket(CMD.CREDENTIALS, data, 0, true);
284
+ }
285
+
286
+ /**
287
+ * Parse credentials response
288
+ * @param {Buffer} packet
289
+ * @returns {boolean} - Success
290
+ */
291
+ function parseCredentialsResponse(packet) {
292
+ const { cmdId, data } = parsePacket(packet);
293
+ if (cmdId !== CMD.CREDENTIALS) {
294
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
295
+ }
296
+ return data.length === 0 || data[0] !== 0;
297
+ }
298
+
299
+ /**
300
+ * Create join enable request
301
+ * @param {number} duration - Duration in seconds (0 = use default)
302
+ * @returns {Buffer}
303
+ */
304
+ function createJoinEnableRequest(duration = 0) {
305
+ const data = Buffer.alloc(1);
306
+ data.writeUInt8(duration, 0);
307
+ return createPacket(CMD.JOIN_ENABLE, data, 0, true);
308
+ }
309
+
310
+ /**
311
+ * Parse join enable response
312
+ * @param {Buffer} packet
313
+ * @returns {Object} - { duration }
314
+ */
315
+ function parseJoinEnableResponse(packet) {
316
+ const { cmdId, data } = parsePacket(packet);
317
+ if (cmdId !== CMD.JOIN_ENABLE) {
318
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
319
+ }
320
+ return {
321
+ duration: data.length > 0 ? data.readUInt8(0) : 0,
322
+ };
323
+ }
324
+
325
+ /**
326
+ * Create join disable request
327
+ * @returns {Buffer}
328
+ */
329
+ function createJoinDisableRequest() {
330
+ return createPacket(CMD.JOIN_DISABLE, Buffer.alloc(0), 0, true);
331
+ }
332
+
333
+ /**
334
+ * Create firmware version request
335
+ * @returns {Buffer}
336
+ */
337
+ function createFirmwareVersionRequest() {
338
+ return createPacket(CMD.FIRMWARE_VERSION, Buffer.alloc(0), 0, true);
339
+ }
340
+
341
+ /**
342
+ * Parse firmware version response
343
+ * @param {Buffer} packet
344
+ * @returns {Object} - { major, minor, patch, version }
345
+ */
346
+ function parseFirmwareVersionResponse(packet) {
347
+ const { cmdId, data } = parsePacket(packet);
348
+ if (cmdId !== CMD.FIRMWARE_VERSION) {
349
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
350
+ }
351
+ if (data.length < 3) {
352
+ throw new Error('Firmware version response too short');
353
+ }
354
+ const major = data.readUInt8(0);
355
+ const minor = data.readUInt8(1);
356
+ const patch = data.readUInt8(2);
357
+ return {
358
+ major,
359
+ minor,
360
+ patch,
361
+ version: `${major}.${minor}.${patch}`,
362
+ };
363
+ }
364
+
365
+ // ============================================================================
366
+ // Device Commands
367
+ // ============================================================================
368
+
369
+ /**
370
+ * Create device discovery request
371
+ * @returns {Buffer}
372
+ */
373
+ function createDeviceDiscoveryRequest() {
374
+ return createPacket(CMD.DEVICE_DISCOVERY, Buffer.alloc(0), 0, true);
375
+ }
376
+
377
+ /**
378
+ * Parse device discovery response
379
+ * @param {Buffer} packet
380
+ * @returns {Array} - List of devices
381
+ */
382
+ function parseDeviceDiscoveryResponse(packet) {
383
+ const { cmdId, listLen, data } = parsePacket(packet);
384
+ if (cmdId !== CMD.DEVICE_DISCOVERY) {
385
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
386
+ }
387
+
388
+ const devices = [];
389
+ // Each device: short_address(2) + type_id(4) + mac_address(8) = 14 bytes
390
+ const deviceSize = 14;
391
+
392
+ for (let i = 0; i < listLen && (i * deviceSize) < data.length; i++) {
393
+ const offset = i * deviceSize;
394
+ const shortAddress = data.readUInt16BE(offset);
395
+ const typeId = data.readUInt32BE(offset + 2);
396
+ const macAddress = data.subarray(offset + 6, offset + 14);
397
+
398
+ devices.push({
399
+ shortAddress,
400
+ typeId,
401
+ typeName: DEVICE_TYPES[typeId] || `Unknown (0x${typeId.toString(16)})`,
402
+ category: DEVICE_TYPE_CATEGORY[typeId] || 'unknown',
403
+ macAddress: macAddress.toString('hex').toUpperCase(),
404
+ });
405
+ }
406
+
407
+ return devices;
408
+ }
409
+
410
+ /**
411
+ * Create device status request
412
+ * @param {number[]} deviceIds - Array of device short addresses (use DEVICE_ID_BROADCAST for all)
413
+ * @returns {Buffer}
414
+ */
415
+ function createDeviceStatusRequest(deviceIds = [DEVICE_ID_BROADCAST]) {
416
+ const data = Buffer.alloc(deviceIds.length * 2);
417
+ deviceIds.forEach((id, index) => {
418
+ data.writeUInt16BE(id, index * 2);
419
+ });
420
+ return createPacket(CMD.DEVICE_STATUS, data, deviceIds.length, true);
421
+ }
422
+
423
+ /**
424
+ * Parse device status response
425
+ * @param {Buffer} packet
426
+ * @returns {Array} - List of device statuses
427
+ */
428
+ function parseDeviceStatusResponse(packet) {
429
+ const { cmdId, listLen, data } = parsePacket(packet);
430
+ if (cmdId !== CMD.DEVICE_STATUS) {
431
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
432
+ }
433
+
434
+ const devices = [];
435
+ let offset = 0;
436
+
437
+ for (let i = 0; i < listLen && offset < data.length; i++) {
438
+ const shortAddress = data.readUInt16BE(offset);
439
+ const typeId = data.readUInt32BE(offset + 2);
440
+ const stateLen = data.readUInt8(offset + 6);
441
+ const stateData = data.subarray(offset + 7, offset + 7 + stateLen);
442
+
443
+ const device = {
444
+ shortAddress,
445
+ typeId,
446
+ typeName: DEVICE_TYPES[typeId] || `Unknown (0x${typeId.toString(16)})`,
447
+ category: DEVICE_TYPE_CATEGORY[typeId] || 'unknown',
448
+ };
449
+
450
+ // Parse state based on device category
451
+ const category = DEVICE_TYPE_CATEGORY[typeId];
452
+ if (category === DEVICE_CATEGORY.LIGHT && stateLen >= 3) {
453
+ device.on = stateData[0] !== 0;
454
+ device.brightness = stateData[1]; // 0-255
455
+ device.temperature = stateData[2]; // 0-255
456
+ } else if (category === DEVICE_CATEGORY.FAN && stateLen >= 2) {
457
+ device.on = stateData[0] !== 0;
458
+ device.speed = stateData[1]; // 0-255
459
+ } else if (category === DEVICE_CATEGORY.PLUG && stateLen >= 1) {
460
+ device.on = stateData[0] !== 0;
461
+ } else {
462
+ device.rawState = stateData.toString('hex').toUpperCase();
463
+ }
464
+
465
+ devices.push(device);
466
+ offset += 7 + stateLen;
467
+ }
468
+
469
+ return devices;
470
+ }
471
+
472
+ /**
473
+ * Create device switch request (on/off)
474
+ * @param {boolean} on - True to turn on, false to turn off
475
+ * @param {number[]} deviceIds - Array of device short addresses
476
+ * @returns {Buffer}
477
+ */
478
+ function createDeviceSwitchRequest(on, deviceIds) {
479
+ // Format: on(1 byte boolean) + device_ids (2 bytes each)
480
+ const data = Buffer.alloc(1 + deviceIds.length * 2);
481
+ data.writeUInt8(on ? 1 : 0, 0);
482
+ deviceIds.forEach((id, index) => {
483
+ data.writeUInt16BE(id, 1 + index * 2);
484
+ });
485
+ return createPacket(CMD.DEVICE_SWITCH, data, deviceIds.length, true);
486
+ }
487
+
488
+ /**
489
+ * Parse device switch response
490
+ * @param {Buffer} packet
491
+ * @returns {Object} - { deviceIds }
492
+ */
493
+ function parseDeviceSwitchResponse(packet) {
494
+ const { cmdId, listLen, data } = parsePacket(packet);
495
+ if (cmdId !== CMD.DEVICE_SWITCH) {
496
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
497
+ }
498
+
499
+ const deviceIds = [];
500
+ for (let i = 0; i < listLen; i++) {
501
+ deviceIds.push(data.readUInt16BE(i * 2));
502
+ }
503
+ return { deviceIds };
504
+ }
505
+
506
+ /**
507
+ * Create light dim request
508
+ * @param {number} brightness - Brightness level (0-255)
509
+ * @param {number[]} deviceIds - Array of device short addresses
510
+ * @returns {Buffer}
511
+ */
512
+ function createLightDimRequest(brightness, deviceIds) {
513
+ // Format: brightness(1 byte) + device_ids (2 bytes each)
514
+ const data = Buffer.alloc(1 + deviceIds.length * 2);
515
+ data.writeUInt8(brightness, 0);
516
+ deviceIds.forEach((id, index) => {
517
+ data.writeUInt16BE(id, 1 + index * 2);
518
+ });
519
+ return createPacket(CMD.LIGHT_DIM, data, deviceIds.length, true);
520
+ }
521
+
522
+ /**
523
+ * Parse light dim response
524
+ * @param {Buffer} packet
525
+ * @returns {Object} - { deviceIds }
526
+ */
527
+ function parseLightDimResponse(packet) {
528
+ const { cmdId, listLen, data } = parsePacket(packet);
529
+ if (cmdId !== CMD.LIGHT_DIM) {
530
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
531
+ }
532
+
533
+ const deviceIds = [];
534
+ for (let i = 0; i < listLen; i++) {
535
+ deviceIds.push(data.readUInt16BE(i * 2));
536
+ }
537
+ return { deviceIds };
538
+ }
539
+
540
+ /**
541
+ * Create light temperature request
542
+ * @param {number} temperature - Color temperature (0-255, warm to cool)
543
+ * @param {number[]} deviceIds - Array of device short addresses
544
+ * @returns {Buffer}
545
+ */
546
+ function createLightTemperatureRequest(temperature, deviceIds) {
547
+ // Format: temperature(1 byte) + device_ids (2 bytes each)
548
+ const data = Buffer.alloc(1 + deviceIds.length * 2);
549
+ data.writeUInt8(temperature, 0);
550
+ deviceIds.forEach((id, index) => {
551
+ data.writeUInt16BE(id, 1 + index * 2);
552
+ });
553
+ return createPacket(CMD.LIGHT_TEMPERATURE, data, deviceIds.length, true);
554
+ }
555
+
556
+ /**
557
+ * Parse light temperature response
558
+ * @param {Buffer} packet
559
+ * @returns {Object} - { deviceIds }
560
+ */
561
+ function parseLightTemperatureResponse(packet) {
562
+ const { cmdId, listLen, data } = parsePacket(packet);
563
+ if (cmdId !== CMD.LIGHT_TEMPERATURE) {
564
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
565
+ }
566
+
567
+ const deviceIds = [];
568
+ for (let i = 0; i < listLen; i++) {
569
+ deviceIds.push(data.readUInt16BE(i * 2));
570
+ }
571
+ return { deviceIds };
572
+ }
573
+
574
+ /**
575
+ * Create fan control request
576
+ * @param {number} speed - Fan speed (0-255)
577
+ * @param {number[]} deviceIds - Array of device short addresses
578
+ * @returns {Buffer}
579
+ */
580
+ function createFanControlRequest(speed, deviceIds) {
581
+ const data = Buffer.alloc(1 + deviceIds.length * 2);
582
+ data.writeUInt8(speed, 0);
583
+ deviceIds.forEach((id, index) => {
584
+ data.writeUInt16BE(id, 1 + index * 2);
585
+ });
586
+ return createPacket(CMD.FAN_CONTROL, data, deviceIds.length, true);
587
+ }
588
+
589
+ /**
590
+ * Create light dim batch request (different brightness per device)
591
+ * @param {Array<{deviceId: number, brightness: number}>} devices
592
+ * @returns {Buffer}
593
+ */
594
+ function createLightDimBatchRequest(devices) {
595
+ // Format: (device_id(2) + brightness(1)) per device
596
+ const data = Buffer.alloc(devices.length * 3);
597
+ devices.forEach((device, index) => {
598
+ data.writeUInt16BE(device.deviceId, index * 3);
599
+ data.writeUInt8(device.brightness, index * 3 + 2);
600
+ });
601
+ return createPacket(CMD.LIGHT_DIM_BATCH, data, devices.length, true);
602
+ }
603
+
604
+ /**
605
+ * Create light temperature batch request (different temperature per device)
606
+ * @param {Array<{deviceId: number, temperature: number}>} devices
607
+ * @returns {Buffer}
608
+ */
609
+ function createLightTemperatureBatchRequest(devices) {
610
+ // Format: (device_id(2) + temperature(1)) per device
611
+ const data = Buffer.alloc(devices.length * 3);
612
+ devices.forEach((device, index) => {
613
+ data.writeUInt16BE(device.deviceId, index * 3);
614
+ data.writeUInt8(device.temperature, index * 3 + 2);
615
+ });
616
+ return createPacket(CMD.LIGHT_TEMPERATURE_BATCH, data, devices.length, true);
617
+ }
618
+
619
+ // ============================================================================
620
+ // Database Commands
621
+ // ============================================================================
622
+
623
+ /**
624
+ * Create DB list device request (IDs only)
625
+ * @returns {Buffer}
626
+ */
627
+ function createDbListDeviceRequest() {
628
+ return createPacket(CMD.DB_LIST_DEVICE, Buffer.alloc(0), 0, true);
629
+ }
630
+
631
+ /**
632
+ * Parse DB list device response
633
+ * @param {Buffer} packet
634
+ * @returns {Object} - { deviceIds }
635
+ */
636
+ function parseDbListDeviceResponse(packet) {
637
+ const { cmdId, listLen, data } = parsePacket(packet);
638
+ if (cmdId !== CMD.DB_LIST_DEVICE) {
639
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
640
+ }
641
+
642
+ const deviceIds = [];
643
+ for (let i = 0; i < listLen; i++) {
644
+ deviceIds.push(data.readUInt16BE(i * 2));
645
+ }
646
+ return { deviceIds };
647
+ }
648
+
649
+ /**
650
+ * Create DB list device full request
651
+ * @returns {Buffer}
652
+ */
653
+ function createDbListDeviceFullRequest() {
654
+ return createPacket(CMD.DB_LIST_DEVICE_FULL, Buffer.alloc(0), 0, true);
655
+ }
656
+
657
+ /**
658
+ * Parse DB list device full response
659
+ * @param {Buffer} packet
660
+ * @returns {Array} - List of devices with short_address, type_id, mac_address
661
+ */
662
+ function parseDbListDeviceFullResponse(packet) {
663
+ const { cmdId, listLen, data } = parsePacket(packet);
664
+ if (cmdId !== CMD.DB_LIST_DEVICE_FULL) {
665
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
666
+ }
667
+
668
+ const devices = [];
669
+ // Each device: short_address(2) + type_id(4) + mac_address(8) = 14 bytes
670
+ const deviceSize = 14;
671
+
672
+ for (let i = 0; i < listLen && (i * deviceSize) < data.length; i++) {
673
+ const offset = i * deviceSize;
674
+ const shortAddress = data.readUInt16BE(offset);
675
+ const typeId = data.readUInt32BE(offset + 2);
676
+ const macAddress = data.readBigUInt64BE(offset + 6);
677
+
678
+ devices.push({
679
+ shortAddress,
680
+ typeId,
681
+ typeName: DEVICE_TYPES[typeId] || `Unknown (0x${typeId.toString(16)})`,
682
+ category: DEVICE_TYPE_CATEGORY[typeId] || 'unknown',
683
+ macAddress: macAddress.toString(16).toUpperCase().padStart(16, '0'),
684
+ });
685
+ }
686
+
687
+ return devices;
688
+ }
689
+
690
+ /**
691
+ * Create DB add device request
692
+ * @param {number[]} deviceIds - Array of device short addresses to add
693
+ * @returns {Buffer}
694
+ */
695
+ function createDbAddDeviceRequest(deviceIds) {
696
+ const data = Buffer.alloc(deviceIds.length * 2);
697
+ deviceIds.forEach((id, index) => {
698
+ data.writeUInt16BE(id, index * 2);
699
+ });
700
+ return createPacket(CMD.DB_ADD_DEVICE, data, deviceIds.length, true);
701
+ }
702
+
703
+ /**
704
+ * Parse DB add device response
705
+ * @param {Buffer} packet
706
+ * @returns {Object} - { errorIds } - device IDs that failed to add
707
+ */
708
+ function parseDbAddDeviceResponse(packet) {
709
+ const { cmdId, listLen, data } = parsePacket(packet);
710
+ if (cmdId !== CMD.DB_ADD_DEVICE) {
711
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
712
+ }
713
+
714
+ const errorIds = [];
715
+ for (let i = 0; i < listLen; i++) {
716
+ errorIds.push(data.readUInt16BE(i * 2));
717
+ }
718
+ return { errorIds };
719
+ }
720
+
721
+ /**
722
+ * Create DB remove device request
723
+ * @param {number[]} deviceIds - Array of device short addresses to remove
724
+ * @returns {Buffer}
725
+ */
726
+ function createDbRemoveDeviceRequest(deviceIds) {
727
+ const data = Buffer.alloc(deviceIds.length * 2);
728
+ deviceIds.forEach((id, index) => {
729
+ data.writeUInt16BE(id, index * 2);
730
+ });
731
+ return createPacket(CMD.DB_REMOVE_DEVICE, data, deviceIds.length, true);
732
+ }
733
+
734
+ /**
735
+ * Parse DB remove device response
736
+ * @param {Buffer} packet
737
+ * @returns {Object} - { errorIds } - device IDs that failed to remove
738
+ */
739
+ function parseDbRemoveDeviceResponse(packet) {
740
+ const { cmdId, listLen, data } = parsePacket(packet);
741
+ if (cmdId !== CMD.DB_REMOVE_DEVICE) {
742
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
743
+ }
744
+
745
+ const errorIds = [];
746
+ for (let i = 0; i < listLen; i++) {
747
+ errorIds.push(data.readUInt16BE(i * 2));
748
+ }
749
+ return { errorIds };
750
+ }
751
+
752
+ // ============================================================================
753
+ // Group Commands
754
+ // ============================================================================
755
+
756
+ /**
757
+ * Create group list request
758
+ * @returns {Buffer}
759
+ */
760
+ function createGroupListRequest() {
761
+ return createPacket(CMD.GROUP_LIST, Buffer.alloc(0), 0, true);
762
+ }
763
+
764
+ /**
765
+ * Parse group list response
766
+ * @param {Buffer} packet
767
+ * @returns {Object} - { groupIds }
768
+ */
769
+ function parseGroupListResponse(packet) {
770
+ const { cmdId, listLen, data } = parsePacket(packet);
771
+ if (cmdId !== CMD.GROUP_LIST) {
772
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
773
+ }
774
+
775
+ const groupIds = [];
776
+ for (let i = 0; i < listLen; i++) {
777
+ groupIds.push(data.readUInt16BE(i * 2));
778
+ }
779
+ return { groupIds };
780
+ }
781
+
782
+ /**
783
+ * Create group create request
784
+ * @param {number[]} deviceIds - Array of device short addresses to include in group
785
+ * @returns {Buffer}
786
+ */
787
+ function createGroupCreateRequest(deviceIds) {
788
+ const data = Buffer.alloc(deviceIds.length * 2);
789
+ deviceIds.forEach((id, index) => {
790
+ data.writeUInt16BE(id, index * 2);
791
+ });
792
+ return createPacket(CMD.GROUP_CREATE, data, deviceIds.length, true);
793
+ }
794
+
795
+ /**
796
+ * Parse group create response
797
+ * @param {Buffer} packet
798
+ * @returns {Object} - { groupId } - 0xFFFF on error
799
+ */
800
+ function parseGroupCreateResponse(packet) {
801
+ const { cmdId, data } = parsePacket(packet);
802
+ if (cmdId !== CMD.GROUP_CREATE) {
803
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
804
+ }
805
+ return {
806
+ groupId: data.readUInt16BE(0),
807
+ success: data.readUInt16BE(0) !== 0xFFFF,
808
+ };
809
+ }
810
+
811
+ /**
812
+ * Create group update request
813
+ * @param {number} groupId - Group short address
814
+ * @param {number[]} deviceIds - New array of device short addresses
815
+ * @returns {Buffer}
816
+ */
817
+ function createGroupUpdateRequest(groupId, deviceIds) {
818
+ const data = Buffer.alloc(2 + deviceIds.length * 2);
819
+ data.writeUInt16BE(groupId, 0);
820
+ deviceIds.forEach((id, index) => {
821
+ data.writeUInt16BE(id, 2 + index * 2);
822
+ });
823
+ return createPacket(CMD.GROUP_UPDATE, data, deviceIds.length, true);
824
+ }
825
+
826
+ /**
827
+ * Parse group update response
828
+ * @param {Buffer} packet
829
+ * @returns {Object} - { groupId } - 0xFFFF on error
830
+ */
831
+ function parseGroupUpdateResponse(packet) {
832
+ const { cmdId, data } = parsePacket(packet);
833
+ if (cmdId !== CMD.GROUP_UPDATE) {
834
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
835
+ }
836
+ return {
837
+ groupId: data.readUInt16BE(0),
838
+ success: data.readUInt16BE(0) !== 0xFFFF,
839
+ };
840
+ }
841
+
842
+ /**
843
+ * Create group read request
844
+ * @param {number} groupId - Group short address
845
+ * @returns {Buffer}
846
+ */
847
+ function createGroupReadRequest(groupId) {
848
+ const data = Buffer.alloc(2);
849
+ data.writeUInt16BE(groupId, 0);
850
+ return createPacket(CMD.GROUP_READ, data, 0, true);
851
+ }
852
+
853
+ /**
854
+ * Parse group read response
855
+ * @param {Buffer} packet
856
+ * @returns {Object} - { groupId, deviceIds }
857
+ */
858
+ function parseGroupReadResponse(packet) {
859
+ const { cmdId, listLen, data } = parsePacket(packet);
860
+ if (cmdId !== CMD.GROUP_READ) {
861
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
862
+ }
863
+
864
+ const groupId = data.readUInt16BE(0);
865
+ const deviceIds = [];
866
+ for (let i = 0; i < listLen; i++) {
867
+ deviceIds.push(data.readUInt16BE(2 + i * 2));
868
+ }
869
+ return {
870
+ groupId,
871
+ success: groupId !== 0xFFFF,
872
+ deviceIds,
873
+ };
874
+ }
875
+
876
+ /**
877
+ * Create group delete request
878
+ * @param {number[]} groupIds - Array of group short addresses to delete
879
+ * @returns {Buffer}
880
+ */
881
+ function createGroupDeleteRequest(groupIds) {
882
+ const data = Buffer.alloc(groupIds.length * 2);
883
+ groupIds.forEach((id, index) => {
884
+ data.writeUInt16BE(id, index * 2);
885
+ });
886
+ return createPacket(CMD.GROUP_DELETE, data, groupIds.length, true);
887
+ }
888
+
889
+ /**
890
+ * Parse group delete response
891
+ * @param {Buffer} packet
892
+ * @returns {Object} - { errorIds } - group IDs that failed to delete
893
+ */
894
+ function parseGroupDeleteResponse(packet) {
895
+ const { cmdId, listLen, data } = parsePacket(packet);
896
+ if (cmdId !== CMD.GROUP_DELETE) {
897
+ throw new Error(`Unexpected command ID: 0x${cmdId.toString(16)}`);
898
+ }
899
+
900
+ const errorIds = [];
901
+ for (let i = 0; i < listLen; i++) {
902
+ errorIds.push(data.readUInt16BE(i * 2));
903
+ }
904
+ return { errorIds };
905
+ }
906
+
907
+ // ============================================================================
908
+ // Exports
909
+ // ============================================================================
910
+
911
+ module.exports = {
912
+ // Constants
913
+ CMD,
914
+ DEVICE_TYPES,
915
+ DEVICE_CATEGORY,
916
+ DEVICE_TYPE_CATEGORY,
917
+ HUB_PORT,
918
+ START_MARK_REQUEST,
919
+ START_MARK_RESPONSE,
920
+ END_MARK,
921
+ DEVICE_ID_BROADCAST,
922
+
923
+ // Core
924
+ computeChecksum,
925
+ createPacket,
926
+ parsePacket,
927
+
928
+ // System commands
929
+ createGatewayIdRequest,
930
+ parseGatewayIdResponse,
931
+ createPingRequest,
932
+ parsePingResponse,
933
+ createCredentialsRequest,
934
+ parseCredentialsResponse,
935
+ createJoinEnableRequest,
936
+ parseJoinEnableResponse,
937
+ createJoinDisableRequest,
938
+ createFirmwareVersionRequest,
939
+ parseFirmwareVersionResponse,
940
+
941
+ // Device commands
942
+ createDeviceDiscoveryRequest,
943
+ parseDeviceDiscoveryResponse,
944
+ createDeviceStatusRequest,
945
+ parseDeviceStatusResponse,
946
+ createDeviceSwitchRequest,
947
+ parseDeviceSwitchResponse,
948
+ createLightDimRequest,
949
+ parseLightDimResponse,
950
+ createLightTemperatureRequest,
951
+ parseLightTemperatureResponse,
952
+ createFanControlRequest,
953
+ createLightDimBatchRequest,
954
+ createLightTemperatureBatchRequest,
955
+
956
+ // Database commands
957
+ createDbListDeviceRequest,
958
+ parseDbListDeviceResponse,
959
+ createDbListDeviceFullRequest,
960
+ parseDbListDeviceFullResponse,
961
+ createDbAddDeviceRequest,
962
+ parseDbAddDeviceResponse,
963
+ createDbRemoveDeviceRequest,
964
+ parseDbRemoveDeviceResponse,
965
+
966
+ // Group commands
967
+ createGroupListRequest,
968
+ parseGroupListResponse,
969
+ createGroupCreateRequest,
970
+ parseGroupCreateResponse,
971
+ createGroupUpdateRequest,
972
+ parseGroupUpdateResponse,
973
+ createGroupReadRequest,
974
+ parseGroupReadResponse,
975
+ createGroupDeleteRequest,
976
+ parseGroupDeleteResponse,
977
+ };