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
@@ -0,0 +1,369 @@
1
+ // src/cync/cync-light-accessory.ts
2
+ import type { PlatformAccessory } from 'homebridge';
3
+ import type { CyncDevice, CyncDeviceMesh } from './config-client.js';
4
+ import type { CyncAccessoryContext, CyncAccessoryEnv } from './cync-accessory-helpers.js';
5
+ import { applyAccessoryInformationFromCyncDevice, hsvToRgb } from './cync-accessory-helpers.js';
6
+
7
+ export function configureCyncLightAccessory(
8
+ env: CyncAccessoryEnv,
9
+ mesh: CyncDeviceMesh,
10
+ device: CyncDevice,
11
+ accessory: PlatformAccessory,
12
+ deviceName: string,
13
+ deviceId: string,
14
+ ): void {
15
+ // If this accessory used to be a switch, remove that service
16
+ const existingSwitch = accessory.getService(env.api.hap.Service.Switch);
17
+ if (existingSwitch) {
18
+ env.log.info(
19
+ 'Cync: removing stale Switch service from %s (deviceId=%s) before configuring as Lightbulb',
20
+ deviceName,
21
+ deviceId,
22
+ );
23
+ accessory.removeService(existingSwitch);
24
+ }
25
+
26
+ const service =
27
+ accessory.getService(env.api.hap.Service.Lightbulb) ||
28
+ accessory.addService(env.api.hap.Service.Lightbulb, deviceName);
29
+
30
+ // Optionally update accessory category so UIs treat it as a light
31
+ if (accessory.category !== env.api.hap.Categories.LIGHTBULB) {
32
+ accessory.category = env.api.hap.Categories.LIGHTBULB;
33
+ }
34
+
35
+ // Populate Accessory Information from Cync metadata
36
+ applyAccessoryInformationFromCyncDevice(env.api, accessory, device, deviceName, deviceId);
37
+
38
+ // Ensure context is initialized
39
+ const ctx = accessory.context as CyncAccessoryContext;
40
+ ctx.cync = ctx.cync ?? {
41
+ meshId: mesh.id,
42
+ deviceId,
43
+ productId: device.product_id,
44
+ on: false,
45
+ };
46
+
47
+ // Remember mapping for LAN updates
48
+ env.registerAccessoryForDevice(deviceId, accessory);
49
+ env.markDeviceSeen(deviceId);
50
+ env.startPollingDevice(deviceId);
51
+
52
+ const Characteristic = env.api.hap.Characteristic;
53
+
54
+ // ----- On/Off -----
55
+ service
56
+ .getCharacteristic(Characteristic.On)
57
+ .onGet(() => {
58
+ const currentOn = !!ctx.cync?.on;
59
+
60
+ if (env.isDeviceProbablyOffline(deviceId)) {
61
+ env.log.debug(
62
+ 'Cync: Light On.get offline-heuristic hit; returning cached=%s for %s (deviceId=%s)',
63
+ String(currentOn),
64
+ deviceName,
65
+ deviceId,
66
+ );
67
+ return currentOn;
68
+ }
69
+
70
+ env.log.info(
71
+ 'Cync: Light On.get -> %s for %s (deviceId=%s)',
72
+ String(currentOn),
73
+ deviceName,
74
+ deviceId,
75
+ );
76
+
77
+ return currentOn;
78
+ })
79
+ .onSet(async (value) => {
80
+ const cyncMeta = ctx.cync;
81
+
82
+ if (!cyncMeta?.deviceId) {
83
+ env.log.warn(
84
+ 'Cync: Light On.set called for %s but no cync.deviceId in context',
85
+ deviceName,
86
+ );
87
+ return;
88
+ }
89
+
90
+ const on = value === true || value === 1;
91
+
92
+ env.log.info(
93
+ 'Cync: Light On.set -> %s for %s (deviceId=%s)',
94
+ String(on),
95
+ deviceName,
96
+ cyncMeta.deviceId,
97
+ );
98
+
99
+ // Optimistic local cache; LAN update will confirm
100
+ cyncMeta.on = on;
101
+
102
+ try {
103
+ await env.tcpClient.setSwitchState(cyncMeta.deviceId, { on });
104
+ env.markDeviceSeen(cyncMeta.deviceId);
105
+ } catch (err) {
106
+ env.log.warn(
107
+ 'Cync: Light On.set failed for %s (deviceId=%s): %s',
108
+ deviceName,
109
+ cyncMeta.deviceId,
110
+ (err as Error).message ?? String(err),
111
+ );
112
+
113
+ throw new env.api.hap.HapStatusError(
114
+ env.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
115
+ );
116
+ }
117
+ });
118
+
119
+ // ----- Brightness (dimming via LAN combo_control) -----
120
+ service
121
+ .getCharacteristic(Characteristic.Brightness)
122
+ .onGet(() => {
123
+ const current = ctx.cync?.brightness;
124
+
125
+ const cachedBrightness =
126
+ typeof current === 'number'
127
+ ? current
128
+ : (ctx.cync?.on ?? false) ? 100 : 0;
129
+
130
+ if (env.isDeviceProbablyOffline(deviceId)) {
131
+ env.log.debug(
132
+ 'Cync: Light Brightness.get offline-heuristic hit; returning cached=%d for %s (deviceId=%s)',
133
+ cachedBrightness,
134
+ deviceName,
135
+ deviceId,
136
+ );
137
+ return cachedBrightness;
138
+ }
139
+
140
+ return cachedBrightness;
141
+ })
142
+ .onSet(async (value) => {
143
+ const cyncMeta = ctx.cync;
144
+
145
+ if (!cyncMeta?.deviceId) {
146
+ env.log.warn(
147
+ 'Cync: Light Brightness.set called for %s but no cync.deviceId in context',
148
+ deviceName,
149
+ );
150
+ return;
151
+ }
152
+
153
+ const brightness = Math.max(0, Math.min(100, Number(value)));
154
+
155
+ if (!Number.isFinite(brightness)) {
156
+ env.log.warn(
157
+ 'Cync: Light Brightness.set received invalid value=%o for %s (deviceId=%s)',
158
+ value,
159
+ deviceName,
160
+ cyncMeta.deviceId,
161
+ );
162
+ return;
163
+ }
164
+
165
+ // Optimistic cache
166
+ cyncMeta.brightness = brightness;
167
+ cyncMeta.on = brightness > 0;
168
+
169
+ env.log.info(
170
+ 'Cync: Light Brightness.set -> %d for %s (deviceId=%s)',
171
+ brightness,
172
+ deviceName,
173
+ cyncMeta.deviceId,
174
+ );
175
+
176
+ try {
177
+ // If we're in "color mode", keep the existing RGB and scale brightness via setColor();
178
+ // otherwise treat this as a white-brightness change.
179
+ if (cyncMeta.colorActive && cyncMeta.rgb) {
180
+ await env.tcpClient.setColor(
181
+ cyncMeta.deviceId,
182
+ cyncMeta.rgb,
183
+ brightness,
184
+ );
185
+ } else {
186
+ await env.tcpClient.setBrightness(cyncMeta.deviceId, brightness);
187
+ }
188
+
189
+ env.markDeviceSeen(cyncMeta.deviceId);
190
+ } catch (err) {
191
+ env.log.warn(
192
+ 'Cync: Light Brightness.set failed for %s (deviceId=%s): %s',
193
+ deviceName,
194
+ cyncMeta.deviceId,
195
+ (err as Error).message ?? String(err),
196
+ );
197
+
198
+ throw new env.api.hap.HapStatusError(
199
+ env.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
200
+ );
201
+ }
202
+ });
203
+
204
+ // ----- Hue -----
205
+ service
206
+ .getCharacteristic(Characteristic.Hue)
207
+ .onGet(() => {
208
+ const hue = typeof ctx.cync?.hue === 'number' ? ctx.cync.hue : 0;
209
+
210
+ if (env.isDeviceProbablyOffline(deviceId)) {
211
+ env.log.debug(
212
+ 'Cync: Light Hue.get offline-heuristic hit; returning cached=%d for %s (deviceId=%s)',
213
+ hue,
214
+ deviceName,
215
+ deviceId,
216
+ );
217
+ }
218
+
219
+ return hue;
220
+ })
221
+ .onSet(async (value) => {
222
+ const cyncMeta = ctx.cync;
223
+
224
+ if (!cyncMeta?.deviceId) {
225
+ env.log.warn(
226
+ 'Cync: Light Hue.set called for %s but no cync.deviceId in context',
227
+ deviceName,
228
+ );
229
+ return;
230
+ }
231
+
232
+ const hue = Math.max(0, Math.min(360, Number(value)));
233
+ if (!Number.isFinite(hue)) {
234
+ env.log.warn(
235
+ 'Cync: Light Hue.set received invalid value=%o for %s (deviceId=%s)',
236
+ value,
237
+ deviceName,
238
+ cyncMeta.deviceId,
239
+ );
240
+ return;
241
+ }
242
+
243
+ // Use cached saturation/brightness if available, otherwise sane defaults
244
+ const saturation =
245
+ typeof cyncMeta.saturation === 'number' ? cyncMeta.saturation : 100;
246
+
247
+ const brightness =
248
+ typeof cyncMeta.brightness === 'number' ? cyncMeta.brightness : 100;
249
+
250
+ const rgb = hsvToRgb(hue, saturation, brightness);
251
+
252
+ // Optimistic cache
253
+ cyncMeta.hue = hue;
254
+ cyncMeta.saturation = saturation;
255
+ cyncMeta.rgb = rgb;
256
+ cyncMeta.colorActive = true;
257
+ cyncMeta.on = brightness > 0;
258
+ cyncMeta.brightness = brightness;
259
+
260
+ env.log.info(
261
+ 'Cync: Light Hue.set -> %d for %s (deviceId=%s) -> rgb=(%d,%d,%d) brightness=%d',
262
+ hue,
263
+ deviceName,
264
+ cyncMeta.deviceId,
265
+ rgb.r,
266
+ rgb.g,
267
+ rgb.b,
268
+ brightness,
269
+ );
270
+
271
+ try {
272
+ await env.tcpClient.setColor(cyncMeta.deviceId, rgb, brightness);
273
+ env.markDeviceSeen(cyncMeta.deviceId);
274
+ } catch (err) {
275
+ env.log.warn(
276
+ 'Cync: Light Hue.set failed for %s (deviceId=%s): %s',
277
+ deviceName,
278
+ cyncMeta.deviceId,
279
+ (err as Error).message ?? String(err),
280
+ );
281
+
282
+ throw new env.api.hap.HapStatusError(
283
+ env.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
284
+ );
285
+ }
286
+ });
287
+
288
+ // ----- Saturation -----
289
+ service
290
+ .getCharacteristic(Characteristic.Saturation)
291
+ .onGet(() => {
292
+ const sat = typeof ctx.cync?.saturation === 'number' ? ctx.cync.saturation : 100;
293
+
294
+ if (env.isDeviceProbablyOffline(deviceId)) {
295
+ env.log.debug(
296
+ 'Cync: Light Saturation.get offline-heuristic hit; returning cached=%d for %s (deviceId=%s)',
297
+ sat,
298
+ deviceName,
299
+ deviceId,
300
+ );
301
+ }
302
+
303
+ return sat;
304
+ })
305
+ .onSet(async (value) => {
306
+ const cyncMeta = ctx.cync;
307
+
308
+ if (!cyncMeta?.deviceId) {
309
+ env.log.warn(
310
+ 'Cync: Light Saturation.set called for %s but no cync.deviceId in context',
311
+ deviceName,
312
+ );
313
+ return;
314
+ }
315
+
316
+ const saturation = Math.max(0, Math.min(100, Number(value)));
317
+ if (!Number.isFinite(saturation)) {
318
+ env.log.warn(
319
+ 'Cync: Light Saturation.set received invalid value=%o for %s (deviceId=%s)',
320
+ value,
321
+ deviceName,
322
+ cyncMeta.deviceId,
323
+ );
324
+ return;
325
+ }
326
+
327
+ const hue = typeof cyncMeta.hue === 'number' ? cyncMeta.hue : 0;
328
+
329
+ const brightness =
330
+ typeof cyncMeta.brightness === 'number' ? cyncMeta.brightness : 100;
331
+
332
+ const rgb = hsvToRgb(hue, saturation, brightness);
333
+
334
+ // Optimistic cache
335
+ cyncMeta.hue = hue;
336
+ cyncMeta.saturation = saturation;
337
+ cyncMeta.rgb = rgb;
338
+ cyncMeta.colorActive = true;
339
+ cyncMeta.on = brightness > 0;
340
+ cyncMeta.brightness = brightness;
341
+
342
+ env.log.info(
343
+ 'Cync: Light Saturation.set -> %d for %s (deviceId=%s) -> rgb=(%d,%d,%d) brightness=%d',
344
+ saturation,
345
+ deviceName,
346
+ cyncMeta.deviceId,
347
+ rgb.r,
348
+ rgb.g,
349
+ rgb.b,
350
+ brightness,
351
+ );
352
+
353
+ try {
354
+ await env.tcpClient.setColor(cyncMeta.deviceId, rgb, brightness);
355
+ env.markDeviceSeen(cyncMeta.deviceId);
356
+ } catch (err) {
357
+ env.log.warn(
358
+ 'Cync: Light Saturation.set failed for %s (deviceId=%s): %s',
359
+ deviceName,
360
+ cyncMeta.deviceId,
361
+ (err as Error).message ?? String(err),
362
+ );
363
+
364
+ throw new env.api.hap.HapStatusError(
365
+ env.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
366
+ );
367
+ }
368
+ });
369
+ }
@@ -0,0 +1,119 @@
1
+ // src/cync/cync-switch-accessory.ts
2
+ import type { PlatformAccessory } from 'homebridge';
3
+ import type { CyncDevice, CyncDeviceMesh } from './config-client.js';
4
+ import type { CyncAccessoryContext, CyncAccessoryEnv } from './cync-accessory-helpers.js';
5
+ import { applyAccessoryInformationFromCyncDevice } from './cync-accessory-helpers.js';
6
+
7
+ export function configureCyncSwitchAccessory(
8
+ env: CyncAccessoryEnv,
9
+ mesh: CyncDeviceMesh,
10
+ device: CyncDevice,
11
+ accessory: PlatformAccessory,
12
+ deviceName: string,
13
+ deviceId: string,
14
+ ): void {
15
+ const service =
16
+ accessory.getService(env.api.hap.Service.Switch) ||
17
+ accessory.addService(env.api.hap.Service.Switch, deviceName);
18
+
19
+ const existingLight = accessory.getService(env.api.hap.Service.Lightbulb);
20
+ if (existingLight) {
21
+ env.log.info(
22
+ 'Cync: removing stale Lightbulb service from %s (deviceId=%s) before configuring as Switch',
23
+ deviceName,
24
+ deviceId,
25
+ );
26
+ accessory.removeService(existingLight);
27
+ }
28
+
29
+ applyAccessoryInformationFromCyncDevice(env.api, accessory, device, deviceName, deviceId);
30
+
31
+ // Ensure context is initialized
32
+ const ctx = accessory.context as CyncAccessoryContext;
33
+ ctx.cync = ctx.cync ?? {
34
+ meshId: mesh.id,
35
+ deviceId,
36
+ productId: device.product_id,
37
+ on: false,
38
+ };
39
+
40
+ // Remember mapping for LAN updates
41
+ env.registerAccessoryForDevice(deviceId, accessory);
42
+ env.markDeviceSeen(deviceId);
43
+ env.startPollingDevice(deviceId);
44
+
45
+ service
46
+ .getCharacteristic(env.api.hap.Characteristic.On)
47
+ .onGet(() => {
48
+ const currentOn = !!ctx.cync?.on;
49
+
50
+ if (env.isDeviceProbablyOffline(deviceId)) {
51
+ env.log.debug(
52
+ 'Cync: Switch On.get offline-heuristic hit; returning cached=%s for %s (deviceId=%s)',
53
+ String(currentOn),
54
+ deviceName,
55
+ deviceId,
56
+ );
57
+ return currentOn;
58
+ }
59
+
60
+ env.log.info(
61
+ 'Cync: On.get -> %s for %s (deviceId=%s)',
62
+ String(currentOn),
63
+ deviceName,
64
+ deviceId,
65
+ );
66
+
67
+ return currentOn;
68
+ })
69
+ .onSet(async (value) => {
70
+ const cyncMeta = ctx.cync;
71
+
72
+ if (!cyncMeta?.deviceId) {
73
+ env.log.warn(
74
+ 'Cync: Switch On.set called for %s but no cync.deviceId in context',
75
+ deviceName,
76
+ );
77
+ return;
78
+ }
79
+
80
+ const on = value === true || value === 1;
81
+
82
+ env.log.info(
83
+ 'Cync: Switch On.set -> %s for %s (deviceId=%s)',
84
+ String(on),
85
+ deviceName,
86
+ cyncMeta.deviceId,
87
+ );
88
+
89
+ // Optimistic cache
90
+ cyncMeta.on = on;
91
+
92
+ try {
93
+ if (!on) {
94
+ await env.tcpClient.setSwitchState(cyncMeta.deviceId, { on: false });
95
+ env.markDeviceSeen(cyncMeta.deviceId);
96
+ return;
97
+ }
98
+
99
+ if (cyncMeta.colorActive && cyncMeta.rgb && typeof cyncMeta.brightness === 'number') {
100
+ await env.tcpClient.setColor(cyncMeta.deviceId, cyncMeta.rgb, cyncMeta.brightness);
101
+ } else {
102
+ await env.tcpClient.setSwitchState(cyncMeta.deviceId, { on: true });
103
+ }
104
+
105
+ env.markDeviceSeen(cyncMeta.deviceId);
106
+ } catch (err) {
107
+ env.log.warn(
108
+ 'Cync: Switch On.set failed for %s (deviceId=%s): %s',
109
+ deviceName,
110
+ cyncMeta.deviceId,
111
+ (err as Error).message ?? String(err),
112
+ );
113
+
114
+ throw new env.api.hap.HapStatusError(
115
+ env.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
116
+ );
117
+ }
118
+ });
119
+ }
@@ -30,22 +30,27 @@ export const DEVICE_CATALOG: Record<number, CyncDeviceModel> = {
30
30
  marketingName: 'Cync reveal HD+',
31
31
  // defaultCategory: Categories.LIGHTBULB,
32
32
  },
