homebridge-cync-app 0.1.8 → 0.1.10
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 +41 -0
- package/dist/cync/config-client.js +35 -6
- package/dist/cync/config-client.js.map +1 -1
- package/dist/cync/cync-accessory-helpers.js +4 -2
- package/dist/cync/cync-accessory-helpers.js.map +1 -1
- package/dist/cync/cync-client.d.ts +3 -0
- package/dist/cync/cync-client.js +51 -12
- package/dist/cync/cync-client.js.map +1 -1
- package/dist/cync/cync-light-accessory.js +16 -23
- package/dist/cync/cync-light-accessory.js.map +1 -1
- package/dist/cync/cync-switch-accessory.js +22 -16
- package/dist/cync/cync-switch-accessory.js.map +1 -1
- package/dist/cync/device-catalog.js +21 -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/platform.js +8 -5
- package/dist/platform.js.map +1 -1
- package/package.json +1 -1
- package/src/cync/config-client.ts +62 -12
- package/src/cync/cync-accessory-helpers.ts +5 -5
- package/src/cync/cync-client.ts +79 -14
- package/src/cync/cync-light-accessory.ts +39 -29
- package/src/cync/cync-switch-accessory.ts +36 -17
- package/src/cync/device-catalog.ts +21 -4
- package/src/cync/tcp-client.ts +153 -53
- package/src/platform.ts +37 -32
|
@@ -28,6 +28,38 @@ type CyncErrorBody = {
|
|
|
28
28
|
[key: string]: unknown;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
type CyncApiError = {
|
|
32
|
+
status: number;
|
|
33
|
+
statusText: string;
|
|
34
|
+
body: unknown;
|
|
35
|
+
code?: number;
|
|
36
|
+
msg?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function extractCyncError(body: unknown): { code?: number; msg?: string } {
|
|
40
|
+
if (!body || typeof body !== 'object') {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const obj = body as Record<string, unknown>;
|
|
45
|
+
const err = obj.error;
|
|
46
|
+
|
|
47
|
+
if (!err || typeof err !== 'object') {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const e = err as Record<string, unknown>;
|
|
52
|
+
const code = typeof e.code === 'number' ? e.code : undefined;
|
|
53
|
+
const msg = typeof e.msg === 'string' ? e.msg : undefined;
|
|
54
|
+
|
|
55
|
+
return { code, msg };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isDevicePropertyNotExists(status: number, body: unknown): boolean {
|
|
59
|
+
const { code, msg } = extractCyncError(body);
|
|
60
|
+
return status === 404 && (code === 4041009 || msg === 'device property not exists');
|
|
61
|
+
}
|
|
62
|
+
|
|
31
63
|
export interface CyncLoginSession {
|
|
32
64
|
accessToken: string;
|
|
33
65
|
userId: string;
|
|
@@ -525,19 +557,37 @@ export class ConfigClient {
|
|
|
525
557
|
});
|
|
526
558
|
|
|
527
559
|
if (!res.ok) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
560
|
+
const { code, msg } = extractCyncError(json);
|
|
561
|
+
const outMsg = msg ?? `Cync properties failed with ${res.status}`;
|
|
562
|
+
|
|
563
|
+
if (isDevicePropertyNotExists(res.status, json)) {
|
|
564
|
+
this.log.debug(
|
|
565
|
+
'Cync properties call failed (expected): HTTP %d %s code=%s msg=%s',
|
|
566
|
+
res.status,
|
|
567
|
+
res.statusText,
|
|
568
|
+
code !== undefined ? String(code) : 'unknown',
|
|
569
|
+
outMsg,
|
|
570
|
+
);
|
|
571
|
+
} else {
|
|
572
|
+
this.log.error(
|
|
573
|
+
'Cync properties call failed: HTTP %d %s %o',
|
|
574
|
+
res.status,
|
|
575
|
+
res.statusText,
|
|
576
|
+
json,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const e: CyncApiError = {
|
|
581
|
+
status: res.status,
|
|
582
|
+
statusText: res.statusText,
|
|
583
|
+
body: json,
|
|
584
|
+
code,
|
|
585
|
+
msg: outMsg,
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
throw e;
|
|
538
589
|
}
|
|
539
590
|
|
|
540
|
-
// We keep this as a loose record; callers can shape it as needed.
|
|
541
591
|
return json as Record<string, unknown>;
|
|
542
592
|
}
|
|
543
593
|
|
|
@@ -559,7 +609,7 @@ export class ConfigClient {
|
|
|
559
609
|
throw new Error('Cync session not initialised. Call loginWithTwoFactor() first.');
|
|
560
610
|
}
|
|
561
611
|
}
|
|
562
|
-
//
|
|
612
|
+
// LAN Login Blob Builder: Generates the auth_code payload used by Cync LAN TCP
|
|
563
613
|
public static buildLanLoginCode(userId: string, authorize: string): Uint8Array {
|
|
564
614
|
const authBytes = Buffer.from(authorize, 'ascii');
|
|
565
615
|
const lengthByte = 10 + authBytes.length;
|
|
@@ -223,11 +223,11 @@ export function applyAccessoryInformationFromCyncDevice(
|
|
|
223
223
|
|
|
224
224
|
infoService.updateCharacteristic(Characteristic.SerialNumber, serial);
|
|
225
225
|
|
|
226
|
-
// Firmware
|
|
226
|
+
// Firmware / Software revision
|
|
227
227
|
if (typeof rawDevice.firmwareVersion === 'string' && rawDevice.firmwareVersion.trim().length > 0) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
);
|
|
228
|
+
const rev = rawDevice.firmwareVersion.trim();
|
|
229
|
+
|
|
230
|
+
infoService.updateCharacteristic(Characteristic.FirmwareRevision, rev);
|
|
231
|
+
infoService.updateCharacteristic(Characteristic.SoftwareRevision, rev);
|
|
232
232
|
}
|
|
233
233
|
}
|
package/src/cync/cync-client.ts
CHANGED
|
@@ -28,15 +28,48 @@ export class CyncClient {
|
|
|
28
28
|
private readonly log: CyncLogger;
|
|
29
29
|
private readonly configClient: ConfigClient;
|
|
30
30
|
private readonly tcpClient: TcpClient;
|
|
31
|
-
|
|
31
|
+
private readonly unsupportedPropertiesProductIds = new Set<string>();
|
|
32
32
|
private readonly tokenStore: CyncTokenStore;
|
|
33
33
|
private tokenData: CyncTokenData | null = null;
|
|
34
|
+
private isDevicePropertyNotExistsError(err: unknown): boolean {
|
|
35
|
+
if (!err) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type ErrorWithShape = {
|
|
40
|
+
status?: number;
|
|
41
|
+
error?: {
|
|
42
|
+
code?: number;
|
|
43
|
+
msg?: string;
|
|
44
|
+
};
|
|
45
|
+
message?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const e = err as ErrorWithShape;
|
|
49
|
+
|
|
50
|
+
// Observed cloud response:
|
|
51
|
+
// HTTP 404 Not Found { error: { msg: 'device property not exists', code: 4041009 } }
|
|
52
|
+
if (e.status === 404 && e.error?.code === 4041009) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Fallback (if shape changes or gets wrapped)
|
|
57
|
+
if (e.message && e.message.includes('device property not exists')) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
34
60
|
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private formatMeshLabel(mesh: { name?: string | null; id: string | number }): string {
|
|
65
|
+
const rawName = typeof mesh.name === 'string' ? mesh.name.trim() : '';
|
|
66
|
+
return rawName.length > 0 ? rawName : `No Name (id=${String(mesh.id)})`;
|
|
67
|
+
}
|
|
35
68
|
// Populated after successful login.
|
|
36
69
|
private session: CyncLoginSession | null = null;
|
|
37
70
|
private cloudConfig: CyncCloudConfig | null = null;
|
|
38
71
|
|
|
39
|
-
//
|
|
72
|
+
// LAN Topology Cache: mirrors HA's home_devices / home_controllers / switchID_to_homeID
|
|
40
73
|
private homeDevices: Record<string, string[]> = {};
|
|
41
74
|
private homeControllers: Record<string, number[]> = {};
|
|
42
75
|
private switchIdToHomeId: Record<number, string> = {};
|
|
@@ -52,7 +85,7 @@ export class CyncClient {
|
|
|
52
85
|
// Optional LAN update hook for the platform
|
|
53
86
|
private lanUpdateHandler: ((update: unknown) => void) | null = null;
|
|
54
87
|
|
|
55
|
-
//
|
|
88
|
+
// Password Login Helper: background username/password login for new tokens
|
|
56
89
|
private async loginWithPasswordForToken(): Promise<CyncTokenData | null> {
|
|
57
90
|
const { username, password } = this.loginConfig;
|
|
58
91
|
|
|
@@ -139,12 +172,12 @@ export class CyncClient {
|
|
|
139
172
|
}
|
|
140
173
|
}
|
|
141
174
|
|
|
142
|
-
//
|
|
175
|
+
// LAN Update Bridge: allow platform to handle device updates
|
|
143
176
|
public onLanDeviceUpdate(handler: (update: unknown) => void): void {
|
|
144
177
|
this.lanUpdateHandler = handler;
|
|
145
178
|
}
|
|
146
179
|
|
|
147
|
-
//
|
|
180
|
+
// LAN Auth Blob Getter: Returns the LAN login code if available
|
|
148
181
|
public getLanLoginCode(): Uint8Array {
|
|
149
182
|
if (!this.tokenData?.lanLoginCode) {
|
|
150
183
|
this.log.debug('CyncClient: getLanLoginCode() → no LAN blob in token store.');
|
|
@@ -184,7 +217,7 @@ export class CyncClient {
|
|
|
184
217
|
);
|
|
185
218
|
}
|
|
186
219
|
|
|
187
|
-
//
|
|
220
|
+
// LAN Login Code Builder
|
|
188
221
|
private buildLanLoginCode(authorize: string, userId: number): Uint8Array {
|
|
189
222
|
const authorizeBytes = Buffer.from(authorize, 'ascii');
|
|
190
223
|
|
|
@@ -408,7 +441,7 @@ export class CyncClient {
|
|
|
408
441
|
* // user reads email, gets code…
|
|
409
442
|
* await client.submitTwoFactor(username, password, code); // completes login
|
|
410
443
|
*/
|
|
411
|
-
//
|
|
444
|
+
// Refresh Error Detector: identifies "Access-Token Expired" responses
|
|
412
445
|
private isAccessTokenExpiredError(err: unknown): boolean {
|
|
413
446
|
if (!err) {
|
|
414
447
|
return false;
|
|
@@ -456,7 +489,7 @@ export class CyncClient {
|
|
|
456
489
|
return false;
|
|
457
490
|
}
|
|
458
491
|
|
|
459
|
-
//
|
|
492
|
+
// Token Refresh Helper: exchanges refreshToken for a new accessToken, or falls back to password login
|
|
460
493
|
private async refreshAccessToken(
|
|
461
494
|
stored: CyncTokenData,
|
|
462
495
|
): Promise<CyncTokenData | null> {
|
|
@@ -510,7 +543,7 @@ export class CyncClient {
|
|
|
510
543
|
return viaPassword;
|
|
511
544
|
}
|
|
512
545
|
|
|
513
|
-
//
|
|
546
|
+
// Cloud Config Wrapper: auto-refreshes access token
|
|
514
547
|
private async getCloudConfigWithRefresh(): Promise<CyncCloudConfig> {
|
|
515
548
|
try {
|
|
516
549
|
return await this.configClient.getCloudConfig();
|
|
@@ -530,18 +563,33 @@ export class CyncClient {
|
|
|
530
563
|
}
|
|
531
564
|
}
|
|
532
565
|
|
|
533
|
-
//
|
|
566
|
+
// Device Properties Wrapper: auto-refresh on Access-Token Expired for mesh calls
|
|
534
567
|
private async getDevicePropertiesWithRefresh(
|
|
535
568
|
productId: string | number,
|
|
536
569
|
meshId: string | number,
|
|
537
570
|
): Promise<Record<string, unknown>> {
|
|
538
|
-
// Normalise to strings for ConfigClient
|
|
539
571
|
const productIdStr = String(productId);
|
|
540
572
|
const meshIdStr = String(meshId);
|
|
541
573
|
|
|
574
|
+
// If we've already learned this product_id never supports the endpoint, skip.
|
|
575
|
+
if (this.unsupportedPropertiesProductIds.has(productIdStr)) {
|
|
576
|
+
return {};
|
|
577
|
+
}
|
|
578
|
+
|
|
542
579
|
try {
|
|
543
580
|
return await this.configClient.getDeviceProperties(productIdStr, meshIdStr);
|
|
544
581
|
} catch (err) {
|
|
582
|
+
// Expected case for some product lines: endpoint not implemented.
|
|
583
|
+
if (this.isDevicePropertyNotExistsError(err)) {
|
|
584
|
+
this.unsupportedPropertiesProductIds.add(productIdStr);
|
|
585
|
+
this.log.debug(
|
|
586
|
+
'CyncClient: properties unsupported for product_id=%s (mesh id=%s); caching and skipping.',
|
|
587
|
+
productIdStr,
|
|
588
|
+
meshIdStr,
|
|
589
|
+
);
|
|
590
|
+
return {};
|
|
591
|
+
}
|
|
592
|
+
|
|
545
593
|
if (this.isAccessTokenExpiredError(err) && this.tokenData) {
|
|
546
594
|
this.log.warn(
|
|
547
595
|
'CyncClient: access token expired when calling getDeviceProperties(); refreshing and retrying once.',
|
|
@@ -626,7 +674,7 @@ export class CyncClient {
|
|
|
626
674
|
|
|
627
675
|
// Debug: inspect per-mesh properties so we can find the real devices.
|
|
628
676
|
for (const mesh of cfg.meshes) {
|
|
629
|
-
const meshName = mesh.name
|
|
677
|
+
const meshName = this.formatMeshLabel({ name: mesh.name, id: mesh.id });
|
|
630
678
|
const homeId = String(mesh.id);
|
|
631
679
|
|
|
632
680
|
this.log.debug(
|
|
@@ -639,7 +687,15 @@ export class CyncClient {
|
|
|
639
687
|
// Per-home maps, mirroring HA's CyncUserData.get_cync_config()
|
|
640
688
|
const homeDevices: string[] = [];
|
|
641
689
|
const homeControllers: number[] = [];
|
|
642
|
-
|
|
690
|
+
const productIdStr = String(mesh.product_id);
|
|
691
|
+
if (this.unsupportedPropertiesProductIds.has(productIdStr)) {
|
|
692
|
+
this.log.debug(
|
|
693
|
+
'CyncClient: skipping properties probe for mesh %s (product_id=%s) — previously marked unsupported.',
|
|
694
|
+
meshName,
|
|
695
|
+
productIdStr,
|
|
696
|
+
);
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
643
699
|
try {
|
|
644
700
|
const props = await this.getDevicePropertiesWithRefresh(
|
|
645
701
|
mesh.product_id,
|
|
@@ -663,7 +719,7 @@ export class CyncClient {
|
|
|
663
719
|
bulbsArray[0] ? Object.keys(bulbsArray[0] as Record<string, unknown>) : [],
|
|
664
720
|
);
|
|
665
721
|
|
|
666
|
-
//
|
|
722
|
+
// Bulb Capability Debug: log each bulb so we can classify plugs vs lights
|
|
667
723
|
bulbsArray.forEach((bulb, index) => {
|
|
668
724
|
const record = bulb as Record<string, unknown>;
|
|
669
725
|
|
|
@@ -791,6 +847,15 @@ export class CyncClient {
|
|
|
791
847
|
);
|
|
792
848
|
}
|
|
793
849
|
} catch (err) {
|
|
850
|
+
if (this.isDevicePropertyNotExistsError(err)) {
|
|
851
|
+
this.log.debug(
|
|
852
|
+
'CyncClient: getDeviceProperties not supported for mesh %s (%s).',
|
|
853
|
+
meshName,
|
|
854
|
+
String(mesh.id),
|
|
855
|
+
);
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
|
|
794
859
|
this.log.warn(
|
|
795
860
|
'CyncClient: getDeviceProperties failed for mesh %s (%s): %s',
|
|
796
861
|
meshName,
|
|
@@ -55,19 +55,25 @@ export function configureCyncLightAccessory(
|
|
|
55
55
|
service
|
|
56
56
|
.getCharacteristic(Characteristic.On)
|
|
57
57
|
.onGet(() => {
|
|
58
|
+
const currentOn = !!ctx.cync?.on;
|
|
59
|
+
|
|
58
60
|
if (env.isDeviceProbablyOffline(deviceId)) {
|
|
59
|
-
|
|
60
|
-
|
|
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,
|
|
61
66
|
);
|
|
67
|
+
return currentOn;
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
const currentOn = !!ctx.cync?.on;
|
|
65
70
|
env.log.info(
|
|
66
71
|
'Cync: Light On.get -> %s for %s (deviceId=%s)',
|
|
67
72
|
String(currentOn),
|
|
68
73
|
deviceName,
|
|
69
74
|
deviceId,
|
|
70
75
|
);
|
|
76
|
+
|
|
71
77
|
return currentOn;
|
|
72
78
|
})
|
|
73
79
|
.onSet(async (value) => {
|
|
@@ -114,21 +120,24 @@ export function configureCyncLightAccessory(
|
|
|
114
120
|
service
|
|
115
121
|
.getCharacteristic(Characteristic.Brightness)
|
|
116
122
|
.onGet(() => {
|
|
117
|
-
if (env.isDeviceProbablyOffline(deviceId)) {
|
|
118
|
-
throw new env.api.hap.HapStatusError(
|
|
119
|
-
env.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
123
|
const current = ctx.cync?.brightness;
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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;
|
|
128
138
|
}
|
|
129
139
|
|
|
130
|
-
|
|
131
|
-
return on ? 100 : 0;
|
|
140
|
+
return cachedBrightness;
|
|
132
141
|
})
|
|
133
142
|
.onSet(async (value) => {
|
|
134
143
|
const cyncMeta = ctx.cync;
|
|
@@ -196,18 +205,18 @@ export function configureCyncLightAccessory(
|
|
|
196
205
|
service
|
|
197
206
|
.getCharacteristic(Characteristic.Hue)
|
|
198
207
|
.onGet(() => {
|
|
208
|
+
const hue = typeof ctx.cync?.hue === 'number' ? ctx.cync.hue : 0;
|
|
209
|
+
|
|
199
210
|
if (env.isDeviceProbablyOffline(deviceId)) {
|
|
200
|
-
|
|
201
|
-
|
|
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,
|
|
202
216
|
);
|
|
203
217
|
}
|
|
204
218
|
|
|
205
|
-
|
|
206
|
-
if (typeof hue === 'number') {
|
|
207
|
-
return hue;
|
|
208
|
-
}
|
|
209
|
-
// Default to 0° (red) if we have no color history
|
|
210
|
-
return 0;
|
|
219
|
+
return hue;
|
|
211
220
|
})
|
|
212
221
|
.onSet(async (value) => {
|
|
213
222
|
const cyncMeta = ctx.cync;
|
|
@@ -280,17 +289,18 @@ export function configureCyncLightAccessory(
|
|
|
280
289
|
service
|
|
281
290
|
.getCharacteristic(Characteristic.Saturation)
|
|
282
291
|
.onGet(() => {
|
|
292
|
+
const sat = typeof ctx.cync?.saturation === 'number' ? ctx.cync.saturation : 100;
|
|
293
|
+
|
|
283
294
|
if (env.isDeviceProbablyOffline(deviceId)) {
|
|
284
|
-
|
|
285
|
-
|
|
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,
|
|
286
300
|
);
|
|
287
301
|
}
|
|
288
302
|
|
|
289
|
-
|
|
290
|
-
if (typeof sat === 'number') {
|
|
291
|
-
return sat;
|
|
292
|
-
}
|
|
293
|
-
return 100;
|
|
303
|
+
return sat;
|
|
294
304
|
})
|
|
295
305
|
.onSet(async (value) => {
|
|
296
306
|
const cyncMeta = ctx.cync;
|
|
@@ -45,19 +45,25 @@ export function configureCyncSwitchAccessory(
|
|
|
45
45
|
service
|
|
46
46
|
.getCharacteristic(env.api.hap.Characteristic.On)
|
|
47
47
|
.onGet(() => {
|
|
48
|
+
const currentOn = !!ctx.cync?.on;
|
|
49
|
+
|
|
48
50
|
if (env.isDeviceProbablyOffline(deviceId)) {
|
|
49
|
-
|
|
50
|
-
|
|
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,
|
|
51
56
|
);
|
|
57
|
+
return currentOn;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
const currentOn = !!ctx.cync?.on;
|
|
55
60
|
env.log.info(
|
|
56
61
|
'Cync: On.get -> %s for %s (deviceId=%s)',
|
|
57
62
|
String(currentOn),
|
|
58
63
|
deviceName,
|
|
59
64
|
deviceId,
|
|
60
65
|
);
|
|
66
|
+
|
|
61
67
|
return currentOn;
|
|
62
68
|
})
|
|
63
69
|
.onSet(async (value) => {
|
|
@@ -65,7 +71,7 @@ export function configureCyncSwitchAccessory(
|
|
|
65
71
|
|
|
66
72
|
if (!cyncMeta?.deviceId) {
|
|
67
73
|
env.log.warn(
|
|
68
|
-
'Cync:
|
|
74
|
+
'Cync: Switch On.set called for %s but no cync.deviceId in context',
|
|
69
75
|
deviceName,
|
|
70
76
|
);
|
|
71
77
|
return;
|
|
@@ -74,27 +80,40 @@ export function configureCyncSwitchAccessory(
|
|
|
74
80
|
const on = value === true || value === 1;
|
|
75
81
|
|
|
76
82
|
env.log.info(
|
|
77
|
-
'Cync:
|
|
83
|
+
'Cync: Switch On.set -> %s for %s (deviceId=%s)',
|
|
78
84
|
String(on),
|
|
79
85
|
deviceName,
|
|
80
86
|
cyncMeta.deviceId,
|
|
81
87
|
);
|
|
82
88
|
|
|
89
|
+
// Optimistic cache
|
|
83
90
|
cyncMeta.on = on;
|
|
84
91
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
try {
|
|
93
|
+
if (!on) {
|
|
94
|
+
await env.tcpClient.setSwitchState(cyncMeta.deviceId, { on: false });
|
|
95
|
+
env.markDeviceSeen(cyncMeta.deviceId);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
90
98
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
);
|
|
98
117
|
}
|
|
99
118
|
});
|
|
100
119
|
}
|
|
@@ -30,22 +30,39 @@ 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: '
|
|
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
|
},
|
|
49
|
+
137: {
|
|
50
|
+
deviceType: 137,
|
|
51
|
+
modelName: 'A19 Full Color Direct Connect Smart Bulb (3-in-1)',
|
|
52
|
+
marketingName: 'GE Cync A19 Smart LED Light Bulb, Color Changing Smart WiFi Light',
|
|
53
|
+
notes: 'Reported by users as full color + dimming bulbs; must be Lightbulb, not Switch.',
|
|
54
|
+
},
|
|
55
|
+
171: {
|
|
56
|
+
deviceType: 171,
|
|
57
|
+
modelName: 'A19 Full Color Direct Connect Smart Bulb (3-in-1)',
|
|
58
|
+
marketingName: 'GE Cync A19 Smart LED Light Bulb, Color Changing Smart WiFi Light',
|
|
59
|
+
notes: 'Reported alongside deviceType=137 in the same home; appears to be same class of bulb.',
|
|
60
|
+
},
|
|
45
61
|
172: {
|
|
46
62
|
deviceType: 172,
|
|
47
63
|
modelName: 'Indoor Smart Plug (3in1)',
|
|
48
64
|
marketingName: 'Cync Indoor Smart Plug',
|
|
65
|
+
notes: 'Matter-capable hardware. Replaces legacy C by GE On/Off Smart Plug.',
|
|
49
66
|
// defaultCategory: Categories.OUTLET,
|
|
50
67
|
},
|
|
51
68
|
};
|