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.
- package/CHANGELOG.md +13 -0
- package/dist/cync/config-client.d.ts +11 -0
- package/dist/cync/config-client.js +113 -6
- 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 +140 -0
- package/dist/cync/cync-accessory-helpers.js.map +1 -0
- package/dist/cync/cync-client.d.ts +4 -0
- package/dist/cync/cync-client.js +150 -34
- 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 +190 -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 +64 -0
- package/dist/cync/cync-switch-accessory.js.map +1 -0
- package/dist/cync/device-catalog.js +9 -4
- package/dist/cync/device-catalog.js.map +1 -1
- package/dist/cync/tcp-client.d.ts +7 -0
- package/dist/cync/tcp-client.js +122 -30
- package/dist/cync/tcp-client.js.map +1 -1
- package/dist/cync/token-store.js +2 -2
- package/dist/cync/token-store.js.map +1 -1
- package/dist/platform.d.ts +1 -3
- package/dist/platform.js +18 -382
- package/dist/platform.js.map +1 -1
- package/package.json +1 -1
- package/src/cync/config-client.ts +175 -12
- package/src/cync/cync-accessory-helpers.ts +233 -0
- package/src/cync/cync-client.ts +231 -44
- package/src/cync/cync-light-accessory.ts +369 -0
- package/src/cync/cync-switch-accessory.ts +119 -0
- package/src/cync/device-catalog.ts +9 -4
- package/src/cync/tcp-client.ts +153 -53
- package/src/cync/token-store.ts +3 -2
- package/src/platform.ts +49 -661
package/src/cync/tcp-client.ts
CHANGED
|
@@ -14,7 +14,11 @@ const defaultLogger: CyncLogger = {
|
|
|
14
14
|
export type DeviceUpdateCallback = (payload: unknown) => void;
|
|
15
15
|
export type RawFrameListener = (frame: Buffer) => void;
|
|
16
16
|
|
|
17
|
+
type TransportMode = 'tls_strict' | 'tls_relaxed' | 'tcp';
|
|
18
|
+
|
|
17
19
|
export class TcpClient {
|
|
20
|
+
private transportMode: TransportMode | null = null;
|
|
21
|
+
|
|
18
22
|
public registerSwitchMapping(controllerId: number, deviceId: string): void {
|
|
19
23
|
if (!Number.isFinite(controllerId)) {
|
|
20
24
|
return;
|
|
@@ -38,6 +42,10 @@ export class TcpClient {
|
|
|
38
42
|
private heartbeatTimer: NodeJS.Timeout | null = null;
|
|
39
43
|
private rawFrameListeners: RawFrameListener[] = [];
|
|
40
44
|
private controllerToDevice = new Map<number, string>();
|
|
45
|
+
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
46
|
+
private reconnectAttempt = 0;
|
|
47
|
+
private connectInFlight: Promise<void> | null = null;
|
|
48
|
+
private shuttingDown = false;
|
|
41
49
|
|
|
42
50
|
private enqueueCommand<T>(fn: () => Promise<T>): Promise<T> {
|
|
43
51
|
let resolveWrapper: (value: T | PromiseLike<T>) => void;
|
|
@@ -159,6 +167,7 @@ export class TcpClient {
|
|
|
159
167
|
loginCode: Uint8Array,
|
|
160
168
|
config: CyncCloudConfig,
|
|
161
169
|
): Promise<void> {
|
|
170
|
+
this.shuttingDown = false;
|
|
162
171
|
this.loginCode = loginCode;
|
|
163
172
|
this.config = config;
|
|
164
173
|
|
|
@@ -168,10 +177,6 @@ export class TcpClient {
|
|
|
168
177
|
);
|
|
169
178
|
return;
|
|
170
179
|
}
|
|
171
|
-
|
|
172
|
-
// Optional eager connect at startup; failures are logged and we rely
|
|
173
|
-
// on ensureConnected() to reconnect on demand later.
|
|
174
|
-
await this.ensureConnected();
|
|
175
180
|
}
|
|
176
181
|
|
|
177
182
|
public applyLanTopology(topology: {
|
|
@@ -201,16 +206,55 @@ export class TcpClient {
|
|
|
201
206
|
}
|
|
202
207
|
|
|
203
208
|
if (!this.loginCode || !this.loginCode.length || !this.config) {
|
|
204
|
-
this.log.warn(
|
|
205
|
-
'[Cync TCP] ensureConnected() called without loginCode/config; cannot open socket.',
|
|
206
|
-
);
|
|
209
|
+
this.log.warn('[Cync TCP] ensureConnected() called without loginCode/config; cannot open socket.');
|
|
207
210
|
return false;
|
|
208
211
|
}
|
|
209
212
|
|
|
210
|
-
|
|
213
|
+
if (this.connectInFlight) {
|
|
214
|
+
await this.connectInFlight;
|
|
215
|
+
return !!(this.socket && !this.socket.destroyed);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.connectInFlight = this.establishSocket()
|
|
219
|
+
.finally(() => {
|
|
220
|
+
this.connectInFlight = null;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await this.connectInFlight;
|
|
211
224
|
return !!(this.socket && !this.socket.destroyed);
|
|
212
225
|
}
|
|
213
226
|
|
|
227
|
+
private scheduleReconnect(reason: string): void {
|
|
228
|
+
if (this.shuttingDown) {
|
|
229
|
+
this.log.debug('[Cync TCP] Not scheduling reconnect (shutting down): %s', reason);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (this.reconnectTimer) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!this.loginCode || !this.loginCode.length || !this.config) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const attempt = this.reconnectAttempt;
|
|
242
|
+
const delayMs = Math.min(30_000, 1_000 * Math.pow(2, attempt));
|
|
243
|
+
this.reconnectAttempt++;
|
|
244
|
+
|
|
245
|
+
this.log.debug('[Cync TCP] Scheduling reconnect in %dms (%s)', delayMs, reason);
|
|
246
|
+
|
|
247
|
+
this.reconnectTimer = setTimeout(() => {
|
|
248
|
+
this.reconnectTimer = null;
|
|
249
|
+
|
|
250
|
+
// Fire and forget; ensureConnected() logs failures already
|
|
251
|
+
void this.ensureConnected().catch((err: unknown) => {
|
|
252
|
+
this.log.debug('[Cync TCP] Reconnect attempt failed: %s', String(err));
|
|
253
|
+
this.scheduleReconnect('retry');
|
|
254
|
+
});
|
|
255
|
+
}, delayMs);
|
|
256
|
+
}
|
|
257
|
+
|
|
214
258
|
private async establishSocket(): Promise<void> {
|
|
215
259
|
const host = 'cm.gelighting.com';
|
|
216
260
|
const portTLS = 23779;
|
|
@@ -219,34 +263,36 @@ export class TcpClient {
|
|
|
219
263
|
this.log.info('[Cync TCP] Connecting to %s…', host);
|
|
220
264
|
|
|
221
265
|
let socket: net.Socket | null = null;
|
|
222
|
-
|
|
266
|
+
if (this.socket) {
|
|
267
|
+
this.cleanupSocket(this.socket);
|
|
268
|
+
this.socket.destroy();
|
|
269
|
+
this.socket = null;
|
|
270
|
+
}
|
|
223
271
|
try {
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
socket = await this.openTlsSocket(host, portTLS,
|
|
227
|
-
}
|
|
228
|
-
this.
|
|
272
|
+
// If we already learned the best mode, reuse it.
|
|
273
|
+
if (this.transportMode === 'tls_relaxed') {
|
|
274
|
+
socket = await this.openTlsSocket(host, portTLS, false);
|
|
275
|
+
} else if (this.transportMode === 'tcp') {
|
|
276
|
+
socket = await this.openTcpSocket(host, portTCP);
|
|
277
|
+
} else {
|
|
278
|
+
// Default path: strict once, then downgrade and remember.
|
|
229
279
|
try {
|
|
230
|
-
socket = await this.openTlsSocket(host, portTLS,
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
280
|
+
socket = await this.openTlsSocket(host, portTLS, true);
|
|
281
|
+
this.transportMode = 'tls_strict';
|
|
282
|
+
} catch {
|
|
283
|
+
this.log.debug('[Cync TCP] TLS strict failed; trying relaxed TLS…');
|
|
284
|
+
try {
|
|
285
|
+
socket = await this.openTlsSocket(host, portTLS, false);
|
|
286
|
+
this.transportMode = 'tls_relaxed';
|
|
287
|
+
} catch {
|
|
288
|
+
this.log.debug('[Cync TCP] TLS relaxed failed; falling back to plain TCP…');
|
|
289
|
+
socket = await this.openTcpSocket(host, portTCP);
|
|
290
|
+
this.transportMode = 'tcp';
|
|
291
|
+
}
|
|
236
292
|
}
|
|
237
293
|
}
|
|
238
294
|
} catch (err) {
|
|
239
|
-
this.log.error(
|
|
240
|
-
'[Cync TCP] Failed to connect to %s: %s',
|
|
241
|
-
host,
|
|
242
|
-
String(err),
|
|
243
|
-
);
|
|
244
|
-
this.socket = null;
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (!socket) {
|
|
249
|
-
this.log.error('[Cync TCP] Socket is null after connect attempts.');
|
|
295
|
+
this.log.error('[Cync TCP] Failed to connect to %s: %s', host, String(err));
|
|
250
296
|
this.socket = null;
|
|
251
297
|
return;
|
|
252
298
|
}
|
|
@@ -254,41 +300,68 @@ export class TcpClient {
|
|
|
254
300
|
this.socket = socket;
|
|
255
301
|
this.attachSocketListeners(this.socket);
|
|
256
302
|
|
|
257
|
-
// Send loginCode immediately, as HA does.
|
|
258
303
|
if (this.loginCode && this.loginCode.length > 0) {
|
|
259
304
|
this.socket.write(Buffer.from(this.loginCode));
|
|
260
|
-
this.log.info(
|
|
261
|
-
'[Cync TCP] Login code sent (%d bytes).',
|
|
262
|
-
this.loginCode.length,
|
|
263
|
-
);
|
|
305
|
+
this.log.info('[Cync TCP] Login code sent (%d bytes).', this.loginCode.length);
|
|
264
306
|
} else {
|
|
265
|
-
this.log.warn(
|
|
266
|
-
'[Cync TCP] establishSocket() reached with no loginCode; skipping auth write.',
|
|
267
|
-
);
|
|
307
|
+
this.log.warn('[Cync TCP] establishSocket() reached with no loginCode; skipping auth write.');
|
|
268
308
|
}
|
|
269
309
|
|
|
270
|
-
// Start heartbeat: every 180 seconds send d3 00 00 00 00
|
|
271
310
|
this.startHeartbeat();
|
|
311
|
+
this.reconnectAttempt = 0;
|
|
312
|
+
if (this.reconnectTimer) {
|
|
313
|
+
clearTimeout(this.reconnectTimer);
|
|
314
|
+
this.reconnectTimer = null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private cleanupSocket(sock: net.Socket | null): void {
|
|
319
|
+
if (!sock) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
sock.removeAllListeners('data');
|
|
324
|
+
sock.removeAllListeners('close');
|
|
325
|
+
sock.removeAllListeners('error');
|
|
326
|
+
|
|
327
|
+
// Note: caller decides whether to destroy()
|
|
272
328
|
}
|
|
273
329
|
|
|
274
330
|
private openTlsSocket(host: string, port: number, strict: boolean): Promise<net.Socket> {
|
|
275
331
|
return new Promise((resolve, reject) => {
|
|
276
|
-
const sock = tls.connect(
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
)
|
|
284
|
-
|
|
332
|
+
const sock = tls.connect({ host, port, rejectUnauthorized: strict });
|
|
333
|
+
|
|
334
|
+
const onError = (err: Error) => {
|
|
335
|
+
// 'secureConnect' is registered with once(); no need to remove it here.
|
|
336
|
+
reject(err);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const onSecure = () => {
|
|
340
|
+
sock.removeListener('error', onError);
|
|
341
|
+
resolve(sock);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
sock.once('error', onError);
|
|
345
|
+
sock.once('secureConnect', onSecure);
|
|
285
346
|
});
|
|
286
347
|
}
|
|
287
348
|
|
|
288
349
|
private openTcpSocket(host: string, port: number): Promise<net.Socket> {
|
|
289
350
|
return new Promise((resolve, reject) => {
|
|
290
|
-
const sock = net.createConnection({ host, port }
|
|
291
|
-
|
|
351
|
+
const sock = net.createConnection({ host, port });
|
|
352
|
+
|
|
353
|
+
const onError = (err: Error) => {
|
|
354
|
+
// 'connect' is registered with once(); no need to remove it here.
|
|
355
|
+
reject(err);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const onConnect = () => {
|
|
359
|
+
sock.removeListener('error', onError);
|
|
360
|
+
resolve(sock);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
sock.once('error', onError);
|
|
364
|
+
sock.once('connect', onConnect);
|
|
292
365
|
});
|
|
293
366
|
}
|
|
294
367
|
|
|
@@ -418,11 +491,21 @@ export class TcpClient {
|
|
|
418
491
|
}
|
|
419
492
|
public async disconnect(): Promise<void> {
|
|
420
493
|
this.log.info('[Cync TCP] disconnect() called.');
|
|
494
|
+
this.shuttingDown = true;
|
|
495
|
+
|
|
496
|
+
if (this.reconnectTimer) {
|
|
497
|
+
clearTimeout(this.reconnectTimer);
|
|
498
|
+
this.reconnectTimer = null;
|
|
499
|
+
}
|
|
500
|
+
this.reconnectAttempt = 0;
|
|
501
|
+
|
|
421
502
|
if (this.heartbeatTimer) {
|
|
422
503
|
clearInterval(this.heartbeatTimer);
|
|
423
504
|
this.heartbeatTimer = null;
|
|
424
505
|
}
|
|
506
|
+
|
|
425
507
|
if (this.socket) {
|
|
508
|
+
this.cleanupSocket(this.socket);
|
|
426
509
|
this.socket.destroy();
|
|
427
510
|
this.socket = null;
|
|
428
511
|
}
|
|
@@ -674,7 +757,23 @@ export class TcpClient {
|
|
|
674
757
|
|
|
675
758
|
socket.on('close', () => {
|
|
676
759
|
this.log.warn('[Cync TCP] Socket closed.');
|
|
677
|
-
|
|
760
|
+
|
|
761
|
+
if (this.heartbeatTimer) {
|
|
762
|
+
clearInterval(this.heartbeatTimer);
|
|
763
|
+
this.heartbeatTimer = null;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
this.cleanupSocket(socket);
|
|
767
|
+
if (this.socket === socket) {
|
|
768
|
+
this.socket = null;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
this.reconnectAttempt = 0;
|
|
772
|
+
|
|
773
|
+
if (this.reconnectTimer) {
|
|
774
|
+
clearTimeout(this.reconnectTimer);
|
|
775
|
+
this.reconnectTimer = null;
|
|
776
|
+
}
|
|
678
777
|
});
|
|
679
778
|
|
|
680
779
|
socket.on('error', (err) => {
|
|
@@ -682,6 +781,7 @@ export class TcpClient {
|
|
|
682
781
|
});
|
|
683
782
|
}
|
|
684
783
|
|
|
784
|
+
|
|
685
785
|
private processIncoming(): void {
|
|
686
786
|
while (this.readBuffer.length >= 5) {
|
|
687
787
|
const type = this.readBuffer.readUInt8(0);
|
|
@@ -736,7 +836,7 @@ export class TcpClient {
|
|
|
736
836
|
);
|
|
737
837
|
}
|
|
738
838
|
|
|
739
|
-
//
|
|
839
|
+
// Incoming Frame Handler: routes LAN messages to raw + parsed callbacks
|
|
740
840
|
private handleIncomingFrame(frame: Buffer, type: number): void {
|
|
741
841
|
// Fan out raw frame to higher layers (CyncClient) for debugging
|
|
742
842
|
for (const listener of this.rawFrameListeners) {
|
package/src/cync/token-store.ts
CHANGED
|
@@ -31,8 +31,9 @@ export class CyncTokenStore {
|
|
|
31
31
|
const raw = await fs.readFile(this.filePath, 'utf8');
|
|
32
32
|
const data = JSON.parse(raw) as CyncTokenData;
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
|
|
36
|
+
if (typeof data.expiresAt === 'number' && data.expiresAt <= now) {
|
|
36
37
|
return null;
|
|
37
38
|
}
|
|
38
39
|
|