homebridge-cync-app 0.1.6 → 0.1.8

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 (35) hide show
  1. package/.github/ISSUE_TEMPLATE/add-support-for-a-new-cync-device.md +139 -0
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +3 -1
  4. package/dist/cync/config-client.d.ts +23 -0
  5. package/dist/cync/config-client.js +133 -0
  6. package/dist/cync/config-client.js.map +1 -1
  7. package/dist/cync/cync-accessory-helpers.d.ts +46 -0
  8. package/dist/cync/cync-accessory-helpers.js +138 -0
  9. package/dist/cync/cync-accessory-helpers.js.map +1 -0
  10. package/dist/cync/cync-client.d.ts +5 -0
  11. package/dist/cync/cync-client.js +156 -2
  12. package/dist/cync/cync-client.js.map +1 -1
  13. package/dist/cync/cync-light-accessory.d.ts +4 -0
  14. package/dist/cync/cync-light-accessory.js +197 -0
  15. package/dist/cync/cync-light-accessory.js.map +1 -0
  16. package/dist/cync/cync-switch-accessory.d.ts +4 -0
  17. package/dist/cync/cync-switch-accessory.js +58 -0
  18. package/dist/cync/cync-switch-accessory.js.map +1 -0
  19. package/dist/cync/device-catalog.d.ts +19 -0
  20. package/dist/cync/device-catalog.js +35 -0
  21. package/dist/cync/device-catalog.js.map +1 -0
  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 +8 -3
  25. package/dist/platform.js +49 -320
  26. package/dist/platform.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/cync/config-client.ts +192 -0
  29. package/src/cync/cync-accessory-helpers.ts +233 -0
  30. package/src/cync/cync-client.ts +238 -2
  31. package/src/cync/cync-light-accessory.ts +359 -0
  32. package/src/cync/cync-switch-accessory.ts +100 -0
  33. package/src/cync/device-catalog.ts +55 -0
  34. package/src/cync/token-store.ts +3 -2
  35. package/src/platform.ts +87 -549
package/src/platform.ts CHANGED
@@ -10,26 +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
-
17
- // Narrowed view of the Cync device properties returned by getDeviceProperties()
18
- type CyncDeviceRaw = {
19
- displayName?: string;
20
- firmwareVersion?: string;
21
- mac?: string;
22
- wifiMac?: string;
23
- deviceType?: number;
24
- deviceID?: number;
25
- commissionedDate?: string;
26
- [key: string]: unknown;
27
- };
28
-
29
- // CyncDevice as seen by the platform, possibly enriched with a `raw` block
30
- type CyncDeviceWithRaw = CyncDevice & {
31
- raw?: CyncDeviceRaw;
32
- };
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';
33
23
 
