homebridge-roborock-matter 2.1.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.
Files changed (59) hide show
  1. package/.gitattributes +13 -0
  2. package/CHANGELOG.md +469 -0
  3. package/LICENSE +23 -0
  4. package/README.md +45 -0
  5. package/__tests__/testdata/b01_q7_get_prop_response.json +1 -0
  6. package/__tests__/testdata/b01_q7_map_fixture.json +16 -0
  7. package/config.schema.json +118 -0
  8. package/dist/crypto.js +65 -0
  9. package/dist/crypto.js.map +1 -0
  10. package/dist/index.js +10 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/live_message.js +25 -0
  13. package/dist/live_message.js.map +1 -0
  14. package/dist/logger.js +37 -0
  15. package/dist/logger.js.map +1 -0
  16. package/dist/matter_vacuum_accessory.js +1877 -0
  17. package/dist/matter_vacuum_accessory.js.map +1 -0
  18. package/dist/platform.js +441 -0
  19. package/dist/platform.js.map +1 -0
  20. package/dist/settings.js +8 -0
  21. package/dist/settings.js.map +1 -0
  22. package/dist/types.js +3 -0
  23. package/dist/types.js.map +1 -0
  24. package/dist/ui/index.js +741 -0
  25. package/dist/ui/index.js.map +1 -0
  26. package/docs/matter-rvc-updating-homebridge-report.md +120 -0
  27. package/homebridge-roborock-vacuum.code-workspace +7 -0
  28. package/homebridge-ui/public/index.html +257 -0
  29. package/homebridge-ui/public/index.js +1276 -0
  30. package/homebridge-ui/public/styles.css +611 -0
  31. package/homebridge-ui/server.js +3 -0
  32. package/package.json +91 -0
  33. package/roborockLib/i18n/de/translations.json +188 -0
  34. package/roborockLib/i18n/en/translations.json +208 -0
  35. package/roborockLib/i18n/es/translations.json +188 -0
  36. package/roborockLib/i18n/fr/translations.json +188 -0
  37. package/roborockLib/i18n/it/translations.json +188 -0
  38. package/roborockLib/i18n/nl/translations.json +188 -0
  39. package/roborockLib/i18n/pl/translations.json +188 -0
  40. package/roborockLib/i18n/pt/translations.json +188 -0
  41. package/roborockLib/i18n/ru/translations.json +188 -0
  42. package/roborockLib/i18n/uk/translations.json +188 -0
  43. package/roborockLib/i18n/zh-cn/translations.json +188 -0
  44. package/roborockLib/lib/RRMapParser.js +459 -0
  45. package/roborockLib/lib/b01Q7Adapter.js +468 -0
  46. package/roborockLib/lib/deviceFeatures.js +1349 -0
  47. package/roborockLib/lib/localConnector.js +629 -0
  48. package/roborockLib/lib/map/map.html +137 -0
  49. package/roborockLib/lib/map/zones.js +851 -0
  50. package/roborockLib/lib/message.js +402 -0
  51. package/roborockLib/lib/messageQueueHandler.js +411 -0
  52. package/roborockLib/lib/roborockAuth.js +178 -0
  53. package/roborockLib/lib/roborockCrypto.js +55 -0
  54. package/roborockLib/lib/roborockPackageHelper.js +142 -0
  55. package/roborockLib/lib/roborock_mqtt_connector.js +567 -0
  56. package/roborockLib/lib/sniffing/mitmproxy_roborock.py +300 -0
  57. package/roborockLib/lib/vacuum.js +992 -0
  58. package/roborockLib/roborockAPI.js +3635 -0
  59. package/tsconfig.roborockLib.json +17 -0
