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.
- package/LICENSE +21 -0
- package/README.md +367 -0
- package/config.schema.json +71 -0
- package/package.json +57 -0
- package/src/SmartikaCrypto.js +134 -0
- package/src/SmartikaDiscovery.js +177 -0
- package/src/SmartikaHubConnection.js +528 -0
- package/src/SmartikaPlatform.js +379 -0
- package/src/SmartikaProtocol.js +977 -0
- package/src/accessories/SmartikaFanAccessory.js +162 -0
- package/src/accessories/SmartikaLightAccessory.js +203 -0
- package/src/accessories/SmartikaPlugAccessory.js +112 -0
- package/src/index.js +12 -0
- package/src/settings.js +16 -0
- package/tools/smartika-cli.js +1443 -0
|
@@ -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
|
+
};
|