homebridge-cync-app 0.1.3 → 0.1.6
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/CHANGELOG.md +44 -10
- package/README.md +30 -22
- package/config.schema.json +2 -7
- package/dist/cync/cync-client.d.ts +8 -7
- package/dist/cync/cync-client.js +44 -19
- package/dist/cync/cync-client.js.map +1 -1
- package/dist/cync/tcp-client.d.ts +9 -18
- package/dist/cync/tcp-client.js +172 -94
- package/dist/cync/tcp-client.js.map +1 -1
- package/dist/platform.d.ts +3 -16
- package/dist/platform.js +349 -43
- package/dist/platform.js.map +1 -1
- package/dist/platformAccessory.js +1 -0
- package/dist/platformAccessory.js.map +1 -1
- package/homebridge-ui/public/icon.png +0 -0
- package/homebridge-ui/public/index.html +171 -110
- package/homebridge-ui/server.js +38 -73
- package/package.json +6 -9
- package/src/cync/cync-client.ts +64 -26
- package/src/cync/tcp-client.ts +281 -123
- package/src/platform.ts +575 -53
- package/src/platformAccessory.ts +2 -0
- package/nodemon.json +0 -12
- package/src/@types/homebridge-lib.d.ts +0 -14
package/src/cync/tcp-client.ts
CHANGED
|
@@ -14,13 +14,6 @@ const defaultLogger: CyncLogger = {
|
|
|
14
14
|
export type DeviceUpdateCallback = (payload: unknown) => void;
|
|
15
15
|
export type RawFrameListener = (frame: Buffer) => void;
|
|
16
16
|
|
|
17
|
-
interface QueuedPowerPacket {
|
|
18
|
-
deviceId: string;
|
|
19
|
-
on: boolean;
|
|
20
|
-
seq: number;
|
|
21
|
-
packet: Buffer;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
17
|
export class TcpClient {
|
|
25
18
|
public registerSwitchMapping(controllerId: number, deviceId: string): void {
|
|
26
19
|
if (!Number.isFinite(controllerId)) {
|
|
@@ -28,6 +21,7 @@ export class TcpClient {
|
|
|
28
21
|
}
|
|
29
22
|
this.controllerToDevice.set(controllerId, deviceId);
|
|
30
23
|
}
|
|
24
|
+
private commandChain: Promise<void> = Promise.resolve();
|
|
31
25
|
private homeDevices: Record<string, string[]> = {};
|
|
32
26
|
private switchIdToHomeId = new Map<number, string>();
|
|
33
27
|
private readonly log: CyncLogger;
|
|
@@ -44,9 +38,33 @@ export class TcpClient {
|
|
|
44
38
|
private heartbeatTimer: NodeJS.Timeout | null = null;
|
|
45
39
|
private rawFrameListeners: RawFrameListener[] = [];
|
|
46
40
|
private controllerToDevice = new Map<number, string>();
|
|
47
|
-
|
|
48
|
-
private
|
|
49
|
-
|
|
41
|
+
|
|
42
|
+
private enqueueCommand<T>(fn: () => Promise<T>): Promise<T> {
|
|
43
|
+
let resolveWrapper: (value: T | PromiseLike<T>) => void;
|
|
44
|
+
let rejectWrapper: (reason?: unknown) => void;
|
|
45
|
+
|
|
46
|
+
const p = new Promise<T>((resolve, reject) => {
|
|
47
|
+
resolveWrapper = resolve;
|
|
48
|
+
rejectWrapper = reject;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Chain onto the existing promise
|
|
52
|
+
this.commandChain = this.commandChain
|
|
53
|
+
.then(async () => {
|
|
54
|
+
try {
|
|
55
|
+
const result = await fn();
|
|
56
|
+
resolveWrapper(result);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
rejectWrapper(err);
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
.catch(() => {
|
|
62
|
+
// Swallow errors in the chain so a failed command
|
|
63
|
+
// doesn't permanently block the queue.
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return p;
|
|
67
|
+
}
|
|
50
68
|
private parseSwitchStateFrame(frame: Buffer): { controllerId: number; on: boolean; level: number } | null {
|
|
51
69
|
if (frame.length < 16) {
|
|
52
70
|
return null;
|
|
@@ -156,7 +174,6 @@ export class TcpClient {
|
|
|
156
174
|
await this.ensureConnected();
|
|
157
175
|
}
|
|
158
176
|
|
|
159
|
-
|
|
160
177
|
public applyLanTopology(topology: {
|
|
161
178
|
homeDevices: Record<string, string[]>;
|
|
162
179
|
switchIdToHomeId: Record<number, string>;
|
|
@@ -178,11 +195,6 @@ export class TcpClient {
|
|
|
178
195
|
);
|
|
179
196
|
}
|
|
180
197
|
|
|
181
|
-
/**
|
|
182
|
-
* Ensure we have an open, logged-in socket.
|
|
183
|
-
* If multiple callers invoke this concurrently, they will share a single
|
|
184
|
-
* connection attempt via `this.connecting`.
|
|
185
|
-
*/
|
|
186
198
|
private async ensureConnected(): Promise<boolean> {
|
|
187
199
|
if (this.socket && !this.socket.destroyed) {
|
|
188
200
|
return true;
|
|
@@ -195,34 +207,10 @@ export class TcpClient {
|
|
|
195
207
|
return false;
|
|
196
208
|
}
|
|
197
209
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
await this.establishSocket();
|
|
201
|
-
const ok = !!(this.socket && !this.socket.destroyed);
|
|
202
|
-
if (!ok) {
|
|
203
|
-
this.log.warn(
|
|
204
|
-
'[Cync TCP] establishSocket() completed but socket is not usable.',
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
this.connecting = null;
|
|
208
|
-
return ok;
|
|
209
|
-
})().catch((err) => {
|
|
210
|
-
this.log.error(
|
|
211
|
-
'[Cync TCP] ensureConnected() connect attempt failed: %s',
|
|
212
|
-
String(err),
|
|
213
|
-
);
|
|
214
|
-
this.connecting = null;
|
|
215
|
-
return false;
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return this.connecting;
|
|
210
|
+
await this.establishSocket();
|
|
211
|
+
return !!(this.socket && !this.socket.destroyed);
|
|
220
212
|
}
|
|
221
213
|
|
|
222
|
-
/**
|
|
223
|
-
* Open a new socket to cm.gelighting.com and send the loginCode,
|
|
224
|
-
* mirroring the HA integration’s behavior.
|
|
225
|
-
*/
|
|
226
214
|
private async establishSocket(): Promise<void> {
|
|
227
215
|
const host = 'cm.gelighting.com';
|
|
228
216
|
const portTLS = 23779;
|
|
@@ -325,45 +313,6 @@ export class TcpClient {
|
|
|
325
313
|
return this.seq;
|
|
326
314
|
}
|
|
327
315
|
|
|
328
|
-
private async flushQueue(): Promise<void> {
|
|
329
|
-
if (this.sending) {
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
this.sending = true;
|
|
333
|
-
|
|
334
|
-
try {
|
|
335
|
-
while (this.sendQueue.length > 0) {
|
|
336
|
-
const item = this.sendQueue.shift();
|
|
337
|
-
if (!item) {
|
|
338
|
-
break;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const connected = await this.ensureConnected();
|
|
342
|
-
if (!connected || !this.socket || this.socket.destroyed) {
|
|
343
|
-
this.log.warn(
|
|
344
|
-
'[Cync TCP] Socket not available while flushing queue; dropping remaining %d packets.',
|
|
345
|
-
this.sendQueue.length + 1,
|
|
346
|
-
);
|
|
347
|
-
this.sendQueue.length = 0;
|
|
348
|
-
break;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
this.socket.write(item.packet);
|
|
352
|
-
this.log.info(
|
|
353
|
-
'[Cync TCP] Sent power packet: device=%s on=%s seq=%d',
|
|
354
|
-
item.deviceId,
|
|
355
|
-
String(item.on),
|
|
356
|
-
item.seq,
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
// Small delay so we don't hammer the bridge/device with a burst
|
|
360
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
361
|
-
}
|
|
362
|
-
} finally {
|
|
363
|
-
this.sending = false;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
316
|
private buildPowerPacket(
|
|
368
317
|
controllerId: number,
|
|
369
318
|
meshId: number,
|
|
@@ -404,6 +353,69 @@ export class TcpClient {
|
|
|
404
353
|
]);
|
|
405
354
|
}
|
|
406
355
|
|
|
356
|
+
private buildComboPacket(
|
|
357
|
+
controllerId: number,
|
|
358
|
+
meshId: number,
|
|
359
|
+
on: boolean,
|
|
360
|
+
brightnessPct: number,
|
|
361
|
+
colorTone: number,
|
|
362
|
+
rgb: { r: number; g: number; b: number },
|
|
363
|
+
seq: number,
|
|
364
|
+
): Buffer {
|
|
365
|
+
const header = Buffer.from('7300000022', 'hex');
|
|
366
|
+
|
|
367
|
+
const switchBytes = Buffer.alloc(4);
|
|
368
|
+
switchBytes.writeUInt32BE(controllerId, 0);
|
|
369
|
+
|
|
370
|
+
const seqBytes = Buffer.alloc(2);
|
|
371
|
+
seqBytes.writeUInt16BE(seq, 0);
|
|
372
|
+
|
|
373
|
+
const middle = Buffer.from('007e00000000f8f010000000000000', 'hex');
|
|
374
|
+
|
|
375
|
+
const meshBytes = Buffer.alloc(2);
|
|
376
|
+
meshBytes.writeUInt16LE(meshId, 0);
|
|
377
|
+
|
|
378
|
+
const tailPrefix = Buffer.from('f00000', 'hex');
|
|
379
|
+
|
|
380
|
+
const onByte = on ? 1 : 0;
|
|
381
|
+
const brightnessByte = Math.max(0, Math.min(100, Math.round(brightnessPct)));
|
|
382
|
+
const colorToneByte = Math.max(0, Math.min(255, Math.round(colorTone)));
|
|
383
|
+
|
|
384
|
+
const r = Math.max(0, Math.min(255, Math.round(rgb.r)));
|
|
385
|
+
const g = Math.max(0, Math.min(255, Math.round(rgb.g)));
|
|
386
|
+
const b = Math.max(0, Math.min(255, Math.round(rgb.b)));
|
|
387
|
+
|
|
388
|
+
const rgbBytes = Buffer.from([r, g, b]);
|
|
389
|
+
|
|
390
|
+
const checksumSeed =
|
|
391
|
+
496 +
|
|
392
|
+
meshBytes[0] +
|
|
393
|
+
meshBytes[1] +
|
|
394
|
+
onByte +
|
|
395
|
+
brightnessByte +
|
|
396
|
+
colorToneByte +
|
|
397
|
+
r +
|
|
398
|
+
g +
|
|
399
|
+
b;
|
|
400
|
+
|
|
401
|
+
const checksum = Buffer.from([checksumSeed & 0xff]);
|
|
402
|
+
const end = Buffer.from('7e', 'hex');
|
|
403
|
+
|
|
404
|
+
return Buffer.concat([
|
|
405
|
+
header,
|
|
406
|
+
switchBytes,
|
|
407
|
+
seqBytes,
|
|
408
|
+
middle,
|
|
409
|
+
meshBytes,
|
|
410
|
+
tailPrefix,
|
|
411
|
+
Buffer.from([onByte]),
|
|
412
|
+
Buffer.from([brightnessByte]),
|
|
413
|
+
Buffer.from([colorToneByte]),
|
|
414
|
+
rgbBytes,
|
|
415
|
+
checksum,
|
|
416
|
+
end,
|
|
417
|
+
]);
|
|
418
|
+
}
|
|
407
419
|
public async disconnect(): Promise<void> {
|
|
408
420
|
this.log.info('[Cync TCP] disconnect() called.');
|
|
409
421
|
if (this.heartbeatTimer) {
|
|
@@ -414,9 +426,6 @@ export class TcpClient {
|
|
|
414
426
|
this.socket.destroy();
|
|
415
427
|
this.socket = null;
|
|
416
428
|
}
|
|
417
|
-
this.sendQueue.length = 0;
|
|
418
|
-
this.sending = false;
|
|
419
|
-
this.connecting = null;
|
|
420
429
|
}
|
|
421
430
|
|
|
422
431
|
public onDeviceUpdate(cb: DeviceUpdateCallback): void {
|
|
@@ -440,53 +449,203 @@ export class TcpClient {
|
|
|
440
449
|
this.rawFrameListeners.push(listener);
|
|
441
450
|
}
|
|
442
451
|
|
|
443
|
-
/**
|
|
444
|
-
* High-level API to change switch state.
|
|
445
|
-
* Now enqueues a power packet so multiple HomeKit scene writes are
|
|
446
|
-
* serialized over a single TCP session.
|
|
447
|
-
*/
|
|
448
452
|
public async setSwitchState(
|
|
449
453
|
deviceId: string,
|
|
450
454
|
params: { on: boolean },
|
|
451
455
|
): Promise<void> {
|
|
452
|
-
|
|
453
|
-
this.
|
|
454
|
-
|
|
455
|
-
|
|
456
|
+
return this.enqueueCommand(async () => {
|
|
457
|
+
if (!this.config) {
|
|
458
|
+
this.log.warn('[Cync TCP] No config available.');
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
456
461
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
+
const connected = await this.ensureConnected();
|
|
463
|
+
if (!connected || !this.socket || this.socket.destroyed) {
|
|
464
|
+
this.log.warn(
|
|
465
|
+
'[Cync TCP] Cannot send, socket not ready even after reconnect attempt.',
|
|
466
|
+
);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
462
469
|
|
|
463
|
-
|
|
464
|
-
|
|
470
|
+
const device = this.findDevice(deviceId);
|
|
471
|
+
if (!device) {
|
|
472
|
+
this.log.warn('[Cync TCP] Unknown deviceId=%s', deviceId);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
465
475
|
|
|
466
|
-
|
|
467
|
-
this.log.
|
|
468
|
-
'[Cync TCP]
|
|
476
|
+
const record = device as Record<string, unknown>;
|
|
477
|
+
this.log.debug(
|
|
478
|
+
'[Cync TCP] setSwitchState: deviceId=%s device_type=%o switch_controller=%o mesh_id=%o home_id=%o',
|
|
469
479
|
deviceId,
|
|
470
|
-
|
|
471
|
-
|
|
480
|
+
record.device_type,
|
|
481
|
+
record.switch_controller,
|
|
482
|
+
record.mesh_id,
|
|
483
|
+
record.home_id,
|
|
472
484
|
);
|
|
473
|
-
|
|
474
|
-
|
|
485
|
+
const controllerId = Number(record.switch_controller);
|
|
486
|
+
const meshIndex = Number(record.mesh_id);
|
|
487
|
+
|
|
488
|
+
if (!Number.isFinite(controllerId) || !Number.isFinite(meshIndex)) {
|
|
489
|
+
this.log.warn(
|
|
490
|
+
'[Cync TCP] Device %s is missing LAN fields (switch_controller=%o mesh_id=%o)',
|
|
491
|
+
deviceId,
|
|
492
|
+
record.switch_controller,
|
|
493
|
+
record.mesh_id,
|
|
494
|
+
);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
475
497
|
|
|
476
|
-
|
|
477
|
-
|
|
498
|
+
const seq = this.nextSeq();
|
|
499
|
+
const packet = this.buildPowerPacket(controllerId, meshIndex, params.on, seq);
|
|
478
500
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
501
|
+
// At this point socket has been validated above
|
|
502
|
+
this.socket.write(packet);
|
|
503
|
+
this.log.info(
|
|
504
|
+
'[Cync TCP] Sent power packet: device=%s on=%s seq=%d',
|
|
505
|
+
deviceId,
|
|
506
|
+
String(params.on),
|
|
507
|
+
seq,
|
|
508
|
+
);
|
|
484
509
|
});
|
|
510
|
+
}
|
|
485
511
|
|
|
486
|
-
|
|
487
|
-
|
|
512
|
+
public async setBrightness(deviceId: string, brightnessPct: number): Promise<void> {
|
|
513
|
+
return this.enqueueCommand(async () => {
|
|
514
|
+
if (!this.config) {
|
|
515
|
+
this.log.warn('[Cync TCP] setBrightness: no config available.');
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const connected = await this.ensureConnected();
|
|
520
|
+
if (!connected || !this.socket || this.socket.destroyed) {
|
|
521
|
+
this.log.warn(
|
|
522
|
+
'[Cync TCP] setBrightness: socket not ready even after reconnect attempt.',
|
|
523
|
+
);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const device = this.findDevice(deviceId);
|
|
528
|
+
if (!device) {
|
|
529
|
+
this.log.warn('[Cync TCP] setBrightness: unknown deviceId=%s', deviceId);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const record = device as Record<string, unknown>;
|
|
534
|
+
const controllerId = Number(record.switch_controller);
|
|
535
|
+
const meshIndex = Number(record.mesh_id);
|
|
536
|
+
|
|
537
|
+
if (!Number.isFinite(controllerId) || !Number.isFinite(meshIndex)) {
|
|
538
|
+
this.log.warn(
|
|
539
|
+
'[Cync TCP] setBrightness: device %s missing LAN fields (switch_controller=%o mesh_id=%o)',
|
|
540
|
+
deviceId,
|
|
541
|
+
record.switch_controller,
|
|
542
|
+
record.mesh_id,
|
|
543
|
+
);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const clamped = Math.max(0, Math.min(100, Number(brightnessPct)));
|
|
548
|
+
const on = clamped > 0;
|
|
549
|
+
|
|
550
|
+
const seq = this.nextSeq();
|
|
551
|
+
|
|
552
|
+
// White-only combo control for now; color support can layer on this later.
|
|
553
|
+
const packet = this.buildComboPacket(
|
|
554
|
+
controllerId,
|
|
555
|
+
meshIndex,
|
|
556
|
+
on,
|
|
557
|
+
clamped,
|
|
558
|
+
255, // color_tone=255 ("white" in HA integration)
|
|
559
|
+
{ r: 255, g: 255, b: 255 },
|
|
560
|
+
seq,
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
this.socket.write(packet);
|
|
564
|
+
this.log.info(
|
|
565
|
+
'[Cync TCP] Sent combo (brightness) packet: device=%s on=%s brightness=%d seq=%d',
|
|
566
|
+
deviceId,
|
|
567
|
+
String(on),
|
|
568
|
+
clamped,
|
|
569
|
+
seq,
|
|
570
|
+
);
|
|
571
|
+
});
|
|
488
572
|
}
|
|
489
573
|
|
|
574
|
+
public async setColor(
|
|
575
|
+
deviceId: string,
|
|
576
|
+
rgb: { r: number; g: number; b: number },
|
|
577
|
+
brightnessPct?: number,
|
|
578
|
+
): Promise<void> {
|
|
579
|
+
return this.enqueueCommand(async () => {
|
|
580
|
+
if (!this.config) {
|
|
581
|
+
this.log.warn('[Cync TCP] setColor: no config available.');
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const connected = await this.ensureConnected();
|
|
586
|
+
if (!connected || !this.socket || this.socket.destroyed) {
|
|
587
|
+
this.log.warn(
|
|
588
|
+
'[Cync TCP] setColor: socket not ready even after reconnect attempt.',
|
|
589
|
+
);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const device = this.findDevice(deviceId);
|
|
594
|
+
if (!device) {
|
|
595
|
+
this.log.warn('[Cync TCP] setColor: unknown deviceId=%s', deviceId);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const record = device as Record<string, unknown>;
|
|
600
|
+
const controllerId = Number(record.switch_controller);
|
|
601
|
+
const meshIndex = Number(record.mesh_id);
|
|
602
|
+
|
|
603
|
+
if (!Number.isFinite(controllerId) || !Number.isFinite(meshIndex)) {
|
|
604
|
+
this.log.warn(
|
|
605
|
+
'[Cync TCP] setColor: device %s missing LAN fields (switch_controller=%o mesh_id=%o)',
|
|
606
|
+
deviceId,
|
|
607
|
+
record.switch_controller,
|
|
608
|
+
record.mesh_id,
|
|
609
|
+
);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const clampedBrightness = Math.max(
|
|
614
|
+
0,
|
|
615
|
+
Math.min(100, Math.round(brightnessPct ?? 100)),
|
|
616
|
+
);
|
|
617
|
+
const on = clampedBrightness > 0;
|
|
618
|
+
|
|
619
|
+
const r = Math.max(0, Math.min(255, Math.round(rgb.r)));
|
|
620
|
+
const g = Math.max(0, Math.min(255, Math.round(rgb.g)));
|
|
621
|
+
const b = Math.max(0, Math.min(255, Math.round(rgb.b)));
|
|
622
|
+
|
|
623
|
+
const seq = this.nextSeq();
|
|
624
|
+
|
|
625
|
+
// colorTone=254 => "color" mode, per HA's combo_control for RGB
|
|
626
|
+
const packet = this.buildComboPacket(
|
|
627
|
+
controllerId,
|
|
628
|
+
meshIndex,
|
|
629
|
+
on,
|
|
630
|
+
clampedBrightness,
|
|
631
|
+
254,
|
|
632
|
+
{ r, g, b },
|
|
633
|
+
seq,
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
this.socket.write(packet);
|
|
637
|
+
this.log.info(
|
|
638
|
+
'[Cync TCP] Sent color combo packet: device=%s on=%s brightness=%d rgb=(%d,%d,%d) seq=%d',
|
|
639
|
+
deviceId,
|
|
640
|
+
String(on),
|
|
641
|
+
clampedBrightness,
|
|
642
|
+
r,
|
|
643
|
+
g,
|
|
644
|
+
b,
|
|
645
|
+
seq,
|
|
646
|
+
);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
490
649
|
private findDevice(deviceId: string) {
|
|
491
650
|
for (const mesh of this.config?.meshes ?? []) {
|
|
492
651
|
for (const dev of mesh.devices ?? []) {
|
|
@@ -515,9 +674,7 @@ export class TcpClient {
|
|
|
515
674
|
|
|
516
675
|
socket.on('close', () => {
|
|
517
676
|
this.log.warn('[Cync TCP] Socket closed.');
|
|
518
|
-
|
|
519
|
-
this.socket = null;
|
|
520
|
-
}
|
|
677
|
+
this.socket = null;
|
|
521
678
|
});
|
|
522
679
|
|
|
523
680
|
socket.on('error', (err) => {
|
|
@@ -596,13 +753,14 @@ export class TcpClient {
|
|
|
596
753
|
// Default payload is the raw frame
|
|
597
754
|
let payload: unknown = frame;
|
|
598
755
|
|
|
599
|
-
|
|
600
|
-
|
|
756
|
+
// 0x73 / 0x83 carry per-device state updates on the LAN.
|
|
757
|
+
// Mirror the HA integration: try the topology-based parser first.
|
|
758
|
+
if (type === 0x73 || type === 0x83) {
|
|
601
759
|
const lanParsed = this.parseLanSwitchUpdate(frame);
|
|
602
760
|
if (lanParsed) {
|
|
603
761
|
payload = lanParsed;
|
|
604
|
-
} else {
|
|
605
|
-
// Fallback to legacy controller-level parsing
|
|
762
|
+
} else if (type === 0x83) {
|
|
763
|
+
// Fallback to legacy controller-level parsing only for 0x83
|
|
606
764
|
const parsed = this.parseSwitchStateFrame(frame);
|
|
607
765
|
if (parsed) {
|
|
608
766
|
const deviceId = this.controllerToDevice.get(parsed.controllerId);
|