homebridge-cync-app 0.1.7 → 0.1.9

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 (36) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cync/config-client.d.ts +11 -0
  3. package/dist/cync/config-client.js +113 -6
  4. package/dist/cync/config-client.js.map +1 -1
  5. package/dist/cync/cync-accessory-helpers.d.ts +46 -0
  6. package/dist/cync/cync-accessory-helpers.js +140 -0
  7. package/dist/cync/cync-accessory-helpers.js.map +1 -0
  8. package/dist/cync/cync-client.d.ts +4 -0
  9. package/dist/cync/cync-client.js +150 -34
  10. package/dist/cync/cync-client.js.map +1 -1
  11. package/dist/cync/cync-light-accessory.d.ts +4 -0
  12. package/dist/cync/cync-light-accessory.js +190 -0
  13. package/dist/cync/cync-light-accessory.js.map +1 -0
  14. package/dist/cync/cync-switch-accessory.d.ts +4 -0
  15. package/dist/cync/cync-switch-accessory.js +64 -0
  16. package/dist/cync/cync-switch-accessory.js.map +1 -0
  17. package/dist/cync/device-catalog.js +9 -4
  18. package/dist/cync/device-catalog.js.map +1 -1
  19. package/dist/cync/tcp-client.d.ts +7 -0
  20. package/dist/cync/tcp-client.js +122 -30
  21. package/dist/cync/tcp-client.js.map +1 -1
  22. package/dist/cync/token-store.js +2 -2
  23. package/dist/cync/token-store.js.map +1 -1
  24. package/dist/platform.d.ts +1 -3
  25. package/dist/platform.js +18 -382
  26. package/dist/platform.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/cync/config-client.ts +175 -12
  29. package/src/cync/cync-accessory-helpers.ts +233 -0
  30. package/src/cync/cync-client.ts +231 -44
  31. package/src/cync/cync-light-accessory.ts +369 -0
  32. package/src/cync/cync-switch-accessory.ts +119 -0
  33. package/src/cync/device-catalog.ts +9 -4
  34. package/src/cync/tcp-client.ts +153 -53
  35. package/src/cync/token-store.ts +3 -2
  36. package/src/platform.ts +49 -661
