@camstack/addon-provider-wyze 0.2.122 → 0.2.123

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/addon.js CHANGED
@@ -14785,7 +14785,17 @@ var SiteZoneSchema = object({
14785
14785
  var ProviderStatusSchema = object({
14786
14786
  connected: boolean(),
14787
14787
  deviceCount: number(),
14788
- error: string().optional()
14788
+ error: string().optional(),
14789
+ /**
14790
+ * `'orchestrator'`: this provider writes NO camera's `motion` runtime slice
14791
+ * — the orchestrator is its only writer (D696). Set by `BaseDeviceProvider`,
14792
+ * which every provider bundles, so only a provider BUILT from D696-amended
14793
+ * code says it. ABSENT means an older provider that may still write it (raw
14794
+ * VMD writes, a boot seed), and the orchestrator's provider gate keeps that
14795
+ * provider's cameras unwritten (`camera-sensors/motion-slice-provider-gate.ts`)
14796
+ * — the provider twin of the runner's `getLocalLoad().motionSliceWriter`.
14797
+ */
14798
+ motionSliceWriter: literal("orchestrator").optional()
14789
14799
  });
14790
14800
  object({
14791
14801
  externalId: string(),
@@ -20247,6 +20257,29 @@ object({
20247
20257
  firstAt: number(),
20248
20258
  lastAt: number()
20249
20259
  });
20260
+ var NC_ALARM_TRIGGERED_MAX_SEC = 3600;
20261
+ var NcAlarmDelaySecSchema = number().int().min(0).max(600);
20262
+ var NcAlarmTriggeredSecSchema = number().int().min(0).max(NC_ALARM_TRIGGERED_MAX_SEC);
20263
+ var uniqueModes = (entries) => new Set(entries.map((e) => e.mode)).size === entries.length;
20264
+ var NcAlarmModeDelaysListSchema = array(object({
20265
+ mode: AlarmArmModeSchema,
20266
+ exitDelaySec: NcAlarmDelaySecSchema.optional(),
20267
+ entryDelaySec: NcAlarmDelaySecSchema.optional(),
20268
+ triggeredDurationSec: NcAlarmTriggeredSecSchema.optional()
20269
+ })).max(8).refine(uniqueModes, { message: "modeDelays: at most one entry per arm mode" });
20270
+ var NcAlarmModeDelaysEditListSchema = array(object({
20271
+ mode: AlarmArmModeSchema,
20272
+ exitDelaySec: NcAlarmDelaySecSchema.nullable().optional(),
20273
+ entryDelaySec: NcAlarmDelaySecSchema.nullable().optional(),
20274
+ triggeredDurationSec: NcAlarmTriggeredSecSchema.nullable().optional()
20275
+ })).max(8).refine(uniqueModes, { message: "modeDelays: at most one edit per arm mode" });
20276
+ /** One switch of the alarm table: put ONE device on, or take it off, ONE mode's list. */
20277
+ var NcAlarmNonBlockingEditSchema = object({
20278
+ mode: AlarmArmModeSchema,
20279
+ deviceId: number().int().positive(),
20280
+ /** true = on the list (may be open, does not block); false = off the list (blocks). */
20281
+ nonBlocking: boolean()
20282
+ });
20250
20283
  /**
20251
20284
  * The per-mode lists, at most one per mode. Two lists for one mode have no
20252
20285
  * meaning a reader could agree on (union? the last one?), so they are refused
@@ -20255,10 +20288,10 @@ object({
20255
20288
  var NcAlarmNonBlockingListSchema = array(object({
20256
20289
  mode: AlarmArmModeSchema,
20257
20290
  deviceIds: array(number().int()).max(200)
20258
- })).max(8).refine((lists) => new Set(lists.map((l) => l.mode)).size === lists.length, { message: "nonBlocking: at most one list per arm mode" });
20291
+ })).max(8).refine(uniqueModes, { message: "nonBlocking: at most one list per arm mode" });
20259
20292
  /**
20260
- * The three durations the panel's state machine runs on, who hears about its
20261
- * transitions, and which openings each mode tolerates.
20293
+ * The three durations — global, and per mode since D697 — the panel's state
20294
+ * machine runs on, who hears about its transitions, and which openings each mode tolerates.
20262
20295
  *
20263
20296
  * They live on the NOTIFICATION-RULES cap, not on `alarm-panel`, on purpose:
20264
20297
  * `alarm-panel` is `deviceNative` and its other provider mirrors somebody
@@ -20268,15 +20301,15 @@ var NcAlarmNonBlockingListSchema = array(object({
20268
20301
  */
20269
20302
  var NcAlarmSettingsSchema = object({
20270
20303
  /** Grace period between arming and the mode taking effect. 0 = immediate. */
20271
- exitDelaySec: number().int().min(0).max(600),
20304
+ exitDelaySec: NcAlarmDelaySecSchema,
20272
20305
  /** Grace period between a trigger and the alarm firing. 0 = immediate. */
20273
- entryDelaySec: number().int().min(0).max(600),
20306
+ entryDelaySec: NcAlarmDelaySecSchema,
20274
20307
  /**
20275
20308
  * How long `triggered` lasts before the panel re-arms itself.
20276
20309
  * **0 = until an operator disarms it** — the behaviour before this field
20277
20310
  * existed, and therefore what an untouched install keeps doing.
20278
20311
  */
20279
- triggeredDurationSec: number().int().min(0).max(3600),
20312
+ triggeredDurationSec: NcAlarmTriggeredSecSchema,
20280
20313
  /**
20281
20314
  * Deprecated by D630 — every transition is announced. Parsed, never read;
20282
20315
  * remove after one release. Optional so an older admin's patch (which still
@@ -20299,17 +20332,30 @@ var NcAlarmSettingsSchema = object({
20299
20332
  * per mode is meaningful. Defaulted to `[]` so a blob stored before D630
20300
20333
  * parses as "every opening blocks" — the behaviour it was written under.
20301
20334
  */
20302
- nonBlocking: NcAlarmNonBlockingListSchema.default([])
20335
+ nonBlocking: NcAlarmNonBlockingListSchema.default([]),
20336
+ /**
20337
+ * Per arm mode, the delays that mode OVERRIDES (D697). A mode with no entry, or an entry
20338
+ * without a field, uses the global value above — so `[]`, the default, is exactly the
20339
+ * behaviour of a blob stored before this field existed. Read through `resolveModeDelays`.
20340
+ */
20341
+ modeDelays: NcAlarmModeDelaysListSchema.default([])
20303
20342
  });
20304
20343
  /**
20305
20344
  * Every field optional — a tab edits one control at a time.
20306
20345
  *
20307
- * `nonBlocking` is re-declared WITHOUT its default: `.partial()` does not
20308
- * remove an inner `.default()`, so a patch that never named the field would
20309
- * parse to `nonBlocking: []` and wipe every mode's list on an unrelated edit
20310
- * (the same trap {@link NcRulePatchSchema} documents).
20346
+ * `nonBlocking` and `modeDelays` are re-declared WITHOUT their defaults:
20347
+ * `.partial()` does not remove an inner `.default()`, so a patch that never
20348
+ * named the field would parse to `[]` and wipe every mode's list on an
20349
+ * unrelated edit (the same trap {@link NcRulePatchSchema} documents). Here
20350
+ * `modeDelays` means EDITS, not the stored overrides.
20311
20351
  */
20312
- var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.extend({ nonBlocking: NcAlarmNonBlockingListSchema }).partial();
20352
+ var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.extend({
20353
+ nonBlocking: NcAlarmNonBlockingListSchema,
20354
+ /** Per-mode, per-field edits — NOT the whole list (see NcAlarmModeDelaysEditSchema). */
20355
+ modeDelays: NcAlarmModeDelaysEditListSchema,
20356
+ /** ONE membership change; the panel applies it to the list it holds at its turn. */
20357
+ nonBlockingEdit: NcAlarmNonBlockingEditSchema
20358
+ }).partial().refine((p) => p.nonBlocking === void 0 || p.nonBlockingEdit === void 0, { message: "nonBlocking and nonBlockingEdit are exclusive — send the whole lists or one edit" });
20313
20359
  /**
20314
20360
  * What one arm mode actually arms, DERIVED from the enabled rules gated on it.
20315
20361
  * Never authored, never stored — see `alarm-mode-coverage.ts` for why a stored
@@ -20409,6 +20455,28 @@ var NcAlarmConfigSchema = object({
20409
20455
  */
20410
20456
  live: NcAlarmLiveSchema.optional()
20411
20457
  });
20458
+ var NcAlarmSensorReadingSchema = _enum([
20459
+ "open",
20460
+ "closed",
20461
+ "unknown",
20462
+ "not-opening"
20463
+ ]);
20464
+ /** One device's state as the alarm reads it — the classifier the arm check uses. */
20465
+ var NcAlarmSensorStateSchema = object({
20466
+ deviceId: number().int().positive(),
20467
+ reading: NcAlarmSensorReadingSchema,
20468
+ /** The word as read (`open`, `closed`, `unlocked`, `stopped`, …); null when unknown / not an opening. */
20469
+ state: string().nullable(),
20470
+ /** Why `unknown`: the read failed, or the slice holds no word its cap defines (a placeholder included). */
20471
+ unknownWhy: _enum(["read-failed", "unknown-word"]).nullable(),
20472
+ /** The device manager's liveness as the centre mirrors it; null = not known. */
20473
+ online: boolean().nullable()
20474
+ });
20475
+ var NcAlarmSensorsInputSchema = object({ deviceIds: array(number().int().positive()).max(500) });
20476
+ var NcAlarmSensorsSchema = object({
20477
+ sensors: array(NcAlarmSensorStateSchema),
20478
+ readAt: number().int().nonnegative()
20479
+ });
20412
20480
  /**
20413
20481
  * ONE rule's demand on ONE camera's clip ring.
20414
20482
  *
@@ -20499,7 +20567,7 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
20499
20567
  }), method(object({}), NcAlarmConfigSchema, { auth: "admin" }), method(object({ patch: NcAlarmSettingsPatchSchema }), NcAlarmConfigSchema, {
20500
20568
  kind: "mutation",
20501
20569
  auth: "admin"
20502
- });
20570
+ }), method(NcAlarmSensorsInputSchema, NcAlarmSensorsSchema, { auth: "admin" });
20503
20571
  /**
20504
20572
  * Zod schemas for persisted record types.
20505
20573
  *
@@ -39836,13 +39904,14 @@ var BaseDeviceProvider = class extends BaseAddon {
39836
39904
  async getStatus() {
39837
39905
  const all = await this.ctx.kernel.devices?.getAll() ?? [];
39838
39906
  const summary = this.restoreFailureSummary();
39839
- if (summary === null) return {
39907
+ const base = {
39840
39908
  connected: true,
39841
- deviceCount: all.length
39909
+ deviceCount: all.length,
39910
+ motionSliceWriter: "orchestrator"
39842
39911
  };
39912
+ if (summary === null) return base;
39843
39913
  return {
39844
- connected: true,
39845
- deviceCount: all.length,
39914
+ ...base,
39846
39915
  error: summary
39847
39916
  };
39848
39917
  }
@@ -45278,6 +45347,12 @@ Object.freeze({
45278
45347
  addonId: null,
45279
45348
  access: "view"
45280
45349
  },
45350
+ "notificationRules.getAlarmSensors": {
45351
+ capName: "notification-rules",
45352
+ capScope: "system",
45353
+ addonId: null,
45354
+ access: "view"
45355
+ },
45281
45356
  "notificationRules.getClipRetentionAsks": {
45282
45357
  capName: "notification-rules",
45283
45358
  capScope: "system",
@@ -49761,6 +49836,11 @@ Object.freeze({
49761
49836
  form: "single",
49762
49837
  optional: false
49763
49838
  }],
49839
+ "notificationRules.getAlarmSensors": [{
49840
+ name: "deviceIds",
49841
+ form: "array",
49842
+ optional: false
49843
+ }],
49764
49844
  "notificationRules.setDeviceMuted": [{
49765
49845
  name: "deviceId",
49766
49846
  form: "single",
package/dist/addon.mjs CHANGED
@@ -14764,7 +14764,17 @@ var SiteZoneSchema = object({
14764
14764
  var ProviderStatusSchema = object({
14765
14765
  connected: boolean(),
14766
14766
  deviceCount: number(),
14767
- error: string().optional()
14767
+ error: string().optional(),
14768
+ /**
14769
+ * `'orchestrator'`: this provider writes NO camera's `motion` runtime slice
14770
+ * — the orchestrator is its only writer (D696). Set by `BaseDeviceProvider`,
14771
+ * which every provider bundles, so only a provider BUILT from D696-amended
14772
+ * code says it. ABSENT means an older provider that may still write it (raw
14773
+ * VMD writes, a boot seed), and the orchestrator's provider gate keeps that
14774
+ * provider's cameras unwritten (`camera-sensors/motion-slice-provider-gate.ts`)
14775
+ * — the provider twin of the runner's `getLocalLoad().motionSliceWriter`.
14776
+ */
14777
+ motionSliceWriter: literal("orchestrator").optional()
14768
14778
  });
