homebridge-cync-app 0.0.2 → 0.1.0

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/src/platform.ts CHANGED
@@ -10,7 +10,7 @@ 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 } from './cync/config-client.js';
13
+ import type { CyncCloudConfig, CyncDevice, CyncDeviceMesh } 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
16
 
@@ -26,6 +26,7 @@ interface CyncAccessoryContext {
26
26
  meshId: string;
27
27
  deviceId: string;
28
28
  productId?: string;
29
+ on?: boolean;
29
30
  };
30
31
  [key: string]: unknown;
31
32
  }
@@ -45,8 +46,118 @@ export class CyncAppPlatform implements DynamicPlatformPlugin {
45
46
  private readonly api: API;
46
47
  private readonly config: PlatformConfig;
47
48
  private readonly client: CyncClient;
49
+ private readonly tcpClient: TcpClient;
48
50
 
49
51
  private cloudConfig: CyncCloudConfig | null = null;
52
+ private readonly deviceIdToAccessory = new Map<string, PlatformAccessory>();
53
+ private handleLanUpdate(update: unknown): void {
54
+ // We only care about parsed 0x83 frames that look like:
55
+ // { controllerId: number, on: boolean, level: number, deviceId?: string }
56
+ const payload = update as { deviceId?: string; on?: boolean };
57
+
58
+ if (!payload || typeof payload.deviceId !== 'string' || typeof payload.on !== 'boolean') {
59
+ return;
60
+ }
61
+
62
+ const accessory = this.deviceIdToAccessory.get(payload.deviceId);
63
+ if (!accessory) {
64
+ this.log.debug(
65
+ 'Cync: LAN update for unknown deviceId=%s; no accessory mapping',
66
+ payload.deviceId,
67
+ );
68
+ return;
69
+ }
70
+
71
+ const service = accessory.getService(this.api.hap.Service.Switch);
72
+ if (!service) {
73
+ this.log.debug(
74
+ 'Cync: accessory %s has no Switch service for deviceId=%s',
75
+ accessory.displayName,
76
+ payload.deviceId,
77
+ );
78
+ return;
79
+ }
80
+
81
+ // Update cached context state
82
+ const ctx = accessory.context as CyncAccessoryContext;
83
+ ctx.cync = ctx.cync ?? {
84
+ meshId: '',
85
+ deviceId: payload.deviceId,
86
+ };
87
+ ctx.cync.on = payload.on;
88
+
89
+ this.log.info(
90
+ 'Cync: LAN update -> %s is now %s (deviceId=%s)',
91
+ accessory.displayName,
92
+ payload.on ? 'ON' : 'OFF',
93
+ payload.deviceId,
94
+ );
95
+
96
+ // Push the new state into HomeKit
97
+ service.updateCharacteristic(this.api.hap.Characteristic.On, payload.on);
98
+ }
99
+
100
+ private configureCyncSwitchAccessory(
101
+ mesh: CyncDeviceMesh,
102
+ device: CyncDevice,
103
+ accessory: PlatformAccessory,
104
+ deviceName: string,
105
+ deviceId: string,
106
+ ): void {
107
+ const service =
108
+ accessory.getService(this.api.hap.Service.Switch) ||
109
+ accessory.addService(this.api.hap.Service.Switch, deviceName);
110
+
111
+ // Ensure context is initialized
112
+ const ctx = accessory.context as CyncAccessoryContext;
113
+ ctx.cync = ctx.cync ?? {
114
+ meshId: mesh.id,
115
+ deviceId,
116
+ productId: device.product_id,
117
+ on: false,
118
+ };
119
+
120
+ // Remember mapping for LAN updates
121
+ this.deviceIdToAccessory.set(deviceId, accessory);
122
+
123
+ service
124
+ .getCharacteristic(this.api.hap.Characteristic.On)
125
+ .onGet(() => {
126
+ const currentOn = !!ctx.cync?.on;
127
+ this.log.info(
128
+ 'Cync: On.get -> %s for %s (deviceId=%s)',
129
+ String(currentOn),
130
+ deviceName,
131
+ deviceId,
132
+ );
133
+ return currentOn;
134
+ })
135
+ .onSet(async (value) => {
136
+ const cyncMeta = ctx.cync;
137
+
138
+ if (!cyncMeta?.deviceId) {
139
+ this.log.warn(
140
+ 'Cync: On.set called for %s but no cync.deviceId in context',
141
+ deviceName,
142
+ );
143
+ return;
144
+ }
145
+
146
+ const on = value === true || value === 1;
147
+
148
+ this.log.info(
149
+ 'Cync: On.set -> %s for %s (deviceId=%s)',
150
+ String(on),
151
+ deviceName,
152
+ cyncMeta.deviceId,
153
+ );
154
+
155
+ // Optimistic local cache; LAN update will confirm
156
+ cyncMeta.on = on;
157
+
158
+ await this.tcpClient.setSwitchState(cyncMeta.deviceId, { on });
159
+ });
160
+ }
50
161
 
51
162
  constructor(log: Logger, config: PlatformConfig, api: API) {
52
163
  this.log = log;
@@ -59,20 +170,28 @@ export class CyncAppPlatform implements DynamicPlatformPlugin {
59
170
  const password = cfg.password as string | undefined;
60
171
  const twoFactor = cfg.twoFactor as string | undefined;
61
172
 
62
- // Initialize the Cync client with platform logger so all messages
63
- // appear in the Homebridge log.
173
+ const cyncLogger = toCyncLogger(this.log);
174
+ const tcpClient = new TcpClient(cyncLogger);
175
+
64
176
  this.client = new CyncClient(
65
- new ConfigClient(toCyncLogger(this.log)),
66
- new TcpClient(toCyncLogger(this.log)),
177
+ new ConfigClient(cyncLogger),
178
+ tcpClient,
67
179
  {
68
180
  email: username ?? '',
69
181
  password: password ?? '',
70
182
  twoFactor,
71
183
  },
72
184
  this.api.user.storagePath(),
73
- toCyncLogger(this.log),
185
+ cyncLogger,
74
186
  );
75
187
 
188
+ this.tcpClient = tcpClient;
189
+
190
+ // Bridge LAN updates into Homebridge
191
+ this.client.onLanDeviceUpdate((update) => {
192
+ this.handleLanUpdate(update);
193
+ });
194
+
76
195
  this.log.info(this.config.name ?? PLATFORM_NAME, 'initialized');
77
196
 
78
197
  this.api.on('didFinishLaunching', () => {
@@ -114,10 +233,37 @@ export class CyncAppPlatform implements DynamicPlatformPlugin {
114
233
 
115
234
  this.log.info(
116
235
  'Cync: cloud configuration loaded; mesh count=%d',
117
- cloudConfig?.meshes?.length ?? 0,
236
+ cloudConfig.meshes.length,
118
237
  );
119
238
 
239
+ // Ask the CyncClient for the LAN login code derived from stored session.
240
+ // If it returns an empty blob, LAN is disabled but cloud still works.
241
+ let loginCode: Uint8Array = new Uint8Array();
242
+ try {
243
+ loginCode = this.client.getLanLoginCode();
244
+ } catch (err) {
245
+ this.log.warn(
246
+ 'Cync: getLanLoginCode() failed: %s',
247
+ (err as Error).message ?? String(err),
248
+ );
249
+ }
250
+
251
+ if (loginCode.length > 0) {
252
+ this.log.info(
253
+ 'Cync: LAN login code available (%d bytes); starting TCP transport…',
254
+ loginCode.length,
255
+ );
256
+
257
+ // ### 🧩 LAN Transport Bootstrap: wire frame listeners via CyncClient
258
+ await this.client.startTransport(cloudConfig, loginCode);
259
+ } else {
260
+ this.log.info(
261
+ 'Cync: LAN login code unavailable; TCP control disabled (cloud-only).',
262
+ );
263
+ }
264
+
120
265
  this.discoverDevices(cloudConfig);
266
+
121
267
  } catch (err) {
122
268
  this.log.error(
123
269
  'Cync: cloud login failed: %s',
@@ -128,8 +274,7 @@ export class CyncAppPlatform implements DynamicPlatformPlugin {
128
274
 
129
275
  /**
130
276
  * Discover devices from the Cync cloud config and register them as
131
- * Homebridge accessories. For now, each device is exposed as a simple
132
- * dummy Switch that logs state changes.
277
+ * Homebridge accessories.
133
278
  */
134
279
  private discoverDevices(cloudConfig: CyncCloudConfig): void {
135
280
  if (!cloudConfig.meshes?.length) {
@@ -148,11 +293,12 @@ export class CyncAppPlatform implements DynamicPlatformPlugin {
148
293
  }
149
294
 
150
295
  for (const device of devices) {
151
- const deviceId = `${device.id ??
152
- device.device_id ??
153
- device.mac ??
154
- device.sn ??
155
- `${mesh.id}-${device.product_id ?? 'unknown'}`}`;
296
+ const deviceId =
297
+ (device.device_id as string | undefined) ??
298
+ (device.id as string) ??
299
+ (device.mac as string | undefined) ??
300
+ (device.sn as string | undefined) ??
301
+ `${mesh.id}-${device.product_id ?? 'unknown'}`;
156
302
 
157
303
  const preferredName =
158
304
  (device.name as string | undefined) ??
@@ -163,47 +309,27 @@ export class CyncAppPlatform implements DynamicPlatformPlugin {
163
309
  const uuidSeed = `cync-${mesh.id}-${deviceId}`;
164
310
  const uuid = this.api.hap.uuid.generate(uuidSeed);
165
311
 
166
- const existing = this.accessories.find(acc => acc.UUID === uuid);
167
- if (existing) {
312
+ let accessory = this.accessories.find(acc => acc.UUID === uuid);
313
+
314
+ if (accessory) {
168
315
  this.log.info('Cync: using cached accessory for %s (%s)', deviceName, uuidSeed);
169
- continue;
170
- }
316
+ } else {
317
+ this.log.info('Cync: registering new accessory for %s (%s)', deviceName, uuidSeed);
171
318
 
172
- this.log.info('Cync: registering new accessory for %s (%s)', deviceName, uuidSeed);
173
-
174
- const accessory = new this.api.platformAccessory(deviceName, uuid);
175
-
176
- // Simple Switch service for now
177
- const service =
178
- accessory.getService(this.api.hap.Service.Switch) ||
179
- accessory.addService(this.api.hap.Service.Switch, deviceName);
180
-
181
- service
182
- .getCharacteristic(this.api.hap.Characteristic.On)
183
- .onGet(() => {
184
- this.log.info('Cync: On.get -> false for %s', deviceName);
185
- return false;
186
- })
187
- .onSet((value) => {
188
- this.log.info('Cync: On.set -> %s for %s', String(value), deviceName);
189
- });
190
-
191
- // Context for later TCP control
192
- const ctx = accessory.context as CyncAccessoryContext;
193
- ctx.cync = {
194
- meshId: mesh.id,
195
- deviceId,
196
- productId: device.product_id,
197
- };
319
+ accessory = new this.api.platformAccessory(deviceName, uuid);
198
320
 
199
- this.api.registerPlatformAccessories(
200
- 'homebridge-cync-app',
201
- 'CyncAppPlatform',
202
- [accessory],
203
- );
321
+ this.api.registerPlatformAccessories(
322
+ 'homebridge-cync-app',
323
+ 'CyncAppPlatform',
324
+ [accessory],
325
+ );
204
326
 
205
- this.accessories.push(accessory);
327
+ this.accessories.push(accessory);
328
+ }
329
+
330
+ this.configureCyncSwitchAccessory(mesh, device, accessory, deviceName, deviceId);
206
331
  }
207
332
  }
208
333
  }
334
+
209
335
  }
Binary file