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.
- package/.github/ISSUE_TEMPLATE/add-support-for-a-new-cync-device.md +139 -0
- package/CHANGELOG.md +21 -0
- package/README.md +3 -1
- package/dist/cync/config-client.d.ts +23 -0
- package/dist/cync/config-client.js +133 -0
- package/dist/cync/config-client.js.map +1 -1
- package/dist/cync/cync-accessory-helpers.d.ts +46 -0
- package/dist/cync/cync-accessory-helpers.js +138 -0
- package/dist/cync/cync-accessory-helpers.js.map +1 -0
- package/dist/cync/cync-client.d.ts +5 -0
- package/dist/cync/cync-client.js +156 -2
- package/dist/cync/cync-client.js.map +1 -1
- package/dist/cync/cync-light-accessory.d.ts +4 -0
- package/dist/cync/cync-light-accessory.js +197 -0
- package/dist/cync/cync-light-accessory.js.map +1 -0
- package/dist/cync/cync-switch-accessory.d.ts +4 -0
- package/dist/cync/cync-switch-accessory.js +58 -0
- package/dist/cync/cync-switch-accessory.js.map +1 -0
- package/dist/cync/device-catalog.d.ts +19 -0
- package/dist/cync/device-catalog.js +35 -0
- package/dist/cync/device-catalog.js.map +1 -0
- package/dist/cync/token-store.js +2 -2
- package/dist/cync/token-store.js.map +1 -1
- package/dist/platform.d.ts +8 -3
- package/dist/platform.js +49 -320
- package/dist/platform.js.map +1 -1
- package/package.json +1 -1
- package/src/cync/config-client.ts +192 -0
- package/src/cync/cync-accessory-helpers.ts +233 -0
- package/src/cync/cync-client.ts +238 -2
- package/src/cync/cync-light-accessory.ts +359 -0
- package/src/cync/cync-switch-accessory.ts +100 -0
- package/src/cync/device-catalog.ts +55 -0
- package/src/cync/token-store.ts +3 -2
- 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
|
|
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
|
-
|
|
18
|
-
type
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
}
|