14769
14779
  object({
14770
14780
  externalId: string(),
@@ -20226,6 +20236,29 @@ object({
20226
20236
  firstAt: number(),
20227
20237
  lastAt: number()
20228
20238
  });
20239
+ var NC_ALARM_TRIGGERED_MAX_SEC = 3600;
20240
+ var NcAlarmDelaySecSchema = number().int().min(0).max(600);
20241
+ var NcAlarmTriggeredSecSchema = number().int().min(0).max(NC_ALARM_TRIGGERED_MAX_SEC);
20242
+ var uniqueModes = (entries) => new Set(entries.map((e) => e.mode)).size === entries.length;
20243
+ var NcAlarmModeDelaysListSchema = array(object({
20244
+ mode: AlarmArmModeSchema,
20245
+ exitDelaySec: NcAlarmDelaySecSchema.optional(),
20246
+ entryDelaySec: NcAlarmDelaySecSchema.optional(),
20247
+ triggeredDurationSec: NcAlarmTriggeredSecSchema.optional()
20248
+ })).max(8).refine(uniqueModes, { message: "modeDelays: at most one entry per arm mode" });
20249
+ var NcAlarmModeDelaysEditListSchema = array(object({
20250
+ mode: AlarmArmModeSchema,
20251
+ exitDelaySec: NcAlarmDelaySecSchema.nullable().optional(),
20252
+ entryDelaySec: NcAlarmDelaySecSchema.nullable().optional(),
20253
+ triggeredDurationSec: NcAlarmTriggeredSecSchema.nullable().optional()
20254
+ })).max(8).refine(uniqueModes, { message: "modeDelays: at most one edit per arm mode" });
20255
+ /** One switch of the alarm table: put ONE device on, or take it off, ONE mode's list. */
20256
+ var NcAlarmNonBlockingEditSchema = object({
20257
+ mode: AlarmArmModeSchema,
20258
+ deviceId: number().int().positive(),
20259
+ /** true = on the list (may be open, does not block); false = off the list (blocks). */
20260
+ nonBlocking: boolean()
20261
+ });
20229
20262
  /**
20230
20263
  * The per-mode lists, at most one per mode. Two lists for one mode have no
20231
20264
  * meaning a reader could agree on (union? the last one?), so they are refused
@@ -20234,10 +20267,10 @@ object({
20234
20267
  var NcAlarmNonBlockingListSchema = array(object({
20235
20268
  mode: AlarmArmModeSchema,
20236
20269
  deviceIds: array(number().int()).max(200)
20237
- })).max(8).refine((lists) => new Set(lists.map((l) => l.mode)).size === lists.length, { message: "nonBlocking: at most one list per arm mode" });
20270
+ })).max(8).refine(uniqueModes, { message: "nonBlocking: at most one list per arm mode" });
20238
20271
  /**
20239
- * The three durations the panel's state machine runs on, who hears about its
20240
- * transitions, and which openings each mode tolerates.
20272
+ * The three durations — global, and per mode since D697 — the panel's state
20273
+ * machine runs on, who hears about its transitions, and which openings each mode tolerates.
20241
20274
  *
20242
20275
  * They live on the NOTIFICATION-RULES cap, not on `alarm-panel`, on purpose:
20243
20276
  * `alarm-panel` is `deviceNative` and its other provider mirrors somebody
@@ -20247,15 +20280,15 @@ var NcAlarmNonBlockingListSchema = array(object({
20247
20280
  */
20248
20281
  var NcAlarmSettingsSchema = object({
20249
20282
  /** Grace period between arming and the mode taking effect. 0 = immediate. */
20250
- exitDelaySec: number().int().min(0).max(600),
20283
+ exitDelaySec: NcAlarmDelaySecSchema,
20251
20284
  /** Grace period between a trigger and the alarm firing. 0 = immediate. */
20252
- entryDelaySec: number().int().min(0).max(600),
20285
+ entryDelaySec: NcAlarmDelaySecSchema,
20253
20286
  /**
20254
20287
  * How long `triggered` lasts before the panel re-arms itself.
20255
20288
  * **0 = until an operator disarms it** — the behaviour before this field
20256
20289
  * existed, and therefore what an untouched install keeps doing.
20257
20290
  */
20258
- triggeredDurationSec: number().int().min(0).max(3600),
20291
+ triggeredDurationSec: NcAlarmTriggeredSecSchema,
20259
20292
  /**
20260
20293
  * Deprecated by D630 — every transition is announced. Parsed, never read;
20261
20294
  * remove after one release. Optional so an older admin's patch (which still
@@ -20278,17 +20311,30 @@ var NcAlarmSettingsSchema = object({
20278
20311
  * per mode is meaningful. Defaulted to `[]` so a blob stored before D630
20279
20312
  * parses as "every opening blocks" — the behaviour it was written under.
20280
20313
  */
20281
- nonBlocking: NcAlarmNonBlockingListSchema.default([])
20314
+ nonBlocking: NcAlarmNonBlockingListSchema.default([]),
20315
+ /**
20316
+ * Per arm mode, the delays that mode OVERRIDES (D697). A mode with no entry, or an entry
20317
+ * without a field, uses the global value above — so `[]`, the default, is exactly the
20318
+ * behaviour of a blob stored before this field existed. Read through `resolveModeDelays`.
20319
+ */
20320
+ modeDelays: NcAlarmModeDelaysListSchema.default([])
20282
20321
  });
20283
20322
  /**
20284
20323
  * Every field optional — a tab edits one control at a time.
20285
20324
  *
20286
- * `nonBlocking` is re-declared WITHOUT its default: `.partial()` does not
20287
- * remove an inner `.default()`, so a patch that never named the field would
20288
- * parse to `nonBlocking: []` and wipe every mode's list on an unrelated edit
20289
- * (the same trap {@link NcRulePatchSchema} documents).
20325
+ * `nonBlocking` and `modeDelays` are re-declared WITHOUT their defaults:
20326
+ * `.partial()` does not remove an inner `.default()`, so a patch that never
20327
+ * named the field would parse to `[]` and wipe every mode's list on an
20328
+ * unrelated edit (the same trap {@link NcRulePatchSchema} documents). Here
20329
+ * `modeDelays` means EDITS, not the stored overrides.
20290
20330
  */
20291
- var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.extend({ nonBlocking: NcAlarmNonBlockingListSchema }).partial();
20331
+ var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.extend({
20332
+ nonBlocking: NcAlarmNonBlockingListSchema,
20333
+ /** Per-mode, per-field edits — NOT the whole list (see NcAlarmModeDelaysEditSchema). */
20334
+ modeDelays: NcAlarmModeDelaysEditListSchema,
20335
+ /** ONE membership change; the panel applies it to the list it holds at its turn. */
20336
+ nonBlockingEdit: NcAlarmNonBlockingEditSchema
20337
+ }).partial().refine((p) => p.nonBlocking === void 0 || p.nonBlockingEdit === void 0, { message: "nonBlocking and nonBlockingEdit are exclusive — send the whole lists or one edit" });
20292
20338
  /**
20293
20339
  * What one arm mode actually arms, DERIVED from the enabled rules gated on it.
20294
20340
  * Never authored, never stored — see `alarm-mode-coverage.ts` for why a stored
@@ -20388,6 +20434,28 @@ var NcAlarmConfigSchema = object({
20388
20434
  */
20389
20435
  live: NcAlarmLiveSchema.optional()
20390
20436
  });