33
+ // Legacy C by GE On/Off Smart Plug — original hardware
33
34
  64: {
34
35
  deviceType: 64,
35
- modelName: 'Indoor Smart Plug',
36
- marketingName: 'On/Off Smart Plug',
36
+ modelName: 'Indoor Smart Plug (CPLGSTDBLW1)',
37
+ marketingName: 'On/Off Smart Plug (CPLGSTDBLW1)',
38
+ notes: 'Legacy C by GE plug. FCC ID PUU-CPLGSTDBLW1. Original hardware revision. Final firmware 1.x.',
37
39
  // defaultCategory: Categories.OUTLET,
38
40
  },
41
+ // Legacy C by GE On/Off Smart Plug — revised hardware ("T" revision)
39
42
  65: {
40
43
  deviceType: 65,
41
- modelName: 'Indoor Smart Plug',
42
- marketingName: 'Cync Indoor Plug',
44
+ modelName: 'Indoor Smart Plug (CPLGSTDBLW1-T)',
45
+ marketingName: 'On/Off Smart Plug (CPLGSTDBLW1-T)',
46
+ notes: 'Legacy C by GE plug. FCC ID PUU-CPLGSTDBLW1T / HVIN CPLGSTDBLW1T. Revised hardware. Final firmware 2.x.',
43
47
  // defaultCategory: Categories.OUTLET,
44
48
  },
45
49
  172: {
46
50
  deviceType: 172,
47
51
  modelName: 'Indoor Smart Plug (3in1)',
48
52
  marketingName: 'Cync Indoor Smart Plug',
53
+ notes: 'Matter-capable hardware. Replaces legacy C by GE On/Off Smart Plug.',
49
54
  // defaultCategory: Categories.OUTLET,
50
55
  },
51
56
  };