34
24
  const toCyncLogger = (log: Logger): CyncLogger => ({
35
25
  debug: log.debug.bind(log),
@@ -38,86 +28,6 @@ const toCyncLogger = (log: Logger): CyncLogger => ({
38
28
  error: log.error.bind(log),
39
29
  });
40
30
 
41
- interface CyncAccessoryContext {
42
- cync?: {
43
- meshId: string;
44
- deviceId: string;
45
- productId?: string;
46
-
47
- on?: boolean;
48
- brightness?: number; // 0–100 (LAN "level")
49
-
50
- // Color state (local cache, not yet read from LAN frames)
51
- hue?: number; // 0–360
52
- saturation?: number; // 0–100
53
- rgb?: { r: number; g: number; b: number };
54
- colorActive?: boolean; // true if we last set RGB color
55
- };
56
- [key: string]: unknown;
57
- }
58
-
59
- function hsvToRgb(hue: number, saturation: number, value: number): { r: number; g: number; b: number } {
60
- const h = ((hue % 360) + 360) % 360;
61
- const s = Math.max(0, Math.min(100, saturation)) / 100;
62
- const v = Math.max(0, Math.min(100, value)) / 100;
63
-
64
- if (s === 0) {
65
- const grey = Math.round(v * 255);
66
- return { r: grey, g: grey, b: grey };
67
- }
68
-
69
- const sector = h / 60;
70
- const i = Math.floor(sector);
71
- const f = sector - i;
72
-
73
- const p = v * (1 - s);
74
- const q = v * (1 - s * f);
75
- const t = v * (1 - s * (1 - f));
76
-
77
- let r = 0;
78
- let g = 0;
79
- let b = 0;
80
-
81
- switch (i) {
82
- case 0:
83
- r = v;
84
- g = t;
85
- b = p;
86
- break;
87
- case 1:
88
- r = q;
89
- g = v;
90
- b = p;
91
- break;
92
- case 2:
93
- r = p;
94
- g = v;
95
- b = t;
96
- break;
97
- case 3:
98
- r = p;
99
- g = q;
100
- b = v;
101
- break;
102
- case 4:
103
- r = t;
104
- g = p;
105
- b = v;
106
- break;
107
- default:
108
- r = v;
109
- g = p;
110
- b = q;
111
- break;
112
- }
113
-
114
- return {
115
- r: Math.round(r * 255),
116
- g: Math.round(g * 255),
117
- b: Math.round(b * 255),
118
- };
119
- }
120
-
121
31
  export class CyncAppPlatform implements DynamicPlatformPlugin {
122
32
  public readonly accessories: PlatformAccessory[] = [];
123
33
  public configureAccessory(accessory: PlatformAccessory): void {
@@ -129,9 +39,47 @@ export class CyncAppPlatform implements DynamicPlatformPlugin {
129
39
  private readonly config: PlatformConfig;
130
40
  private readonly client: CyncClient;
131
41
  private readonly tcpClient: TcpClient;
42
+ private readonly accessoryEnv: CyncAccessoryEnv;
132
43
 
133
44
  private cloudConfig: CyncCloudConfig | null = null;
134
45
  private readonly deviceIdToAccessory = new Map<string, PlatformAccessory>();
46
+ private readonly deviceLastSeen = new Map<string, number>();
47
+ private readonly devicePollTimers = new Map<string, NodeJS.Timeout>();
48
+
49
+ private readonly offlineTimeoutMs = 5 * 60 * 1000; // 5 minutes
50
+ private readonly pollIntervalMs = 60_000; // 60 seconds
51
+
52
+ private markDeviceSeen(deviceId: string): void {
53
+ this.deviceLastSeen.set(deviceId, Date.now());
54
+ }
55
+
56
+ private isDeviceProbablyOffline(deviceId: string): boolean {
57
+ const last = this.deviceLastSeen.get(deviceId);
58
+ if (!last) {
59
+ // No data yet; treat as online until we know better
60
+ return false;
61
+ }
62
+ return Date.now() - last > this.offlineTimeoutMs;
63
+ }
64
+
65
+ private startPollingDevice(deviceId: string): void {
66
+ // For now this is just a placeholder hook. We keep a timer per device so
67
+ // you can later add a real poll (e.g. TCP “ping” or cloud get) here if you want.
68
+ const existing = this.devicePollTimers.get(deviceId);
69
+ if (existing) {
70
+ clearInterval(existing);
71
+ }
72
+
73
+ const timer = setInterval(() => {
74
+ // Optional future hook:
75
+ // - Call a "getDeviceState" or similar on tcpClient/client
76
+ // - On success, call this.markDeviceSeen(deviceId)
77
+ // - On failure, optionally log or mark offline
78
+ }, this.pollIntervalMs);
79
+
80
+ this.devicePollTimers.set(deviceId, timer);
81
+ }
82
+
135
83
  private handleLanUpdate(update: unknown): void {
136
84
  // Parsed 0x83 frames from TcpClient.parseLanSwitchUpdate look like:
137
85
  // { controllerId: number, deviceId?: string, on: boolean, level: number }
@@ -146,6 +94,7 @@ export class CyncAppPlatform implements DynamicPlatformPlugin {
146
94
  }
147
95
 
148
96
  const accessory = this.deviceIdToAccessory.get(payload.deviceId);
97
+ this.markDeviceSeen(payload.deviceId);
149
98
  if (!accessory) {
150
99
  this.log.debug(
151
100
  'Cync: LAN update for unknown deviceId=%s; no accessory mapping',
@@ -216,420 +165,6 @@ export class CyncAppPlatform implements DynamicPlatformPlugin {
216
165
  }
217
166
  }
218
167
 
219
- private configureCyncSwitchAccessory(
220
- mesh: CyncDeviceMesh,
221
- device: CyncDevice,
222
- accessory: PlatformAccessory,
223
- deviceName: string,
224
- deviceId: string,
225
- ): void {
226
- const service =
227
- accessory.getService(this.api.hap.Service.Switch) ||
228
- accessory.addService(this.api.hap.Service.Switch, deviceName);
229
- const existingLight = accessory.getService(this.api.hap.Service.Lightbulb);
230
- if (existingLight) {
231
- this.log.info(
232
- 'Cync: removing stale Lightbulb service from %s (deviceId=%s) before configuring as Switch',
233
- deviceName,
234
- deviceId,
235
- );
236
- accessory.removeService(existingLight);
237
- }
238
- // Ensure context is initialized
239
- const ctx = accessory.context as CyncAccessoryContext;
240
- ctx.cync = ctx.cync ?? {
241
- meshId: mesh.id,
242
- deviceId,
243
- productId: device.product_id,
244
- on: false,
245
- };
246
-
247
- // Remember mapping for LAN updates
248
- this.deviceIdToAccessory.set(deviceId, accessory);
249
-
250
- service
251
- .getCharacteristic(this.api.hap.Characteristic.On)
252
- .onGet(() => {
253
- const currentOn = !!ctx.cync?.on;
254
- this.log.info(
255
- 'Cync: On.get -> %s for %s (deviceId=%s)',
256
- String(currentOn),
257
- deviceName,
258
- deviceId,
259
- );
260
- return currentOn;
261
- })
262
- .onSet(async (value) => {
263
- const cyncMeta = ctx.cync;
264
-
265
- if (!cyncMeta?.deviceId) {
266
- this.log.warn(
267
- 'Cync: Light On.set called for %s but no cync.deviceId in context',
268
- deviceName,
269
- );
270
- return;
271
- }
272
-
273
- const on = value === true || value === 1;
274
-
275
- this.log.info(
276
- 'Cync: Light On.set -> %s for %s (deviceId=%s)',
277
- String(on),
278
- deviceName,
279
- cyncMeta.deviceId,
280
- );
281
-
282
- cyncMeta.on = on;
283
-
284
- if (!on) {
285
- // Off is always a plain power packet
286
- await this.tcpClient.setSwitchState(cyncMeta.deviceId, { on: false });
287
- return;
288
- }
289
-
290
- // Turning on:
291
- // - If we were in color mode with a known RGB + brightness, restore color.
292
- // - Otherwise, just send a basic power-on packet.
293
- if (cyncMeta.colorActive && cyncMeta.rgb && typeof cyncMeta.brightness === 'number') {
294
- await this.tcpClient.setColor(cyncMeta.deviceId, cyncMeta.rgb, cyncMeta.brightness);
295
- } else {
296
- await this.tcpClient.setSwitchState(cyncMeta.deviceId, { on: true });
297
- }
298
- });
299
- }
300
-
301
- private configureCyncLightAccessory(
302
- mesh: CyncDeviceMesh,
303
- device: CyncDevice,
304
- accessory: PlatformAccessory,
305
- deviceName: string,
306
- deviceId: string,
307
- ): void {
308
- // If this accessory used to be a switch, remove that service
309
- const existingSwitch = accessory.getService(this.api.hap.Service.Switch);
310
- if (existingSwitch) {
311
- this.log.info(
312
- 'Cync: removing stale Switch service from %s (deviceId=%s) before configuring as Lightbulb',
313
- deviceName,
314
- deviceId,
315
- );
316
- accessory.removeService(existingSwitch);
317
- }
318
-
319
- const service =
320
- accessory.getService(this.api.hap.Service.Lightbulb) ||
321
- accessory.addService(this.api.hap.Service.Lightbulb, deviceName);
322
-
323
- // Optionally update accessory category so UIs treat it as a light
324
- if (accessory.category !== this.api.hap.Categories.LIGHTBULB) {
325
- accessory.category = this.api.hap.Categories.LIGHTBULB;
326
- }
327
-
328
- // NEW: populate Accessory Information from Cync metadata
329
- this.applyAccessoryInformationFromCyncDevice(accessory, device, deviceName, deviceId);
330
-
331
- // Ensure context is initialized
332
- const ctx = accessory.context as CyncAccessoryContext;
333
- ctx.cync = ctx.cync ?? {
334
- meshId: mesh.id,
335
- deviceId,
336
- productId: device.product_id,
337
- on: false,
338
- };
339
-
340
- // Remember mapping for LAN updates
341
- this.deviceIdToAccessory.set(deviceId, accessory);
342
-
343
- const Characteristic = this.api.hap.Characteristic;
344
-
345
- // ----- On/Off -----
346
- service
347
- .getCharacteristic(Characteristic.On)
348
- .onGet(() => {
349
- const currentOn = !!ctx.cync?.on;
350
- this.log.info(
351
- 'Cync: Light On.get -> %s for %s (deviceId=%s)',
352
- String(currentOn),
353
- deviceName,
354
- deviceId,
355
- );
356
- return currentOn;
357
- })
358
- .onSet(async (value) => {
359
- const cyncMeta = ctx.cync;
360
-
361
- if (!cyncMeta?.deviceId) {
362
- this.log.warn(
363
- 'Cync: Light On.set called for %s but no cync.deviceId in context',
364
- deviceName,
365
- );
366
- return;
367
- }
368
-
369
- const on = value === true || value === 1;
370
-
371
- this.log.info(
372
- 'Cync: Light On.set -> %s for %s (deviceId=%s)',
373
- String(on),
374
- deviceName,
375
- cyncMeta.deviceId,
376
- );
377
-
378
- // Optimistic local cache; LAN update will confirm
379
- cyncMeta.on = on;
380
-
381
- await this.tcpClient.setSwitchState(cyncMeta.deviceId, { on });
382
- });
383
-
384
- // ----- Brightness (dimming via LAN combo_control) -----
385
- service
386
- .getCharacteristic(Characteristic.Brightness)
387
- .onGet(() => {
388
- const current = ctx.cync?.brightness;
389
-
390
- // If we have a cached LAN level, use it; otherwise infer from On.
391
- if (typeof current === 'number') {
392
- return current;
393
- }
394
-
395
- const on = ctx.cync?.on ?? false;
396
- return on ? 100 : 0;
397
- })
398
- .onSet(async (value) => {
399
- const cyncMeta = ctx.cync;
400
-
401
- if (!cyncMeta?.deviceId) {
402
- this.log.warn(
403
- 'Cync: Light Brightness.set called for %s but no cync.deviceId in context',
404
- deviceName,
405
- );
406
- return;
407
- }
408
-
409
- const brightness = Math.max(
410
- 0,
411
- Math.min(100, Number(value)),
412
- );
413
-
414
- if (!Number.isFinite(brightness)) {
415
- this.log.warn(
416
- 'Cync: Light Brightness.set received invalid value=%o for %s (deviceId=%s)',
417
- value,
418
- deviceName,
419
- cyncMeta.deviceId,
420
- );
421
- return;
422
- }
423
-
424
- // Optimistic cache
425
- cyncMeta.brightness = brightness;
426
- cyncMeta.on = brightness > 0;
427
-
428
- this.log.info(
429
- 'Cync: Light Brightness.set -> %d for %s (deviceId=%s)',
430
- brightness,
431
- deviceName,
432
- cyncMeta.deviceId,
433
- );
434
-
435
- // If we're in "color mode", keep the existing RGB and scale brightness via setColor();
436
- // otherwise treat this as a white-brightness change.
437
- if (cyncMeta.colorActive && cyncMeta.rgb) {
438
- await this.tcpClient.setColor(cyncMeta.deviceId, cyncMeta.rgb, brightness);
439
- } else {
440
- await this.tcpClient.setBrightness(cyncMeta.deviceId, brightness);
441
- }
442
- });
443
- // ----- Hue -----
444
- service
445
- .getCharacteristic(Characteristic.Hue)
446
- .onGet(() => {
447
- const hue = ctx.cync?.hue;
448
- if (typeof hue === 'number') {
449
- return hue;
450
- }
451
- // Default to 0° (red) if we have no color history
452
- return 0;
453
- })
454
- .onSet(async (value) => {
455
- const cyncMeta = ctx.cync;
456
-
457
- if (!cyncMeta?.deviceId) {
458
- this.log.warn(
459
- 'Cync: Light Hue.set called for %s but no cync.deviceId in context',
460
- deviceName,
461
- );
462
- return;
463
- }
464
-
465
- const hue = Math.max(0, Math.min(360, Number(value)));
466
- if (!Number.isFinite(hue)) {
467
- this.log.warn(
468
- 'Cync: Light Hue.set received invalid value=%o for %s (deviceId=%s)',
469
- value,
470
- deviceName,
471
- cyncMeta.deviceId,
472
- );
473
- return;
474
- }
475
-
476
- // Use cached saturation/brightness if available, otherwise sane defaults
477
- const saturation = typeof cyncMeta.saturation === 'number'
478
- ? cyncMeta.saturation
479
- : 100;
480
-
481
- const brightness = typeof cyncMeta.brightness === 'number'
482
- ? cyncMeta.brightness
483
- : 100;
484
-
485
- const rgb = hsvToRgb(hue, saturation, brightness);
486
-
487
- // Optimistic cache
488
- cyncMeta.hue = hue;
489
- cyncMeta.saturation = saturation;
490
- cyncMeta.rgb = rgb;
491
- cyncMeta.colorActive = true;
492
- cyncMeta.on = brightness > 0;
493
- cyncMeta.brightness = brightness;
494
-
495
- this.log.info(
496
- 'Cync: Light Hue.set -> %d for %s (deviceId=%s) -> rgb=(%d,%d,%d) brightness=%d',
497
- hue,
498
- deviceName,
499
- cyncMeta.deviceId,
500
- rgb.r,
501
- rgb.g,
502
- rgb.b,
503
- brightness,
504
- );
505
-
506
- await this.tcpClient.setColor(cyncMeta.deviceId, rgb, brightness);
507
- });
508
-
509
- // ----- Saturation -----
510
- service
511
- .getCharacteristic(Characteristic.Saturation)
512
- .onGet(() => {
513
- const sat = ctx.cync?.saturation;
514
- if (typeof sat === 'number') {
515
- return sat;
516
- }
517
- return 100;
518
- })
519
- .onSet(async (value) => {
520
- const cyncMeta = ctx.cync;
521
-
522
- if (!cyncMeta?.deviceId) {
523
- this.log.warn(
524
- 'Cync: Light Saturation.set called for %s but no cync.deviceId in context',
525
- deviceName,
526
- );
527
- return;
528
- }
529
-
530
- const saturation = Math.max(0, Math.min(100, Number(value)));
531
- if (!Number.isFinite(saturation)) {
532
- this.log.warn(
533
- 'Cync: Light Saturation.set received invalid value=%o for %s (deviceId=%s)',
534
- value,
535
- deviceName,
536
- cyncMeta.deviceId,
537
- );
538
- return;
539
- }
540
-
541
- const hue = typeof cyncMeta.hue === 'number'
542
- ? cyncMeta.hue
543
- : 0;
544
-
545
- const brightness = typeof cyncMeta.brightness === 'number'
546
- ? cyncMeta.brightness
547
- : 100;
548
-
549
- const rgb = hsvToRgb(hue, saturation, brightness);
550
-
551
- // Optimistic cache
552
- cyncMeta.hue = hue;
553
- cyncMeta.saturation = saturation;
554
- cyncMeta.rgb = rgb;
555
- cyncMeta.colorActive = true;
556
- cyncMeta.on = brightness > 0;
557
- cyncMeta.brightness = brightness;
558
-
559
- this.log.info(
560
- 'Cync: Light Saturation.set -> %d for %s (deviceId=%s) -> rgb=(%d,%d,%d) brightness=%d',
561
- saturation,
562
- deviceName,
563
- cyncMeta.deviceId,
564
- rgb.r,
565
- rgb.g,
566
- rgb.b,
567
- brightness,
568
- );
569
-
570
- await this.tcpClient.setColor(cyncMeta.deviceId, rgb, brightness);
571
- });
572
- }
573
-
574
- private applyAccessoryInformationFromCyncDevice(
575
- accessory: PlatformAccessory,
576
- device: CyncDevice,
577
- deviceName: string,
578
- deviceId: string,
579
- ): void {
580
- const infoService = accessory.getService(this.api.hap.Service.AccessoryInformation);
581
- if (!infoService) {
582
- return;
583
- }
584
-
585
- const Characteristic = this.api.hap.Characteristic;
586
- const deviceWithRaw = device as CyncDeviceWithRaw;
587
- const rawDevice = deviceWithRaw.raw ?? {};
588
-
589
- // Name: keep in sync with how we present the accessory
590
- const name = deviceName || accessory.displayName;
591
- infoService.updateCharacteristic(Characteristic.Name, name);
592
-
593
- // Manufacturer: fixed for all Cync devices
594
- infoService.updateCharacteristic(Characteristic.Manufacturer, 'GE Lighting');
595
-
596
- // Model: use the device's displayName + type if available
597
- const modelBase =
598
- typeof rawDevice.displayName === 'string' && rawDevice.displayName.trim().length > 0
599
- ? rawDevice.displayName.trim()
600
- : 'Cync Device';
601
-
602
- const modelSuffix =
603
- typeof rawDevice.deviceType === 'number'
604
- ? ` (Type ${rawDevice.deviceType})`
605
- : '';
606
-
607
- infoService.updateCharacteristic(
608
- Characteristic.Model,
609
- modelBase + modelSuffix,
610
- );
611
-
612
- // Serial: prefer wifiMac, then mac, then deviceID, then the string deviceId
613
- const serial =
614
- (typeof rawDevice.wifiMac === 'string' && rawDevice.wifiMac.trim().length > 0)
615
- ? rawDevice.wifiMac.trim()
616
- : (typeof rawDevice.mac === 'string' && rawDevice.mac.trim().length > 0)
617
- ? rawDevice.mac.trim()
618
- : (rawDevice.deviceID !== undefined
619
- ? String(rawDevice.deviceID)
620
- : deviceId);
621
-
622
- infoService.updateCharacteristic(Characteristic.SerialNumber, serial);
623
-
624
- // Firmware revision, if present
625
- if (typeof rawDevice.firmwareVersion === 'string' && rawDevice.firmwareVersion.trim().length > 0) {
626
- infoService.updateCharacteristic(
627
- Characteristic.FirmwareRevision,
628
- rawDevice.firmwareVersion.trim(),
629
- );
630
- }
631
- }
632
-
633
168
  constructor(log: Logger, config: PlatformConfig, api: API) {
634
169
  this.log = log;
635
170
  this.config = config;
@@ -684,6 +219,17 @@ export class CyncAppPlatform implements DynamicPlatformPlugin {
684
219
  this.log.info(PLATFORM_NAME, 'didFinishLaunching');
685
220
  void this.loadCync();
686
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
+ };
687
233
  }
688
234
 
689
235
  private async loadCync(): Promise<void> {
@@ -809,47 +355,39 @@ export class CyncAppPlatform implements DynamicPlatformPlugin {
809
355
  this.accessories.push(accessory);
810
356
  }
811
357
 
812
- // Decide how to expose this device in HomeKit based on device_type / raw.deviceType
813
- const typedDevice = device as unknown as {
814
- device_type?: number;
815
- raw?: { deviceType?: number | string };
816
- };
817
-
818
- let deviceType: number | undefined;
819
-
820
- if (typeof typedDevice.device_type === 'number') {
821
- deviceType = typedDevice.device_type;
822
- } else if (typedDevice.raw && typeof typedDevice.raw.deviceType === 'number') {
823
- deviceType = typedDevice.raw.deviceType;
824
- } else if (
825
- typedDevice.raw &&
826
- typeof typedDevice.raw.deviceType === 'string' &&
827
- typedDevice.raw.deviceType.trim() !== ''
828
- ) {
829
- const parsed = Number(typedDevice.raw.deviceType);
830
- if (!Number.isNaN(parsed)) {
831
- deviceType = parsed;
832
- }
833
- }
834
-
358
+ const deviceType = resolveDeviceType(device);
835
359
  const isDownlight = deviceType === 46;
836
360
 
837
361
  if (isDownlight) {
838
- this.log.info(
839
- 'Cync: configuring %s as Lightbulb (deviceType=%s, deviceId=%s)',
840
- deviceName,
841
- String(deviceType),
842
- deviceId,
843
- );
844
- 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
+ );
845
376
  } else {
846
- this.log.info(
847
- 'Cync: configuring %s as Switch (deviceType=%s, deviceId=%s)',
848
- deviceName,
849
- deviceType ?? 'unknown',
850
- deviceId,
851
- );
852
- 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
+ );
853
391
  }
854
392
  }
855
393
  }