20437
+ var NcAlarmSensorReadingSchema = _enum([
20438
+ "open",
20439
+ "closed",
20440
+ "unknown",
20441
+ "not-opening"
20442
+ ]);
20443
+ /** One device's state as the alarm reads it — the classifier the arm check uses. */
20444
+ var NcAlarmSensorStateSchema = object({
20445
+ deviceId: number().int().positive(),
20446
+ reading: NcAlarmSensorReadingSchema,
20447
+ /** The word as read (`open`, `closed`, `unlocked`, `stopped`, …); null when unknown / not an opening. */
20448
+ state: string().nullable(),
20449
+ /** Why `unknown`: the read failed, or the slice holds no word its cap defines (a placeholder included). */
20450
+ unknownWhy: _enum(["read-failed", "unknown-word"]).nullable(),
20451
+ /** The device manager's liveness as the centre mirrors it; null = not known. */
20452
+ online: boolean().nullable()
20453
+ });
20454
+ var NcAlarmSensorsInputSchema = object({ deviceIds: array(number().int().positive()).max(500) });
20455
+ var NcAlarmSensorsSchema = object({
20456
+ sensors: array(NcAlarmSensorStateSchema),
20457
+ readAt: number().int().nonnegative()
20458
+ });
20391
20459
  /**
20392
20460
  * ONE rule's demand on ONE camera's clip ring.
20393
20461
  *
@@ -20478,7 +20546,7 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
20478
20546
  }), method(object({}), NcAlarmConfigSchema, { auth: "admin" }), method(object({ patch: NcAlarmSettingsPatchSchema }), NcAlarmConfigSchema, {
20479
20547
  kind: "mutation",
20480
20548
  auth: "admin"
20481
- });
20549
+ }), method(NcAlarmSensorsInputSchema, NcAlarmSensorsSchema, { auth: "admin" });
20482
20550
  /**
20483
20551
  * Zod schemas for persisted record types.
20484
20552
  *
@@ -39815,13 +39883,14 @@ var BaseDeviceProvider = class extends BaseAddon {
39815
39883
  async getStatus() {
39816
39884
  const all = await this.ctx.kernel.devices?.getAll() ?? [];
39817
39885
  const summary = this.restoreFailureSummary();
39818
- if (summary === null) return {
39886
+ const base = {
39819
39887
  connected: true,
39820
- deviceCount: all.length
39888
+ deviceCount: all.length,
39889
+ motionSliceWriter: "orchestrator"
39821
39890
  };
39891
+ if (summary === null) return base;
39822
39892
  return {
39823
- connected: true,
39824
- deviceCount: all.length,
39893
+ ...base,
39825
39894
  error: summary
39826
39895
  };
39827
39896
  }
@@ -45257,6 +45326,12 @@ Object.freeze({
45257
45326
  addonId: null,
45258
45327
  access: "view"
45259
45328
  },
45329
+ "notificationRules.getAlarmSensors": {
45330
+ capName: "notification-rules",
45331
+ capScope: "system",
45332
+ addonId: null,
45333
+ access: "view"
45334
+ },
45260
45335
  "notificationRules.getClipRetentionAsks": {
45261
45336
  capName: "notification-rules",
45262
45337
  capScope: "system",
@@ -49740,6 +49815,11 @@ Object.freeze({
49740
49815
  form: "single",
49741
49816
  optional: false
49742
49817
  }],
49818
+ "notificationRules.getAlarmSensors": [{
49819
+ name: "deviceIds",
49820
+ form: "array",
49821
+ optional: false
49822
+ }],
49743
49823
  "notificationRules.setDeviceMuted": [{
49744
49824
  name: "deviceId",
49745
49825
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-wyze",
3
- "version": "0.2.122",
3
+ "version": "0.2.123",
4
4
  "description": "Wyze camera device-provider addon for CamStack — wraps the @apocaliss92/wyze-bridge-js P2P/DTLS client, feeding the stream-broker via the pull-rfc4571 lazy-publish path (a structural twin of addon-provider-reolink)",
5
5
  "keywords": [
6
6
  "camstack",