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.
@@ -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
- this.log.error(
529
- 'Cync properties call failed: HTTP %d %s %o',
530
- res.status,
531
- res.statusText,
532
- json,
533
- );
534
- const errBody = json as CyncErrorBody;
535
- const msg =
536
- errBody.error?.msg ?? `Cync properties failed with ${res.status}`;
537
- throw new Error(msg);
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
- // ### 🧩 LAN Login Blob Builder: Generates the auth_code payload used by Cync LAN TCP
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 revision, if present
226
+ // Firmware / Software revision
227
227
  if (typeof rawDevice.firmwareVersion === 'string' && rawDevice.firmwareVersion.trim().length > 0) {
228
- infoService.updateCharacteristic(
229
- Characteristic.FirmwareRevision,
230
- rawDevice.firmwareVersion.trim(),
231
- );
228
+ const rev = rawDevice.firmwareVersion.trim();
229
+
230
+ infoService.updateCharacteristic(Characteristic.FirmwareRevision, rev);
231
+ infoService.updateCharacteristic(Characteristic.SoftwareRevision, rev);
232
232
  }
233
233
  }
@@ -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
- // ### 🧩 LAN Topology Cache: mirrors HA's home_devices / home_controllers / switchID_to_homeID
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
- // ### 🧩 Password Login Helper: background username/password login for new tokens
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
- // ### 🧩 LAN Update Bridge: allow platform to handle device updates
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
- // ### 🧩 LAN Auth Blob Getter: Returns the LAN login code if available
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
- // ### 🧩 LAN Login Code Builder
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
- // ### 🧩 Refresh Error Detector: identifies "Access-Token Expired" responses
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
- // ### 🧩 Token Refresh Helper: exchanges refreshToken for a new accessToken, or falls back to password login
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
- // ### 🧩 Cloud Config Wrapper: auto-refreshes access token
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
- // ### 🧩 Device Properties Wrapper: auto-refresh on Access-Token Expired for mesh calls
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 ?? mesh.id;
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
- // ### 🧩 Bulb Capability Debug: log each bulb so we can classify plugs vs lights
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
- throw new env.api.hap.HapStatusError(
60
- env.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
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
- // If we have a cached LAN level, use it; otherwise infer from On.
126
- if (typeof current === 'number') {
127
- return current;
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
- const on = ctx.cync?.on ?? false;
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
- throw new env.api.hap.HapStatusError(
201
- env.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
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
- const hue = ctx.cync?.hue;
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
- throw new env.api.hap.HapStatusError(
285
- env.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
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
- const sat = ctx.cync?.saturation;
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
- throw new env.api.hap.HapStatusError(
50
- env.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE,
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: Light On.set called for %s but no cync.deviceId in context',
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: Light On.set -> %s for %s (deviceId=%s)',
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
- if (!on) {
86
- // Off is always a plain power packet
87
- await env.tcpClient.setSwitchState(cyncMeta.deviceId, { on: false });
88
- return;
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
- // Turning on:
92
- // - If we were in color mode with a known RGB + brightness, restore color.
93
- // - Otherwise, just send a basic power-on packet.
94
- if (cyncMeta.colorActive && cyncMeta.rgb && typeof cyncMeta.brightness === 'number') {
95
- await env.tcpClient.setColor(cyncMeta.deviceId, cyncMeta.rgb, cyncMeta.brightness);
96
- } else {
97
- await env.tcpClient.setSwitchState(cyncMeta.deviceId, { on: true });
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: 'Cync Indoor Plug',
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
  };