@@ -0,0 +1,629 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+ const Parser = require("binary-parser").Parser;
5
+ const net = require("net");
6
+ const dgram = require("dgram");
7
+
8
+ const server = dgram.createSocket("udp4");
9
+ const PORT = 58866;
10
+ const TIMEOUT = 5000; // 5 Sekunden Timeout
11
+ const LOCAL_RECONNECT_DELAY_MS = 60000;
12
+ const LOCAL_CONNECT_TIMEOUT_MS = 4000;
13
+
14
+ const BROADCAST_TOKEN = Buffer.from("qWKYcdQWrbm9hPqe", "utf8");
15
+
16
+ // Some adapters provide their own setTimeout/clearTimeout (e.g. to keep timers
17
+ // on the same event loop / for testability). Fall back to the global timer
18
+ // functions when the adapter doesn't provide them.
19
+ function getTimerFns(adapter) {
20
+ return {
21
+ setTimer:
22
+ typeof adapter.setTimeout === "function"
23
+ ? adapter.setTimeout.bind(adapter)
24
+ : setTimeout,
25
+ clearTimer:
26
+ typeof adapter.clearTimeout === "function"
27
+ ? adapter.clearTimeout.bind(adapter)
28
+ : clearTimeout,
29
+ };
30
+ }
31
+
32
+ class EnhancedSocket extends net.Socket {
33
+ constructor(options) {
34
+ super(options);
35
+ this.connected = false;
36
+ this.chunkBuffer = Buffer.alloc(0);
37
+
38
+ this.on("connect", () => {
39
+ this.connected = true;
40
+ });
41
+
42
+ this.on("close", () => {
43
+ this.connected = false;
44
+ });
45
+
46
+ this.on("error", () => {
47
+ this.connected = false;
48
+ });
49
+
50
+ this.on("end", () => {
51
+ this.connected = false;
52
+ });
53
+ }
54
+ }
55
+
56
+ const localMessageParser = new Parser()
57
+ .endianess("big")
58
+ .string("version", {
59
+ length: 3,
60
+ })
61
+ .uint32("seq")
62
+ .uint16("protocol")
63
+ .uint16("payloadLen")
64
+ .buffer("payload", {
65
+ length: "payloadLen",
66
+ })
67
+ .uint32("crc32");
68
+
69
+ const shortMessageParser = new Parser()
70
+ .endianess("big")
71
+ .string("version", {
72
+ length: 3,
73
+ })
74
+ .uint32("seq")
75
+ .uint32("random")
76
+ .uint32("timestamp")
77
+ .uint16("protocol");
78
+
79
+ class localConnector {
80
+ constructor(adapter) {
81
+ this.adapter = adapter;
82
+
83
+ this.localClients = {};
84
+ this.l01HandshakeWaiters = new Map();
85
+ this.reconnectTimers = new Map();
86
+ this.connectPromises = new Map();
87
+ }
88
+
89
+ clearReconnectTimer(duid) {
90
+ const timer = this.reconnectTimers.get(duid);
91
+ if (!timer) {
92
+ return;
93
+ }
94
+
95
+ if (typeof this.adapter.clearTimeout === "function") {
96
+ this.adapter.clearTimeout(timer);
97
+ } else {
98
+ clearTimeout(timer);
99
+ }
100
+ this.reconnectTimers.delete(duid);
101
+ }
102
+
103
+ scheduleReconnect(duid, ip, delayMs = LOCAL_RECONNECT_DELAY_MS) {
104
+ this.clearReconnectTimer(duid);
105
+ const { setTimer } = getTimerFns(this.adapter);
106
+ const timer = setTimer(async () => {
107
+ this.reconnectTimers.delete(duid);
108
+ await this.createClient(duid, ip);
109
+ }, delayMs);
110
+ this.reconnectTimers.set(duid, timer);
111
+ }
112
+
113
+ async ensureConnected(duid, ip) {
114
+ if (!ip) {
115
+ return false;
116
+ }
117
+
118
+ if (this.isConnected(duid)) {
119
+ return true;
120
+ }
121
+
122
+ const pending = this.connectPromises.get(duid);
123
+ if (pending) {
124
+ await pending;
125
+ return Boolean(this.isConnected(duid));
126
+ }
127
+
128
+ const connectPromise = this.createClient(duid, ip)
129
+ .catch((error) => {
130
+ this.adapter.log.debug(
131
+ `Immediate local reconnect failed for ${duid}: ${error.message || error}`
132
+ );
133
+ })
134
+ .finally(() => {
135
+ this.connectPromises.delete(duid);
136
+ });
137
+
138
+ this.connectPromises.set(duid, connectPromise);
139
+ await connectPromise;
140
+ return Boolean(this.isConnected(duid));
141
+ }
142
+
143
+ async resetClient(duid, reason = "local-client-reset") {
144
+ this.clearReconnectTimer(duid);
145
+ const client = this.localClients[duid];
146
+ if (!client) {
147
+ return;
148
+ }
149
+
150
+ if (this.localClients[duid] === client) {
151
+ delete this.localClients[duid];
152
+ }
153
+
154
+ const waiter = this.l01HandshakeWaiters.get(duid);
155
+ if (waiter) {
156
+ this.adapter.clearTimeout(waiter.timeout);
157
+ this.l01HandshakeWaiters.delete(duid);
158
+ waiter.reject(
159
+ new Error(`TCP client reset during L01 handshake for ${duid}`)
160
+ );
161
+ }
162
+
163
+ this.adapter.localL01Nonces.delete(duid);
164
+ client.destroy();
165
+ await this.adapter.updateTransportDiagnostics(duid, {
166
+ tcpConnectionState: "disconnected",
167
+ lastTransport: "cloud",
168
+ lastTransportReason: reason,
169
+ });
170
+ }
171
+
172
+ async markLocalConnected(duid) {
173
+ if (this.adapter.remoteDevices?.delete(duid)) {
174
+ this.adapter.log.debug(
175
+ `Local TCP connected for ${duid}; clearing remote fallback marker.`
176
+ );
177
+ }
178
+
179
+ await this.adapter.updateTransportDiagnostics(duid, {
180
+ tcpConnectionState: "connected",
181
+ isRemote: false,
182
+ remoteReason: null,
183
+ lastTransport: "local",
184
+ lastTransportReason: "tcp-connected",
185
+ });
186
+ }
187
+
188
+ async createClient(duid, ip) {
189
+ this.clearReconnectTimer(duid);
190
+ const existingClient = this.localClients[duid];
191
+ if (existingClient?.connected || existingClient?.connecting) {
192
+ return;
193
+ }
194
+
195
+ const client = new EnhancedSocket();
196
+ await this.adapter.updateTransportDiagnostics(duid, {
197
+ localIp: ip,
198
+ tcpConnectionState: "connecting",
199
+ lastTransport: "local-pending",
200
+ });
201
+
202
+ // Wrap the connect method in a promise to await its completion
203
+ await new Promise((resolve, reject) => {
204
+ let settled = false;
205
+ const { setTimer, clearTimer } = getTimerFns(this.adapter);
206
+ const timeout = setTimer(() => {
207
+ if (settled) {
208
+ return;
209
+ }
210
+
211
+ settled = true;
212
+ client.destroy();
213
+ reject(
214
+ new Error(
215
+ `Timed out connecting local TCP client for ${duid} at ${ip}`
216
+ )
217
+ );
218
+ }, LOCAL_CONNECT_TIMEOUT_MS);
219
+ const finish = (callback, value) => {
220
+ if (settled) {
221
+ return;
222
+ }
223
+
224
+ settled = true;
225
+ clearTimer(timeout);
226
+ callback(value);
227
+ };
228
+
229
+ client
230
+ .connect(58867, ip, async () => {
231
+ this.adapter.log.debug(`tcp client for ${duid} connected`);
232
+ await this.markLocalConnected(duid);
233
+ this.ensureL01Handshake(duid).catch((error) => {
234
+ this.adapter.log.debug(
235
+ `L01 handshake on connect failed for ${duid}: ${error.message}`
236
+ );
237
+ });
238
+ finish(resolve);
239
+ })
240
+ .on("error", (error) => {
241
+ this.adapter.log.debug(
242
+ `error on tcp client for ${duid}. ${error.message}`
243
+ );
244
+ finish(reject, error);
245
+ });
246
+ }).catch(async (error) => {
247
+ const online = await this.adapter.onlineChecker(duid);
248
+ await this.adapter.updateTransportDiagnostics(duid, {
249
+ tcpConnectionState: "connect-failed",
250
+ lastTransport: "cloud",
251
+ lastTransportReason: online
252
+ ? "tcp-connect-failed"
253
+ : "device-offline-during-connect",
254
+ });
255
+ if (online) {
256
+ // if the device is online, we can assume that the device is a remote device
257
+ this.adapter.log.debug(
258
+ `error on tcp client for ${duid}. Marking this device as remote device. Connecting via MQTT instead ${error.message}`
259
+ );
260
+ this.adapter.remoteDevices.add(duid);
261
+ await this.adapter.updateTransportDiagnostics(duid, {
262
+ isRemote: true,
263
+ remoteReason: "marked-remote-after-connect-failure",
264
+ });
265
+ // this.adapter.catchError(`Failed to create tcp client: ${error.stack}`, `function createClient`, duid);
266
+ }
267
+ });
268
+
269
+ client.on("data", async (message) => {
270
+ try {
271
+ if (client.chunkBuffer.length == 0) {
272
+ this.adapter.log.debug(`new chunk started`);
273
+ client.chunkBuffer = message;
274
+ } else {
275
+ this.adapter.log.debug(`new chunk received`);
276
+ client.chunkBuffer = Buffer.concat([client.chunkBuffer, message]);
277
+ }
278
+ // this.adapter.log.debug(`new chunk received: ${message.toString("hex")}`);
279
+
280
+ let offset = 0;
281
+ if (this.checkComplete(client.chunkBuffer)) {
282
+ this.adapter.log.debug(
283
+ `Chunk buffer data is complete. Processing...`
284
+ );
285
+ // this.adapter.log.debug(`chunkBuffer: ${client.chunkBuffer.toString("hex")}`);
286
+ while (offset + 4 <= client.chunkBuffer.length) {
287
+ const segmentLength = client.chunkBuffer.readUInt32BE(offset);
288
+ const currentBuffer = client.chunkBuffer.subarray(
289
+ offset + 4,
290
+ offset + segmentLength + 4
291
+ );
292
+ // length of 17 does not contain any useful data.
293
+ // It seems to be protocol handshake metadata.
294
+ if (segmentLength != 17) {
295
+ const data = this.adapter.message._decodeMsg(currentBuffer, duid);
296
+ if (!data) {
297
+ offset += 4 + segmentLength;
298
+ continue;
299
+ }
300
+
301
+ if (data.protocol == 4) {
302
+ const dps = JSON.parse(data.payload).dps;
303
+
304
+ if (dps) {
305
+ const _102 = JSON.stringify(dps["102"]);
306
+ const parsed_102 = JSON.parse(JSON.parse(_102));
307
+ const id = parsed_102.id;
308
+ const result = parsed_102.result;
309
+
310
+ if (this.adapter.pendingRequests.has(id)) {
311
+ this.adapter.log.debug(
312
+ `Local message with protocol 4 and id ${id} received. Result: ${JSON.stringify(result)}`
313
+ );
314
+ const { resolve, timeout } =
315
+ this.adapter.pendingRequests.get(id);
316
+ this.adapter.clearTimeout(timeout);
317
+ this.adapter.pendingRequests.delete(id);
318
+ resolve(result);
319
+
320
+ if (this.adapter.deviceNotify !== undefined) {
321
+ this.adapter.deviceNotify("LocalMessage", {
322
+ duid,
323
+ payload: result,
324
+ });
325
+ }
326
+ }
327
+ }
328
+ }
329
+ } else {
330
+ try {
331
+ const shortMessage = shortMessageParser.parse(currentBuffer);
332
+ if (
333
+ shortMessage.version == "L01" &&
334
+ shortMessage.protocol == 1
335
+ ) {
336
+ const currentNonces =
337
+ this.adapter.localL01Nonces.get(duid) || {};
338
+ this.adapter.localL01Nonces.set(duid, {
339
+ connectNonce: currentNonces.connectNonce,
340
+ ackNonce: shortMessage.random,
341
+ });
342
+
343
+ const waiter = this.l01HandshakeWaiters.get(duid);
344
+ if (waiter) {
345
+ this.adapter.clearTimeout(waiter.timeout);
346
+ this.l01HandshakeWaiters.delete(duid);
347
+ waiter.resolve(true);
348
+ }
349
+ }
350
+ } catch (error) {
351
+ this.adapter.log.debug(
352
+ `Failed parsing short local message for ${duid}: ${error.message}`
353
+ );
354
+ }
355
+ }
356
+ offset += 4 + segmentLength;
357
+ }
358
+ this.clearChunkBuffer(duid);
359
+ }
360
+ } catch (error) {
361
+ this.adapter.catchError(
362
+ `Failed to create tcp client: ${error.stack}`,
363
+ `function createClient`,
364
+ duid
365
+ );
366
+ }
367
+ });
368
+
369
+ client.on("close", () => {
370
+ if (this.localClients[duid] !== client) {
371
+ return;
372
+ }
373
+ this.adapter.log.debug(
374
+ `tcp client for ${duid} disconnected, attempting to reconnect...`
375
+ );
376
+ this.adapter.updateTransportDiagnostics(duid, {
377
+ tcpConnectionState: "disconnected",
378
+ lastTransport: "cloud",
379
+ lastTransportReason: "tcp-disconnected",
380
+ });
381
+ const waiter = this.l01HandshakeWaiters.get(duid);
382
+ if (waiter) {
383
+ this.adapter.clearTimeout(waiter.timeout);
384
+ this.l01HandshakeWaiters.delete(duid);
385
+ waiter.reject(
386
+ new Error(`TCP client closed during L01 handshake for ${duid}`)
387
+ );
388
+ }
389
+ this.adapter.localL01Nonces.delete(duid);
390
+ this.scheduleReconnect(duid, ip);
391
+ client.connected = false;
392
+ });
393
+
394
+ client.on("error", (error) => {
395
+ this.adapter.log.debug(
396
+ `error on tcp client for ${duid}. ${error.message}`
397
+ );
398
+ this.adapter.updateTransportDiagnostics(duid, {
399
+ tcpConnectionState: "error",
400
+ lastTransportReason: `tcp-error: ${error.message}`,
401
+ });
402
+ });
403
+
404
+ this.localClients[duid] = client;
405
+ }
406
+
407
+ checkComplete(buffer) {
408
+ let totalLength = 0;
409
+ let offset = 0;
410
+
411
+ while (offset + 4 <= buffer.length) {
412
+ const segmentLength = buffer.readUInt32BE(offset);
413
+ totalLength += 4 + segmentLength;
414
+ offset += 4 + segmentLength;
415
+
416
+ if (offset > buffer.length) {
417
+ return false; // Data is not complete yet
418
+ }
419
+ }
420
+
421
+ return totalLength <= buffer.length;
422
+ }
423
+
424
+ clearChunkBuffer(duid) {
425
+ if (this.localClients[duid]) {
426
+ this.localClients[duid].chunkBuffer = Buffer.alloc(0);
427
+ }
428
+ }
429
+
430
+ sendMessage(duid, message) {
431
+ const client = this.localClients[duid];
432
+ if (client) {
433
+ client.write(message);
434
+ }
435
+ }
436
+
437
+ isConnected(duid) {
438
+ if (this.localClients[duid]) {
439
+ return this.localClients[duid].connected;
440
+ }
441
+ }
442
+
443
+ async ensureL01Handshake(duid) {
444
+ const version = await this.adapter.getRobotVersion(duid);
445
+ if (version != "L01") {
446
+ return;
447
+ }
448
+
449
+ const client = this.localClients[duid];
450
+ if (!client || !client.connected) {
451
+ return;
452
+ }
453
+
454
+ const existingNonces = this.adapter.localL01Nonces.get(duid);
455
+ if (
456
+ existingNonces &&
457
+ typeof existingNonces.connectNonce == "number" &&
458
+ typeof existingNonces.ackNonce == "number"
459
+ ) {
460
+ return;
461
+ }
462
+
463
+ const timestamp = Math.floor(Date.now() / 1000);
464
+ const handshakeMessage = await this.adapter.message.buildRoborockMessage(
465
+ duid,
466
+ 1,
467
+ timestamp,
468
+ Buffer.alloc(0)
469
+ );
470
+ if (!handshakeMessage) {
471
+ throw new Error(
472
+ `Failed to build protocol 1 handshake message for ${duid}`
473
+ );
474
+ }
475
+
476
+ const connectNonce = handshakeMessage.readUInt32BE(7);
477
+ this.adapter.localL01Nonces.set(duid, {
478
+ connectNonce,
479
+ ackNonce: undefined,
480
+ });
481
+
482
+ if (this.l01HandshakeWaiters.has(duid)) {
483
+ const waiter = this.l01HandshakeWaiters.get(duid);
484
+ this.adapter.clearTimeout(waiter.timeout);
485
+ this.l01HandshakeWaiters.delete(duid);
486
+ }
487
+
488
+ const handshakePromise = new Promise((resolve, reject) => {
489
+ const timeout = this.adapter.setTimeout(() => {
490
+ this.l01HandshakeWaiters.delete(duid);
491
+ reject(
492
+ new Error(`Timed out waiting for L01 handshake response for ${duid}`)
493
+ );
494
+ }, 3000);
495
+
496
+ this.l01HandshakeWaiters.set(duid, { resolve, reject, timeout });
497
+ });
498
+
499
+ const lengthBuffer = Buffer.alloc(4);
500
+ lengthBuffer.writeUInt32BE(handshakeMessage.length, 0);
501
+ const fullMessage = Buffer.concat([lengthBuffer, handshakeMessage]);
502
+ client.write(fullMessage);
503
+
504
+ await handshakePromise;
505
+ }
506
+
507
+ async getLocalDevices() {
508
+ return new Promise((resolve, reject) => {
509
+ const devices = {};
510
+
511
+ server.on("message", (msg) => {
512
+ const parsedMessage = localMessageParser.parse(msg);
513
+ const decodedMessage = this.decryptECB(
514
+ parsedMessage.payload,
515
+ BROADCAST_TOKEN
516
+ ); // this might be decryptCBC for A01. Haven't checked this yet
517
+
518
+ if (decodedMessage == null) {
519
+ this.adapter.log.debug(`getLocalDevices: decodedMessage is null`);
520
+ return;
521
+ }
522
+
523
+ const parsedDecodedMessage = JSON.parse(decodedMessage);
524
+ this.adapter.log.debug(
525
+ `getLocalDevices parsedDecodedMessage: ${JSON.stringify(parsedDecodedMessage)}`
526
+ );
527
+
528
+ if (parsedDecodedMessage) {
529
+ const localKey = this.adapter.localKeys.get(
530
+ parsedDecodedMessage.duid
531
+ );
532
+ this.adapter.log.debug(
533
+ `getLocalDevices localKey present: ${Boolean(localKey)}`
534
+ );
535
+
536
+ if (localKey) {
537
+ // if there's no localKey, decryption cannot work. For example when the found robot is not associated with a roborock account
538
+ if (!devices[parsedDecodedMessage.duid]) {
539
+ devices[parsedDecodedMessage.duid] = parsedDecodedMessage.ip;
540
+ this.adapter.updateTransportDiagnostics(
541
+ parsedDecodedMessage.duid,
542
+ {
543
+ localIp: parsedDecodedMessage.ip,
544
+ localDiscoveryState: "broadcast-detected",
545
+ lastTransportReason: "udp-broadcast-discovery",
546
+ }
547
+ );
548
+ }
549
+ }
550
+ }
551
+ });
552
+
553
+ server.on("error", (error) => {
554
+ this.adapter.catchError(`Discover server error: ${error.stack}`);
555
+ server.close();
556
+ reject(error);
557
+ });
558
+
559
+ server.bind(PORT);
560
+
561
+ this.localDevicesTimeout = this.adapter.setTimeout(() => {
562
+ server.close();
563
+
564
+ resolve(devices);
565
+ }, TIMEOUT);
566
+ });
567
+ }
568
+
569
+ safeRemovePkcs7(buf) {
570
+ if (!buf || buf.length === 0) return Buffer.alloc(0);
571
+ const pad = buf[buf.length - 1];
572
+ // 僅在 1..16 且最後 pad 個 byte 都等於 pad 時才移除
573
+ if (pad > 0 && pad <= 16) {
574
+ for (let i = 0; i < pad; i++) {
575
+ if (buf[buf.length - 1 - i] !== pad) return buf; // padding 形狀不對,視為無 padding
576
+ }
577
+ return buf.slice(0, buf.length - pad);
578
+ }
579
+ return buf; // 看起來沒有標準 PKCS#7 padding
580
+ }
581
+
582
+ decryptECB(encrypted, aesKey) {
583
+ // --- 1) Key/輸入檢查 ---
584
+ const key = Buffer.isBuffer(aesKey) ? aesKey : Buffer.from(aesKey);
585
+ if (key.length !== 16) {
586
+ // AES-128 需要 16 bytes 的 key
587
+ return null;
588
+ }
589
+
590
+ const input = Buffer.isBuffer(encrypted)
591
+ ? encrypted
592
+ : Buffer.from(encrypted, "latin1"); // "binary" 等同 latin1
593
+ if (input.length === 0 || input.length % 16 !== 0) {
594
+ // 密文長度不是 16 的倍數,多半是封包不完整;丟回 null 讓上層忽略本次
595
+ return null;
596
+ }
597
+
598
+ try {
599
+ // --- 2) 固定用 Buffer,關閉自動 padding(你要自己移除) ---
600
+ const decipher = crypto.createDecipheriv("aes-128-ecb", key, null);
601
+ decipher.setAutoPadding(false);
602
+
603
+ const decryptedBuf = Buffer.concat([
604
+ decipher.update(input),
605
+ decipher.final(),
606
+ ]);
607
+ const unpadded = safeRemovePkcs7(decryptedBuf);
608
+
609
+ // 若原協定內容是 UTF-8,這裡再轉字串;否則直接回傳 Buffer 讓上層處理
610
+ return unpadded.toString("utf8");
611
+ } catch (err) {
612
+ // 例如 wrong final block length、key 不對等情況
613
+ // 這裡不要讓程式炸掉,直接忽略這個封包
614
+ // 你也可以在這裡做一次 debug log
615
+ // console.debug("decryptECB error:", err);
616
+ return null;
617
+ }
618
+ }
619
+
620
+ clearLocalDevicedTimeout() {
621
+ if (this.localDevicesTimeout) {
622
+ this.adapter.clearTimeout(this.localDevicesTimeout);
623
+ }
624
+ }
625
+ }
626
+
627
+ module.exports = {
628
+ localConnector,
629
+ };