homebridge-cync-app 0.1.8 β 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/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 +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/platform.js +1 -1
- 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 +9 -4
- package/src/cync/tcp-client.ts +153 -53
- package/src/platform.ts +1 -1
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,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: '
|
|
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
|
};
|