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/CHANGELOG.md +9 -0
- package/README.md +7 -4
- package/dist/cync/config-client.d.ts +6 -0
- package/dist/cync/config-client.js +38 -0
- package/dist/cync/config-client.js.map +1 -1
- package/dist/cync/cync-client.d.ts +16 -11
- package/dist/cync/cync-client.js +175 -23
- package/dist/cync/cync-client.js.map +1 -1
- package/dist/cync/tcp-client.d.ts +45 -8
- package/dist/cync/tcp-client.js +359 -17
- package/dist/cync/tcp-client.js.map +1 -1
- package/dist/cync/token-store.d.ts +2 -0
- package/dist/cync/token-store.js.map +1 -1
- package/dist/platform.d.ts +5 -2
- package/dist/platform.js +103 -36
- package/dist/platform.js.map +1 -1
- package/docs/cync-api-notes.md +121 -7
- package/package.json +1 -1
- package/src/cync/config-client.ts +55 -0
- package/src/cync/cync-client.ts +221 -35
- package/src/cync/tcp-client.ts +488 -21
- package/src/cync/token-store.ts +3 -1
- package/src/platform.ts +176 -50
- package/homebridge-cync-app-v0.0.1.zip +0 -0
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
|
-
|
|
63
|
-
|
|
173
|
+
const cyncLogger = toCyncLogger(this.log);
|
|
174
|
+
const tcpClient = new TcpClient(cyncLogger);
|
|
175
|
+
|
|
64
176
|
this.client = new CyncClient(
|
|
65
|
-
new ConfigClient(
|
|
66
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 =
|
|
152
|
-
device.device_id ??
|
|
153
|
-
device.
|
|
154
|
-
device.
|
|
155
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
|
|
316
|
+
} else {
|
|
317
|
+
this.log.info('Cync: registering new accessory for %s (%s)', deviceName, uuidSeed);
|
|
171
318
|
|
|
172
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
321
|
+
this.api.registerPlatformAccessories(
|
|
322
|
+
'homebridge-cync-app',
|
|
323
|
+
'CyncAppPlatform',
|
|
324
|
+
[accessory],
|
|
325
|
+
);
|
|
204
326
|
|
|
205
|
-
|
|
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
|