package/src/platform.ts CHANGED
@@ -10,27 +10,16 @@ import type {
10
10
  import { PLATFORM_NAME } from './settings.js';
11
11
  import { CyncClient } from './cync/cync-client.js';
12
12
  import { ConfigClient } from './cync/config-client.js';
13
- import type { CyncCloudConfig, CyncDevice, CyncDeviceMesh } from './cync/config-client.js';
13
+ import type { CyncCloudConfig } from './cync/config-client.js';
14
14
  import { TcpClient } from './cync/tcp-client.js';
15
15
  import type { CyncLogger } from './cync/config-client.js';
16
- import { lookupDeviceModel } from './cync/device-catalog.js';
17
-
18
- // Narrowed view of the Cync device properties returned by getDeviceProperties()
19
- type CyncDeviceRaw = {
20
- displayName?: string;
21
- firmwareVersion?: string;
22
- mac?: string;
23
- wifiMac?: string;
24
- deviceType?: number;
25
- deviceID?: number;
26
- commissionedDate?: string;
27
- [key: string]: unknown;
28
- };
29
-
30
- // CyncDevice as seen by the platform, possibly enriched with a `raw` block
31
- type CyncDeviceWithRaw = CyncDevice & {
32
- raw?: CyncDeviceRaw;
33
- };
16
+ import {
17
+ type CyncAccessoryContext,
18
+ type CyncAccessoryEnv,
19
+ resolveDeviceType,
20
+ } from './cync/cync-accessory-helpers.js';
21
+ import { configureCyncLightAccessory } from './cync/cync-light-accessory.js';
22
+ import { configureCyncSwitchAccessory } from './cync/cync-switch-accessory.js';
34
23
 
35
24
  const toCyncLogger = (log: Logger): CyncLogger => ({
36
25
  debug: log.debug.bind(log),
@@ -39,111 +28,6 @@ const toCyncLogger = (log: Logger): CyncLogger => ({
39
28
  error: log.error.bind(log),
40
29
  });
41
30
 
42
- interface CyncAccessoryContext {
43
- cync?: {
44
- meshId: string;
45
- deviceId: string;
46
- productId?: string;
47
-
48
- on?: boolean;
49
- brightness?: number; // 0–100 (LAN "level")
50
-
51
- // Color state (local cache, not yet read from LAN frames)
52
- hue?: number; // 0–360
53
- saturation?: number; // 0–100
54
- rgb?: { r: number; g: number; b: number };
55
- colorActive?: boolean; // true if we last set RGB color
56
- };
57
- [key: string]: unknown;
58
- }
59
-
60
- function hsvToRgb(hue: number, saturation: number, value: number): { r: number; g: number; b: number } {
61
- const h = ((hue % 360) + 360) % 360;
62
- const s = Math.max(0, Math.min(100, saturation)) / 100;
63
- const v = Math.max(0, Math.min(100, value)) / 100;
64
-
65
- if (s === 0) {
66
- const grey = Math.round(v * 255);
67
- return { r: grey, g: grey, b: grey };
68
- }
69
-
70
- const sector = h / 60;
71
- const i = Math.floor(sector);
72
- const f = sector - i;
73
-
74
- const p = v * (1 - s);
75
- const q = v * (1 - s * f);
76
- const t = v * (1 - s * (1 - f));
77
-
78
- let r = 0;
79
- let g = 0;
80
- let b = 0;
81
-
82
- switch (i) {
83
- case 0:
84
- r = v;
85
- g = t;
86
- b = p;
87
- break;
88
- case 1:
89
- r = q;
90
- g = v;
91
- b = p;
92
- break;
93
- case 2:
94
- r = p;
95
- g = v;
96
- b = t;
97
- break;
98
- case 3:
99
- r = p;
100
- g = q;
101
- b = v;
102
- break;
103
- case 4:
104
- r = t;
105
- g = p;
106
- b = v;
107
- break;
108
- default:
109
- r = v;
110
- g = p;
111
- b = q;
112
- break;
113
- }
114
-
115
- return {
116
- r: Math.round(r * 255),
117
- g: Math.round(g * 255),
118
- b: Math.round(b * 255),
119
- };
120
- }
121
-
122
- function resolveDeviceType(device: CyncDevice): number | undefined {
123
- const typedDevice = device as unknown as {
124
- device_type?: number;
125
- raw?: { deviceType?: number | string };
126
- };
127
-
128
- if (typeof typedDevice.device_type === 'number') {
129
- return typedDevice.device_type;
130
- }
131
-
132
- const rawType = typedDevice.raw?.deviceType;
133
- if (typeof rawType === 'number') {
134
- return rawType;
135
- }
136
-
137
- if (typeof rawType === 'string' && rawType.trim() !== '') {
138
- const parsed = Number(rawType.trim());
139
- if (!Number.isNaN(parsed)) {
140
- return parsed;
141
- }
142
- }
143
-
144
- return undefined;
145
- }
146
-
147
31
  export class CyncAppPlatform implements DynamicPlatformPlugin {
148
32
  public readonly accessories: PlatformAccessory[] = [];
149
33
  public configureAccessory(accessory: PlatformAccessory): void {
@@ -155,13 +39,14 @@ export class CyncAppPlatform implements DynamicPlatformPlugin {
155
39
  private readonly config: PlatformConfig;
156
40
  private readonly client: CyncClient;
157
41
  private readonly tcpClient: TcpClient;
42
+ private readonly accessoryEnv: CyncAccessoryEnv;
158
43
 
159
44
  private cloudConfig: CyncCloudConfig | null = null;
160
45
  private readonly deviceIdToAccessory = new Map<string, PlatformAccessory>();
161
46
  private readonly deviceLastSeen = new Map<string, number>();
162
47
  private readonly devicePollTimers = new Map<string, NodeJS.Timeout>();
163
48
 
164
- private readonly offlineTimeoutMs = 5 * 60 * 1000; // 5 minutes
49
+ private readonly offlineTimeoutMs = 30 * 60 * 1000;
165
50
  private readonly pollIntervalMs = 60_000; // 60 seconds
166
51
 
167
52
  private markDeviceSeen(deviceId: string): void {
@@ -280,528 +165,6 @@ export class CyncAppPlatform implements DynamicPlatformPlugin {
280
165
  }
281
166
  }
282
167
 
283
- private configureCyncSwitchAccessory(
284
- mesh: CyncDeviceMesh,
285
- device: CyncDevice,
286
- accessory: PlatformAccessory,
287
- deviceName: string,
288
- deviceId: string,
289
- ): void {
290
- const service =
291
- accessory.getService(this.api.hap.Service.Switch) ||
292
- accessory.addService(this.api.hap.Service.Switch, deviceName);
293
- const existingLight = accessory.getService(this.api.hap.Service.Lightbulb);
294
- if (existingLight) {
295
- this.log.info(
296
- 'Cync: removing stale Lightbulb service from %s (deviceId=%s) before configuring as Switch',
297
- deviceName,
298
- deviceId,
299
- );
300
- accessory.removeService(existingLight);
301
- }
302
- this.applyAccessoryInformationFromCyncDevice(accessory, device, deviceName, deviceId);
303
-
304
- // Ensure context is initialized
305
- const ctx = accessory.context as CyncAccessoryContext;
306
- ctx.cync = ctx.cync ?? {
307
- meshId: mesh.id,
308
- deviceId,
309
- productId: device.product_id,
310
- on: false,
311
- };
312
-
313
- // Remember mapping for LAN updates
314
- this.deviceIdToAccessory.set(deviceId, accessory);
315
- this.markDeviceSeen(deviceId);
316
- this.startPollingDevice(deviceId);
317
-
318
- service
319
- .getCharacteristic(this.api.hap.Characteristic.On)
320
- .onGet(() => {
321
- if (this.isDeviceProbablyOffline(deviceId)) {
322
- throw new this.api.hap.HapStatusError(
323
- this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
324
- );
325
- }
326
-
327
- const currentOn = !!ctx.cync?.on;
328
- this.log.info(
329
- 'Cync: On.get -> %s for %s (deviceId=%s)',
330
- String(currentOn),
331
- deviceName,
332
- deviceId,
333
- );
334
- return currentOn;
335
- })
336
- .onSet(async (value) => {
337
- const cyncMeta = ctx.cync;
338
-
339
- if (!cyncMeta?.deviceId) {
340
- this.log.warn(
341
- 'Cync: Light On.set called for %s but no cync.deviceId in context',
342
- deviceName,
343
- );
344
- return;
345
- }
346
-
347
- const on = value === true || value === 1;
348
-
349
- this.log.info(
350
- 'Cync: Light On.set -> %s for %s (deviceId=%s)',
351
- String(on),
352
- deviceName,
353
- cyncMeta.deviceId,
354
- );
355
-
356
- cyncMeta.on = on;
357
-
358
- if (!on) {
359
- // Off is always a plain power packet
360
- await this.tcpClient.setSwitchState(cyncMeta.deviceId, { on: false });
361
- return;
362
- }
363
-
364
- // Turning on:
365
- // - If we were in color mode with a known RGB + brightness, restore color.
366
- // - Otherwise, just send a basic power-on packet.
367
- if (cyncMeta.colorActive && cyncMeta.rgb && typeof cyncMeta.brightness === 'number') {
368
- await this.tcpClient.setColor(cyncMeta.deviceId, cyncMeta.rgb, cyncMeta.brightness);
369
- } else {
370
- await this.tcpClient.setSwitchState(cyncMeta.deviceId, { on: true });
371
- }
372
- });
373
- }
374
-
375
- private configureCyncLightAccessory(
376
- mesh: CyncDeviceMesh,
377
- device: CyncDevice,
378
- accessory: PlatformAccessory,
379
- deviceName: string,
380
- deviceId: string,
381
- ): void {
382
- // If this accessory used to be a switch, remove that service
383
- const existingSwitch = accessory.getService(this.api.hap.Service.Switch);
384
- if (existingSwitch) {
385
- this.log.info(
386
- 'Cync: removing stale Switch service from %s (deviceId=%s) before configuring as Lightbulb',
387
- deviceName,
388
- deviceId,
389
- );
390
- accessory.removeService(existingSwitch);
391
- }
392
-
393
- const service =
394
- accessory.getService(this.api.hap.Service.Lightbulb) ||
395
- accessory.addService(this.api.hap.Service.Lightbulb, deviceName);
396
-
397
- // Optionally update accessory category so UIs treat it as a light
398
- if (accessory.category !== this.api.hap.Categories.LIGHTBULB) {
399
- accessory.category = this.api.hap.Categories.LIGHTBULB;
400
- }
401
-
402
- // NEW: populate Accessory Information from Cync metadata
403
- this.applyAccessoryInformationFromCyncDevice(accessory, device, deviceName, deviceId);
404
-
405
- // Ensure context is initialized
406
- const ctx = accessory.context as CyncAccessoryContext;
407
- ctx.cync = ctx.cync ?? {
408
- meshId: mesh.id,
409
- deviceId,
410
- productId: device.product_id,
411
- on: false,
412
- };
413
-
414
- // Remember mapping for LAN updates
415
- this.deviceIdToAccessory.set(deviceId, accessory);
416
- this.markDeviceSeen(deviceId);
417
- this.startPollingDevice(deviceId);
418
-
419
- const Characteristic = this.api.hap.Characteristic;
420
-
421
- // ----- On/Off -----
422
- service
423
- .getCharacteristic(Characteristic.On)
424
- .onGet(() => {
425
- if (this.isDeviceProbablyOffline(deviceId)) {
426
- throw new this.api.hap.HapStatusError(
427
- this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
428
- );
429
- }
430
-
431
- const currentOn = !!ctx.cync?.on;
432
- this.log.info(
433
- 'Cync: Light On.get -> %s for %s (deviceId=%s)',
434
- String(currentOn),
435
- deviceName,
436
- deviceId,
437
- );
438
- return currentOn;
439
- })
440
- .onSet(async (value) => {
441
- const cyncMeta = ctx.cync;
442
-
443
- if (!cyncMeta?.deviceId) {
444
- this.log.warn(
445
- 'Cync: Light On.set called for %s but no cync.deviceId in context',
446
- deviceName,
447
- );
448
- return;
449
- }
450
-
451
- const on = value === true || value === 1;
452
-
453
- this.log.info(
454
- 'Cync: Light On.set -> %s for %s (deviceId=%s)',
455
- String(on),
456
- deviceName,
457
- cyncMeta.deviceId,
458
- );
459
-
460
- // Optimistic local cache; LAN update will confirm
461
- cyncMeta.on = on;
462
-
463
- try {
464
- await this.tcpClient.setSwitchState(cyncMeta.deviceId, { on });
465
- this.markDeviceSeen(cyncMeta.deviceId);
466
- } catch (err) {
467
- this.log.warn(
468
- 'Cync: Light On.set failed for %s (deviceId=%s): %s',
469
- deviceName,
470
- cyncMeta.deviceId,
471
- (err as Error).message ?? String(err),
472
- );
473
-
474
- throw new this.api.hap.HapStatusError(
475
- this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
476
- );
477
- }
478
- });
479
- // ----- Brightness (dimming via LAN combo_control) -----
480
- service
481
- .getCharacteristic(Characteristic.Brightness)
482
- .onGet(() => {
483
- if (this.isDeviceProbablyOffline(deviceId)) {
484
- throw new this.api.hap.HapStatusError(
485
- this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
486
- );
487
- }
488
-
489
- const current = ctx.cync?.brightness;
490
-
491
- // If we have a cached LAN level, use it; otherwise infer from On.
492
- if (typeof current === 'number') {
493
- return current;
494
- }
495
-
496
- const on = ctx.cync?.on ?? false;
497
- return on ? 100 : 0;
498
- })
499
- .onSet(async (value) => {
500
- const cyncMeta = ctx.cync;
501
-
502
- if (!cyncMeta?.deviceId) {
503
- this.log.warn(
504
- 'Cync: Light Brightness.set called for %s but no cync.deviceId in context',
505
- deviceName,
506
- );
507
- return;
508
- }
509
-
510
- const brightness = Math.max(0, Math.min(100, Number(value)));
511
-
512
- if (!Number.isFinite(brightness)) {
513
- this.log.warn(
514
- 'Cync: Light Brightness.set received invalid value=%o for %s (deviceId=%s)',
515
- value,
516
- deviceName,
517
- cyncMeta.deviceId,
518
- );
519
- return;
520
- }
521
-
522
- // Optimistic cache
523
- cyncMeta.brightness = brightness;
524
- cyncMeta.on = brightness > 0;
525
-
526
- this.log.info(
527
- 'Cync: Light Brightness.set -> %d for %s (deviceId=%s)',
528
- brightness,
529
- deviceName,
530
- cyncMeta.deviceId,
531
- );
532
-
533
- try {
534
- // If we're in "color mode", keep the existing RGB and scale brightness via setColor();
535
- // otherwise treat this as a white-brightness change.
536
- if (cyncMeta.colorActive && cyncMeta.rgb) {
537
- await this.tcpClient.setColor(
538
- cyncMeta.deviceId,
539
- cyncMeta.rgb,
540
- brightness,
541
- );
542
- } else {
543
- await this.tcpClient.setBrightness(cyncMeta.deviceId, brightness);
544
- }
545
-
546
- this.markDeviceSeen(cyncMeta.deviceId);
547
- } catch (err) {
548
- this.log.warn(
549
- 'Cync: Light Brightness.set failed for %s (deviceId=%s): %s',
550
- deviceName,
551
- cyncMeta.deviceId,
552
- (err as Error).message ?? String(err),
553
- );
554
-
555
- throw new this.api.hap.HapStatusError(
556
- this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
557
- );
558
- }
559
- });
560
- // ----- Hue -----
561
- service
562
- .getCharacteristic(Characteristic.Hue)
563
- .onGet(() => {
564
- if (this.isDeviceProbablyOffline(deviceId)) {
565
- throw new this.api.hap.HapStatusError(
566
- this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
567
- );
568
- }
569
-
570
- const hue = ctx.cync?.hue;
571
- if (typeof hue === 'number') {
572
- return hue;
573
- }
574
- // Default to 0° (red) if we have no color history
575
- return 0;
576
- })
577
- .onSet(async (value) => {
578
- const cyncMeta = ctx.cync;
579
-
580
- if (!cyncMeta?.deviceId) {
581
- this.log.warn(
582
- 'Cync: Light Hue.set called for %s but no cync.deviceId in context',
583
- deviceName,
584
- );
585
- return;
586
- }
587
-
588
- const hue = Math.max(0, Math.min(360, Number(value)));
589
- if (!Number.isFinite(hue)) {
590
- this.log.warn(
591
- 'Cync: Light Hue.set received invalid value=%o for %s (deviceId=%s)',
592
- value,
593
- deviceName,
594
- cyncMeta.deviceId,
595
- );
596
- return;
597
- }
598
-
599
- // Use cached saturation/brightness if available, otherwise sane defaults
600
- const saturation =
601
- typeof cyncMeta.saturation === 'number' ? cyncMeta.saturation : 100;
602
-
603
- const brightness =
604
- typeof cyncMeta.brightness === 'number' ? cyncMeta.brightness : 100;
605
-
606
- const rgb = hsvToRgb(hue, saturation, brightness);
607
-
608
- // Optimistic cache
609
- cyncMeta.hue = hue;
610
- cyncMeta.saturation = saturation;
611
- cyncMeta.rgb = rgb;
612
- cyncMeta.colorActive = true;
613
- cyncMeta.on = brightness > 0;
614
- cyncMeta.brightness = brightness;
615
-
616
- this.log.info(
617
- 'Cync: Light Hue.set -> %d for %s (deviceId=%s) -> rgb=(%d,%d,%d) brightness=%d',
618
- hue,
619
- deviceName,
620
- cyncMeta.deviceId,
621
- rgb.r,
622
- rgb.g,
623
- rgb.b,
624
- brightness,
625
- );
626
-
627
- try {
628
- await this.tcpClient.setColor(cyncMeta.deviceId, rgb, brightness);
629
- this.markDeviceSeen(cyncMeta.deviceId);
630
- } catch (err) {
631
- this.log.warn(
632
- 'Cync: Light Hue.set failed for %s (deviceId=%s): %s',
633
- deviceName,
634
- cyncMeta.deviceId,
635
- (err as Error).message ?? String(err),
636
- );
637
-
638
- throw new this.api.hap.HapStatusError(
639
- this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
640
- );
641
- }
642
- });
643
- // ----- Saturation -----
644
- service
645
- .getCharacteristic(Characteristic.Saturation)
646
- .onGet(() => {
647
- if (this.isDeviceProbablyOffline(deviceId)) {
648
- throw new this.api.hap.HapStatusError(
649
- this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
650
- );
651
- }
652
-
653
- const sat = ctx.cync?.saturation;
654
- if (typeof sat === 'number') {
655
- return sat;
656
- }
657
- return 100;
658
- })
659
- .onSet(async (value) => {
660
- const cyncMeta = ctx.cync;
661
-
662
- if (!cyncMeta?.deviceId) {
663
- this.log.warn(
664
- 'Cync: Light Saturation.set called for %s but no cync.deviceId in context',
665
- deviceName,
666
- );
667
- return;
668
- }
669
-
670
- const saturation = Math.max(0, Math.min(100, Number(value)));
671
- if (!Number.isFinite(saturation)) {
672
- this.log.warn(
673
- 'Cync: Light Saturation.set received invalid value=%o for %s (deviceId=%s)',
674
- value,
675
- deviceName,
676
- cyncMeta.deviceId,
677
- );
678
- return;
679
- }
680
-
681
- const hue = typeof cyncMeta.hue === 'number' ? cyncMeta.hue : 0;
682
-
683
- const brightness =
684
- typeof cyncMeta.brightness === 'number' ? cyncMeta.brightness : 100;
685
-
686
- const rgb = hsvToRgb(hue, saturation, brightness);
687
-
688
- // Optimistic cache
689
- cyncMeta.hue = hue;
690
- cyncMeta.saturation = saturation;
691
- cyncMeta.rgb = rgb;
692
- cyncMeta.colorActive = true;
693
- cyncMeta.on = brightness > 0;
694
- cyncMeta.brightness = brightness;
695
-
696
- this.log.info(
697
- 'Cync: Light Saturation.set -> %d for %s (deviceId=%s) -> rgb=(%d,%d,%d) brightness=%d',
698
- saturation,
699
- deviceName,
700
- cyncMeta.deviceId,
701
- rgb.r,
702
- rgb.g,
703
- rgb.b,
704
- brightness,
705
- );
706
-
707
- try {
708
- await this.tcpClient.setColor(cyncMeta.deviceId, rgb, brightness);
709
- this.markDeviceSeen(cyncMeta.deviceId);
710
- } catch (err) {
711
- this.log.warn(
712
- 'Cync: Light Saturation.set failed for %s (deviceId=%s): %s',
713
- deviceName,
714
- cyncMeta.deviceId,
715
- (err as Error).message ?? String(err),
716
- );
717
-
718
- throw new this.api.hap.HapStatusError(
719
- this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
720
- );
721
- }
722
- });
723
- }
724
-
725
- private applyAccessoryInformationFromCyncDevice(
726
- accessory: PlatformAccessory,
727
- device: CyncDevice,
728
- deviceName: string,
729
- deviceId: string,
730
- ): void {
731
- const infoService = accessory.getService(this.api.hap.Service.AccessoryInformation);
732
- if (!infoService) {
733
- return;
734
- }
735
-
736
- const Characteristic = this.api.hap.Characteristic;
737
- const deviceWithRaw = device as CyncDeviceWithRaw;
738
- const rawDevice = deviceWithRaw.raw ?? {};
739
-
740
- // Name: keep in sync with how we present the accessory
741
- const name = deviceName || accessory.displayName;
742
- infoService.updateCharacteristic(Characteristic.Name, name);
743
-
744
- // Manufacturer: fixed for all Cync devices
745
- infoService.updateCharacteristic(Characteristic.Manufacturer, 'GE Lighting');
746
-
747
- // Model: prefer catalog entry (Cync app-style model name), fall back to raw info
748
- const resolvedType = resolveDeviceType(device);
749
- const catalogEntry = typeof resolvedType === 'number'
750
- ? lookupDeviceModel(resolvedType)
751
- : undefined;
752
-
753
- let model: string;
754
-
755
- if (catalogEntry) {
756
- // Use the Cync app-style model name
757
- model = catalogEntry.modelName;
758
-
759
- // Persist for debugging / future use
760
- const ctx = accessory.context as Record<string, unknown>;
761
- ctx.deviceType = resolvedType;
762
- ctx.modelName = catalogEntry.modelName;
763
- if (catalogEntry.marketingName) {
764
- ctx.marketingName = catalogEntry.marketingName;
765
- }
766
- } else {
767
- // Fallback: use device displayName + type
768
- const modelBase =
769
- typeof rawDevice.displayName === 'string' && rawDevice.displayName.trim().length > 0
770
- ? rawDevice.displayName.trim()
771
- : 'Cync Device';
772
-
773
- const modelSuffix =
774
- typeof resolvedType === 'number'
775
- ? ` (Type ${resolvedType})`
776
- : '';
777
-
778
- model = modelBase + modelSuffix;
779
- }
780
-
781
- infoService.updateCharacteristic(Characteristic.Model, model);
782
-
783
-
784
- // Serial: prefer wifiMac, then mac, then deviceID, then the string deviceId
785
- const serial =
786
- (typeof rawDevice.wifiMac === 'string' && rawDevice.wifiMac.trim().length > 0)
787
- ? rawDevice.wifiMac.trim()
788
- : (typeof rawDevice.mac === 'string' && rawDevice.mac.trim().length > 0)
789
- ? rawDevice.mac.trim()
790
- : (rawDevice.deviceID !== undefined
791
- ? String(rawDevice.deviceID)
792
- : deviceId);
793
-
794
- infoService.updateCharacteristic(Characteristic.SerialNumber, serial);
795
-
796
- // Firmware revision, if present
797
- if (typeof rawDevice.firmwareVersion === 'string' && rawDevice.firmwareVersion.trim().length > 0) {
798
- infoService.updateCharacteristic(
799
- Characteristic.FirmwareRevision,
800
- rawDevice.firmwareVersion.trim(),
801
- );
802
- }
803
- }
804
-
805
168
  constructor(log: Logger, config: PlatformConfig, api: API) {
806
169
  this.log = log;
807
170
  this.config = config;
@@ -856,6 +219,17 @@ export class CyncAppPlatform implements DynamicPlatformPlugin {
856
219
  this.log.info(PLATFORM_NAME, 'didFinishLaunching');
857
220
  void this.loadCync();
858
221
  });
222
+ this.accessoryEnv = {
223
+ log: this.log,
224
+ api: this.api,
225
+ tcpClient: this.tcpClient,
226
+ isDeviceProbablyOffline: this.isDeviceProbablyOffline.bind(this),
227
+ markDeviceSeen: this.markDeviceSeen.bind(this),
228
+ startPollingDevice: this.startPollingDevice.bind(this),
229
+ registerAccessoryForDevice: (deviceId, accessory) => {
230
+ this.deviceIdToAccessory.set(deviceId, accessory);
231
+ },
232
+ };
859
233
  }
860
234
 
861
235
  private async loadCync(): Promise<void> {
@@ -985,21 +359,35 @@ export class CyncAppPlatform implements DynamicPlatformPlugin {
985
359
  const isDownlight = deviceType === 46;
986
360
 
987
361
  if (isDownlight) {
988
- this.log.info(
989
- 'Cync: configuring %s as Lightbulb (deviceType=%s, deviceId=%s)',
990
- deviceName,
991
- String(deviceType),
992
- deviceId,
993
- );
994
- this.configureCyncLightAccessory(mesh, device, accessory, deviceName, deviceId);
362
+ this.log.info(
363
+ 'Cync: configuring %s as Lightbulb (deviceType=%s, deviceId=%s)',
364
+ deviceName,
365
+ String(deviceType),
366
+ deviceId,
367
+ );
368
+ configureCyncLightAccessory(
369
+ this.accessoryEnv,
370
+ mesh,
371
+ device,
372
+ accessory,
373
+ deviceName,
374
+ deviceId,
375
+ );
995
376
  } else {
996
- this.log.info(
997
- 'Cync: configuring %s as Switch (deviceType=%s, deviceId=%s)',
998
- deviceName,
999
- deviceType ?? 'unknown',
1000
- deviceId,
1001
- );
1002
- this.configureCyncSwitchAccessory(mesh, device, accessory, deviceName, deviceId);
377
+ this.log.info(
378
+ 'Cync: configuring %s as Switch (deviceType=%s, deviceId=%s)',
379
+ deviceName,
380
+ deviceType ?? 'unknown',
381
+ deviceId,
382
+ );
383
+ configureCyncSwitchAccessory(
384
+ this.accessoryEnv,
385
+ mesh,
386
+ device,
387
+ accessory,
388
+ deviceName,
389
+ deviceId,
390
+ );
1003
391
  }
1004
392
  }
1005
393
  }