@camstack/addon-import-alexa 0.2.26 → 0.2.27

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.
Files changed (3) hide show
  1. package/dist/addon.js +184 -34
  2. package/dist/addon.mjs +184 -34
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -5840,6 +5840,13 @@ var BaseAddon = class {
5840
5840
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5841
5841
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5842
5842
  _registeredCapNames = [];
5843
+ /**
5844
+ * True only after `readAddonStore` actually answered. Constructor
5845
+ * defaults look like stored config when the store is down — a forked
5846
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5847
+ * mode, 2026-08-25) is not "the operator chose this".
5848
+ */
5849
+ settingsStoreReady = false;
5843
5850
  /** Default config values. Provided via constructor. */
5844
5851
  defaults;
5845
5852
  constructor(defaults) {
@@ -6240,7 +6247,9 @@ var BaseAddon = class {
6240
6247
  ];
6241
6248
  let lastErr;
6242
6249
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6243
- return await settings.readAddonStore() ?? {};
6250
+ const stored = await settings.readAddonStore() ?? {};
6251
+ this.settingsStoreReady = true;
6252
+ return stored;
6244
6253
  } catch (err) {
6245
6254
  lastErr = err;
6246
6255
  const msg = err instanceof Error ? err.message : String(err);
@@ -6248,6 +6257,7 @@ var BaseAddon = class {
6248
6257
  if (attempt === delaysMs.length) break;
6249
6258
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6250
6259
  }
6260
+ this.settingsStoreReady = false;
6251
6261
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6252
6262
  return {};
6253
6263
  }
@@ -8270,6 +8280,12 @@ var ModelVariantGroupSchema = object({
8270
8280
  */
8271
8281
  resolution: number().int().positive().optional()
8272
8282
  });
8283
+ var ModelProviderIdSchema = _enum([
8284
+ "camstack",
8285
+ "frigate",
8286
+ "scrypted",
8287
+ "custom"
8288
+ ]);
8273
8289
  var ModelCatalogEntrySchema = object({
8274
8290
  id: string(),
8275
8291
  name: string(),
@@ -8367,6 +8383,12 @@ var ModelCatalogEntrySchema = object({
8367
8383
  */
8368
8384
  group: ModelVariantGroupSchema.optional(),
8369
8385
  /**
8386
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8387
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8388
+ * persisted before this field existed (`inferModelProvider` fills those).
8389
+ */
8390
+ provider: ModelProviderIdSchema.optional(),
8391
+ /**
8370
8392
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8371
8393
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8372
8394
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12243,6 +12265,27 @@ var LinkedDeviceSchema = object({
12243
12265
  features: array(string()),
12244
12266
  producesTrackedEvents: boolean().optional()
12245
12267
  });
12268
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12269
+ * The batch answer needs the tag; the single-device answer already has it
12270
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12271
+ var LinkedDevicesForDeviceSchema = object({
12272
+ deviceId: number(),
12273
+ mode: LinkedDevicesModeSchema,
12274
+ devices: array(LinkedDeviceSchema)
12275
+ });
12276
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12277
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12278
+ * object literal is exactly how the three drift apart. */
12279
+ var DeviceBindingsForDeviceSchema = object({
12280
+ deviceId: number(),
12281
+ entries: array(object({
12282
+ capName: string(),
12283
+ kind: _enum(["native", "wrapped"]),
12284
+ providerAddonId: string(),
12285
+ providerNodeId: string(),
12286
+ nativeAddonId: string()
12287
+ }))
12288
+ });
12246
12289
  var SavedDeviceRowSchema = object({
12247
12290
  /** Numeric id reserved at allocateDeviceId time. */
12248
12291
  id: number(),
@@ -12468,11 +12511,25 @@ method(object({
12468
12511
  projection: _enum(["full", "slim"]).optional(),
12469
12512
  /** Return only camera devices. Filtering server-side instead of
12470
12513
  * shipping 293 rows to find 12. */
12471
- isCamera: boolean().optional()
12514
+ isCamera: boolean().optional(),
12515
+ /**
12516
+ * Return only these device ids. For the caller that already KNOWS the
12517
+ * handful it wants and needs a field the id-bearing answer does not
12518
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12519
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12520
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12521
+ * refetches on the reconcile interval, on a phone.
12522
+ *
12523
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12524
+ * keys rather than rejecting them (verified against the live hub
12525
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12526
+ * it answers today and the caller filters as it already does.
12527
+ */
12528
+ deviceIds: array(number()).optional()
12472
12529
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12473
12530
  mode: LinkedDevicesModeSchema,
12474
12531
  devices: array(LinkedDeviceSchema)
12475
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12532
+ })), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12476
12533
  deviceId: number(),
12477
12534
  values: record(string(), unknown())
12478
12535
  }), object({ success: literal(true) }), {
@@ -12499,25 +12556,7 @@ method(object({
12499
12556
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12500
12557
  kind: "mutation",
12501
12558
  auth: "admin"
12502
- }), method(object({ deviceId: number() }), object({
12503
- deviceId: number(),
12504
- entries: array(object({
12505
- capName: string(),
12506
- kind: _enum(["native", "wrapped"]),
12507
- providerAddonId: string(),
12508
- providerNodeId: string(),
12509
- nativeAddonId: string()
12510
- }))
12511
- })), method(object({}), array(object({
12512
- deviceId: number(),
12513
- entries: array(object({
12514
- capName: string(),
12515
- kind: _enum(["native", "wrapped"]),
12516
- providerAddonId: string(),
12517
- providerNodeId: string(),
12518
- nativeAddonId: string()
12519
- }))
12520
- }))), method(object({
12559
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12521
12560
  deviceId: number(),
12522
12561
  capName: string(),
12523
12562
  wrapperAddonId: string(),
@@ -14927,12 +14966,15 @@ var NcOccupancyConditionSchema = object({
14927
14966
  * there is no second switch that can disagree with the first and every rule
14928
14967
  * authored before the decision migrates for free (`audioModeOf`):
14929
14968
  *
14930
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14931
- * classifier labels with one of them. No window, no percentage:
14932
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14933
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14934
- * the analyzer's (`classificationMinScore`, per device) a label only
14935
- * reaches this condition if the classifier was already confident enough.
14969
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14970
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14971
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14972
+ * frames is the wrong question for a classifier that labels 1–3 frames
14973
+ * per episode. The count window is the brake that drops a single-frame
14974
+ * false positive; the rule's own `throttle` cooldown is the other. The
14975
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14976
+ * per device) — a label only reaches this condition if the classifier was
14977
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14936
14978
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14937
14979
  * the condition: at least `hitPercent`% of the samples over
14938
14980
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14959,14 +15001,22 @@ var NcOccupancyConditionSchema = object({
14959
15001
  * an operator who typed `dog` mean the same thing.
14960
15002
  */
14961
15003
  var NcAudioConditionSchema = object({
14962
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
15004
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14963
15005
  labels: array(string().min(1)).min(1).optional(),
14964
15006
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14965
15007
  dbThreshold: number().min(-96).max(0).optional(),
14966
15008
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14967
15009
  hitPercent: number().int().min(1).max(100).default(60),
14968
15010
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14969
- samplingSeconds: number().int().min(1).max(300).default(10)
15011
+ samplingSeconds: number().int().min(1).max(300).default(10),
15012
+ /**
15013
+ * LABEL MODE: how many labelled frames must land inside
15014
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
15015
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
15016
+ */
15017
+ confirmHits: number().int().min(1).max(20).optional(),
15018
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
15019
+ confirmWindowSec: number().int().min(1).max(60).optional()
14970
15020
  });
14971
15021
  /**
14972
15022
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17340,6 +17390,46 @@ var RecentTracksPageSchema = object({
17340
17390
  /** Cursor for the next page, or null when this page is the last. */
17341
17391
  nextCursor: string().nullable()
17342
17392
  });
17393
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17394
+ var LIST_GROUPS_MAX_LIMIT = 100;
17395
+ var AnalyticsGroupRecordSchema = object({
17396
+ id: string(),
17397
+ deviceId: number().int(),
17398
+ openedAt: number().int(),
17399
+ closedAt: number().int(),
17400
+ timestamp: number().int(),
17401
+ memberCount: number().int(),
17402
+ memberTrackIds: array(string()).readonly(),
17403
+ className: string(),
17404
+ classes: array(string()).readonly(),
17405
+ /** Relative event-media path, or null when the group has no picture yet. */
17406
+ mediaUrl: string().nullable(),
17407
+ singleton: boolean()
17408
+ });
17409
+ var AnalyticsGroupMemberSchema = object({
17410
+ trackId: string(),
17411
+ deviceId: number().int(),
17412
+ className: string(),
17413
+ firstSeen: number().int(),
17414
+ lastSeen: number().int(),
17415
+ mediaUrl: string().nullable()
17416
+ });
17417
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17418
+ var ListGroupsQueryInput = object({
17419
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17420
+ deviceIds: array(number()),
17421
+ /** Window lower bound on `closedAt` (inclusive). */
17422
+ since: number().optional(),
17423
+ /** Window upper bound on `openedAt` (inclusive). */
17424
+ until: number().optional(),
17425
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17426
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17427
+ cursor: string().optional()
17428
+ });
17429
+ var ListGroupsPageSchema = object({
17430
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17431
+ nextCursor: string().nullable()
17432
+ });
17343
17433
  var KeyEventQueryInput = object({
17344
17434
  deviceId: number(),
17345
17435
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17415,7 +17505,9 @@ var TrackCascadeCountsSchema = object({
17415
17505
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17416
17506
  plates: number().int(),
17417
17507
  /** Per-track CLIP search vectors removed (best-effort). */
17418
- embeddings: number().int()
17508
+ embeddings: number().int(),
17509
+ /** Group membership + group rows removed with their last member (best-effort). */
17510
+ groups: number().int()
17419
17511
  });
17420
17512
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17421
17513
  var DiskReconcileCountsSchema = object({
@@ -17561,7 +17653,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17561
17653
  * stationary registry). Default false: the timeline lists passages,
17562
17654
  * not parking records (operator decision, 2026-08-15). */
17563
17655
  includeStationary: boolean().optional()
17564
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17656
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17657
+ deviceId: number(),
17658
+ groupId: string().min(1)
17659
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17565
17660
  kind: "mutation",
17566
17661
  auth: "admin"
17567
17662
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -17871,7 +17966,8 @@ var PipelineModelOptionSchema = object({
17871
17966
  sizeMB: number()
17872
17967
  })),
17873
17968
  group: ModelVariantGroupSchema.optional(),
17874
- legacy: boolean().optional()
17969
+ legacy: boolean().optional(),
17970
+ provider: ModelProviderIdSchema.optional()
17875
17971
  });
17876
17972
  var ConfigFieldBridge = custom();
17877
17973
  var PipelineAddonSchemaSchema = object({
@@ -25985,7 +26081,12 @@ var PlateInfoSchema = object({
25985
26081
  plateBbox: BoundingBoxSchema.optional(),
25986
26082
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25987
26083
  keyFrameMediaKey: string().optional(),
25988
- base64: string().optional()
26084
+ base64: string().optional(),
26085
+ /**
26086
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
26087
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
26088
+ */
26089
+ cropUrl: string().optional()
25989
26090
  });
25990
26091
  var MediaFileLiteSchema = object({
25991
26092
  key: string(),
@@ -31509,6 +31610,12 @@ Object.freeze({
31509
31610
  addonId: null,
31510
31611
  access: "view"
31511
31612
  },
31613
+ "deviceManager.getBindingsBatch": {
31614
+ capName: "device-manager",
31615
+ capScope: "system",
31616
+ addonId: null,
31617
+ access: "view"
31618
+ },
31512
31619
  "deviceManager.getChildren": {
31513
31620
  capName: "device-manager",
31514
31621
  capScope: "system",
@@ -31569,6 +31676,12 @@ Object.freeze({
31569
31676
  addonId: null,
31570
31677
  access: "view"
31571
31678
  },
31679
+ "deviceManager.getLinkedDevicesBatch": {
31680
+ capName: "device-manager",
31681
+ capScope: "system",
31682
+ addonId: null,
31683
+ access: "view"
31684
+ },
31572
31685
  "deviceManager.getRoleDisplayDefaults": {
31573
31686
  capName: "device-manager",
31574
31687
  capScope: "system",
@@ -33339,6 +33452,12 @@ Object.freeze({
33339
33452
  addonId: null,
33340
33453
  access: "view"
33341
33454
  },
33455
+ "pipelineAnalytics.getGroup": {
33456
+ capName: "pipeline-analytics",
33457
+ capScope: "device",
33458
+ addonId: null,
33459
+ access: "view"
33460
+ },
33342
33461
  "pipelineAnalytics.getKeyEvents": {
33343
33462
  capName: "pipeline-analytics",
33344
33463
  capScope: "device",
@@ -33423,6 +33542,12 @@ Object.freeze({
33423
33542
  addonId: null,
33424
33543
  access: "view"
33425
33544
  },
33545
+ "pipelineAnalytics.listGroups": {
33546
+ capName: "pipeline-analytics",
33547
+ capScope: "device",
33548
+ addonId: null,
33549
+ access: "view"
33550
+ },
33426
33551
  "pipelineAnalytics.listOpsLog": {
33427
33552
  capName: "pipeline-analytics",
33428
33553
  capScope: "device",
@@ -36204,6 +36329,11 @@ Object.freeze({
36204
36329
  form: "single",
36205
36330
  optional: false
36206
36331
  }],
36332
+ "deviceManager.getBindingsBatch": [{
36333
+ name: "deviceIds",
36334
+ form: "array",
36335
+ optional: false
36336
+ }],
36207
36337
  "deviceManager.getChildren": [{
36208
36338
  name: "parentDeviceId",
36209
36339
  form: "single",
@@ -36249,6 +36379,11 @@ Object.freeze({
36249
36379
  form: "single",
36250
36380
  optional: false
36251
36381
  }],
36382
+ "deviceManager.getLinkedDevicesBatch": [{
36383
+ name: "deviceIds",
36384
+ form: "array",
36385
+ optional: false
36386
+ }],
36252
36387
  "deviceManager.getSettingsSchema": [{
36253
36388
  name: "deviceId",
36254
36389
  form: "single",
@@ -36269,6 +36404,11 @@ Object.freeze({
36269
36404
  form: "single",
36270
36405
  optional: false
36271
36406
  }],
36407
+ "deviceManager.listAll": [{
36408
+ name: "deviceIds",
36409
+ form: "array",
36410
+ optional: true
36411
+ }],
36272
36412
  "deviceManager.loadConfig": [{
36273
36413
  name: "deviceId",
36274
36414
  form: "single",
@@ -36842,6 +36982,11 @@ Object.freeze({
36842
36982
  form: "single",
36843
36983
  optional: false
36844
36984
  }],
36985
+ "pipelineAnalytics.getGroup": [{
36986
+ name: "deviceId",
36987
+ form: "single",
36988
+ optional: false
36989
+ }],
36845
36990
  "pipelineAnalytics.getKeyEvents": [{
36846
36991
  name: "deviceId",
36847
36992
  form: "single",
@@ -36897,6 +37042,11 @@ Object.freeze({
36897
37042
  form: "array",
36898
37043
  optional: false
36899
37044
  }],
37045
+ "pipelineAnalytics.listGroups": [{
37046
+ name: "deviceIds",
37047
+ form: "array",
37048
+ optional: false
37049
+ }],
36900
37050
  "pipelineAnalytics.listOpsLog": [{
36901
37051
  name: "deviceId",
36902
37052
  form: "single",
package/dist/addon.mjs CHANGED
@@ -5840,6 +5840,13 @@ var BaseAddon = class {
5840
5840
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5841
5841
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5842
5842
  _registeredCapNames = [];
5843
+ /**
5844
+ * True only after `readAddonStore` actually answered. Constructor
5845
+ * defaults look like stored config when the store is down — a forked
5846
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5847
+ * mode, 2026-08-25) is not "the operator chose this".
5848
+ */
5849
+ settingsStoreReady = false;
5843
5850
  /** Default config values. Provided via constructor. */
5844
5851
  defaults;
5845
5852
  constructor(defaults) {
@@ -6240,7 +6247,9 @@ var BaseAddon = class {
6240
6247
  ];
6241
6248
  let lastErr;
6242
6249
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6243
- return await settings.readAddonStore() ?? {};
6250
+ const stored = await settings.readAddonStore() ?? {};
6251
+ this.settingsStoreReady = true;
6252
+ return stored;
6244
6253
  } catch (err) {
6245
6254
  lastErr = err;
6246
6255
  const msg = err instanceof Error ? err.message : String(err);
@@ -6248,6 +6257,7 @@ var BaseAddon = class {
6248
6257
  if (attempt === delaysMs.length) break;
6249
6258
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6250
6259
  }
6260
+ this.settingsStoreReady = false;
6251
6261
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6252
6262
  return {};
6253
6263
  }
@@ -8270,6 +8280,12 @@ var ModelVariantGroupSchema = object({
8270
8280
  */
8271
8281
  resolution: number().int().positive().optional()
8272
8282
  });
8283
+ var ModelProviderIdSchema = _enum([
8284
+ "camstack",
8285
+ "frigate",
8286
+ "scrypted",
8287
+ "custom"
8288
+ ]);
8273
8289
  var ModelCatalogEntrySchema = object({
8274
8290
  id: string(),
8275
8291
  name: string(),
@@ -8367,6 +8383,12 @@ var ModelCatalogEntrySchema = object({
8367
8383
  */
8368
8384
  group: ModelVariantGroupSchema.optional(),
8369
8385
  /**
8386
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8387
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8388
+ * persisted before this field existed (`inferModelProvider` fills those).
8389
+ */
8390
+ provider: ModelProviderIdSchema.optional(),
8391
+ /**
8370
8392
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8371
8393
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8372
8394
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12243,6 +12265,27 @@ var LinkedDeviceSchema = object({
12243
12265
  features: array(string()),
12244
12266
  producesTrackedEvents: boolean().optional()
12245
12267
  });
12268
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12269
+ * The batch answer needs the tag; the single-device answer already has it
12270
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12271
+ var LinkedDevicesForDeviceSchema = object({
12272
+ deviceId: number(),
12273
+ mode: LinkedDevicesModeSchema,
12274
+ devices: array(LinkedDeviceSchema)
12275
+ });
12276
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12277
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12278
+ * object literal is exactly how the three drift apart. */
12279
+ var DeviceBindingsForDeviceSchema = object({
12280
+ deviceId: number(),
12281
+ entries: array(object({
12282
+ capName: string(),
12283
+ kind: _enum(["native", "wrapped"]),
12284
+ providerAddonId: string(),
12285
+ providerNodeId: string(),
12286
+ nativeAddonId: string()
12287
+ }))
12288
+ });
12246
12289
  var SavedDeviceRowSchema = object({
12247
12290
  /** Numeric id reserved at allocateDeviceId time. */
12248
12291
  id: number(),
@@ -12468,11 +12511,25 @@ method(object({
12468
12511
  projection: _enum(["full", "slim"]).optional(),
12469
12512
  /** Return only camera devices. Filtering server-side instead of
12470
12513
  * shipping 293 rows to find 12. */
12471
- isCamera: boolean().optional()
12514
+ isCamera: boolean().optional(),
12515
+ /**
12516
+ * Return only these device ids. For the caller that already KNOWS the
12517
+ * handful it wants and needs a field the id-bearing answer does not
12518
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12519
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12520
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12521
+ * refetches on the reconcile interval, on a phone.
12522
+ *
12523
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12524
+ * keys rather than rejecting them (verified against the live hub
12525
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12526
+ * it answers today and the caller filters as it already does.
12527
+ */
12528
+ deviceIds: array(number()).optional()
12472
12529
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12473
12530
  mode: LinkedDevicesModeSchema,
12474
12531
  devices: array(LinkedDeviceSchema)
12475
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12532
+ })), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12476
12533
  deviceId: number(),
12477
12534
  values: record(string(), unknown())
12478
12535
  }), object({ success: literal(true) }), {
@@ -12499,25 +12556,7 @@ method(object({
12499
12556
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12500
12557
  kind: "mutation",
12501
12558
  auth: "admin"
12502
- }), method(object({ deviceId: number() }), object({
12503
- deviceId: number(),
12504
- entries: array(object({
12505
- capName: string(),
12506
- kind: _enum(["native", "wrapped"]),
12507
- providerAddonId: string(),
12508
- providerNodeId: string(),
12509
- nativeAddonId: string()
12510
- }))
12511
- })), method(object({}), array(object({
12512
- deviceId: number(),
12513
- entries: array(object({
12514
- capName: string(),
12515
- kind: _enum(["native", "wrapped"]),
12516
- providerAddonId: string(),
12517
- providerNodeId: string(),
12518
- nativeAddonId: string()
12519
- }))
12520
- }))), method(object({
12559
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12521
12560
  deviceId: number(),
12522
12561
  capName: string(),
12523
12562
  wrapperAddonId: string(),
@@ -14927,12 +14966,15 @@ var NcOccupancyConditionSchema = object({
14927
14966
  * there is no second switch that can disagree with the first and every rule
14928
14967
  * authored before the decision migrates for free (`audioModeOf`):
14929
14968
  *
14930
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14931
- * classifier labels with one of them. No window, no percentage:
14932
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14933
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14934
- * the analyzer's (`classificationMinScore`, per device) a label only
14935
- * reaches this condition if the classifier was already confident enough.
14969
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14970
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14971
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14972
+ * frames is the wrong question for a classifier that labels 1–3 frames
14973
+ * per episode. The count window is the brake that drops a single-frame
14974
+ * false positive; the rule's own `throttle` cooldown is the other. The
14975
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14976
+ * per device) — a label only reaches this condition if the classifier was
14977
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14936
14978
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14937
14979
  * the condition: at least `hitPercent`% of the samples over
14938
14980
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14959,14 +15001,22 @@ var NcOccupancyConditionSchema = object({
14959
15001
  * an operator who typed `dog` mean the same thing.
14960
15002
  */
14961
15003
  var NcAudioConditionSchema = object({
14962
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
15004
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14963
15005
  labels: array(string().min(1)).min(1).optional(),
14964
15006
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14965
15007
  dbThreshold: number().min(-96).max(0).optional(),
14966
15008
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14967
15009
  hitPercent: number().int().min(1).max(100).default(60),
14968
15010
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14969
- samplingSeconds: number().int().min(1).max(300).default(10)
15011
+ samplingSeconds: number().int().min(1).max(300).default(10),
15012
+ /**
15013
+ * LABEL MODE: how many labelled frames must land inside
15014
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
15015
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
15016
+ */
15017
+ confirmHits: number().int().min(1).max(20).optional(),
15018
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
15019
+ confirmWindowSec: number().int().min(1).max(60).optional()
14970
15020
  });
14971
15021
  /**
14972
15022
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17340,6 +17390,46 @@ var RecentTracksPageSchema = object({
17340
17390
  /** Cursor for the next page, or null when this page is the last. */
17341
17391
  nextCursor: string().nullable()
17342
17392
  });
17393
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17394
+ var LIST_GROUPS_MAX_LIMIT = 100;
17395
+ var AnalyticsGroupRecordSchema = object({
17396
+ id: string(),
17397
+ deviceId: number().int(),
17398
+ openedAt: number().int(),
17399
+ closedAt: number().int(),
17400
+ timestamp: number().int(),
17401
+ memberCount: number().int(),
17402
+ memberTrackIds: array(string()).readonly(),
17403
+ className: string(),
17404
+ classes: array(string()).readonly(),
17405
+ /** Relative event-media path, or null when the group has no picture yet. */
17406
+ mediaUrl: string().nullable(),
17407
+ singleton: boolean()
17408
+ });
17409
+ var AnalyticsGroupMemberSchema = object({
17410
+ trackId: string(),
17411
+ deviceId: number().int(),
17412
+ className: string(),
17413
+ firstSeen: number().int(),
17414
+ lastSeen: number().int(),
17415
+ mediaUrl: string().nullable()
17416
+ });
17417
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17418
+ var ListGroupsQueryInput = object({
17419
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17420
+ deviceIds: array(number()),
17421
+ /** Window lower bound on `closedAt` (inclusive). */
17422
+ since: number().optional(),
17423
+ /** Window upper bound on `openedAt` (inclusive). */
17424
+ until: number().optional(),
17425
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17426
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17427
+ cursor: string().optional()
17428
+ });
17429
+ var ListGroupsPageSchema = object({
17430
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17431
+ nextCursor: string().nullable()
17432
+ });
17343
17433
  var KeyEventQueryInput = object({
17344
17434
  deviceId: number(),
17345
17435
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17415,7 +17505,9 @@ var TrackCascadeCountsSchema = object({
17415
17505
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17416
17506
  plates: number().int(),
17417
17507
  /** Per-track CLIP search vectors removed (best-effort). */
17418
- embeddings: number().int()
17508
+ embeddings: number().int(),
17509
+ /** Group membership + group rows removed with their last member (best-effort). */
17510
+ groups: number().int()
17419
17511
  });
17420
17512
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17421
17513
  var DiskReconcileCountsSchema = object({
@@ -17561,7 +17653,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17561
17653
  * stationary registry). Default false: the timeline lists passages,
17562
17654
  * not parking records (operator decision, 2026-08-15). */
17563
17655
  includeStationary: boolean().optional()
17564
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17656
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17657
+ deviceId: number(),
17658
+ groupId: string().min(1)
17659
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17565
17660
  kind: "mutation",
17566
17661
  auth: "admin"
17567
17662
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -17871,7 +17966,8 @@ var PipelineModelOptionSchema = object({
17871
17966
  sizeMB: number()
17872
17967
  })),
17873
17968
  group: ModelVariantGroupSchema.optional(),
17874
- legacy: boolean().optional()
17969
+ legacy: boolean().optional(),
17970
+ provider: ModelProviderIdSchema.optional()
17875
17971
  });
17876
17972
  var ConfigFieldBridge = custom();
17877
17973
  var PipelineAddonSchemaSchema = object({
@@ -25985,7 +26081,12 @@ var PlateInfoSchema = object({
25985
26081
  plateBbox: BoundingBoxSchema.optional(),
25986
26082
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25987
26083
  keyFrameMediaKey: string().optional(),
25988
- base64: string().optional()
26084
+ base64: string().optional(),
26085
+ /**
26086
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
26087
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
26088
+ */
26089
+ cropUrl: string().optional()
25989
26090
  });
25990
26091
  var MediaFileLiteSchema = object({
25991
26092
  key: string(),
@@ -31509,6 +31610,12 @@ Object.freeze({
31509
31610
  addonId: null,
31510
31611
  access: "view"
31511
31612
  },
31613
+ "deviceManager.getBindingsBatch": {
31614
+ capName: "device-manager",
31615
+ capScope: "system",
31616
+ addonId: null,
31617
+ access: "view"
31618
+ },
31512
31619
  "deviceManager.getChildren": {
31513
31620
  capName: "device-manager",
31514
31621
  capScope: "system",
@@ -31569,6 +31676,12 @@ Object.freeze({
31569
31676
  addonId: null,
31570
31677
  access: "view"
31571
31678
  },
31679
+ "deviceManager.getLinkedDevicesBatch": {
31680
+ capName: "device-manager",
31681
+ capScope: "system",
31682
+ addonId: null,
31683
+ access: "view"
31684
+ },
31572
31685
  "deviceManager.getRoleDisplayDefaults": {
31573
31686
  capName: "device-manager",
31574
31687
  capScope: "system",
@@ -33339,6 +33452,12 @@ Object.freeze({
33339
33452
  addonId: null,
33340
33453
  access: "view"
33341
33454
  },
33455
+ "pipelineAnalytics.getGroup": {
33456
+ capName: "pipeline-analytics",
33457
+ capScope: "device",
33458
+ addonId: null,
33459
+ access: "view"
33460
+ },
33342
33461
  "pipelineAnalytics.getKeyEvents": {
33343
33462
  capName: "pipeline-analytics",
33344
33463
  capScope: "device",
@@ -33423,6 +33542,12 @@ Object.freeze({
33423
33542
  addonId: null,
33424
33543
  access: "view"
33425
33544
  },
33545
+ "pipelineAnalytics.listGroups": {
33546
+ capName: "pipeline-analytics",
33547
+ capScope: "device",
33548
+ addonId: null,
33549
+ access: "view"
33550
+ },
33426
33551
  "pipelineAnalytics.listOpsLog": {
33427
33552
  capName: "pipeline-analytics",
33428
33553
  capScope: "device",
@@ -36204,6 +36329,11 @@ Object.freeze({
36204
36329
  form: "single",
36205
36330
  optional: false
36206
36331
  }],
36332
+ "deviceManager.getBindingsBatch": [{
36333
+ name: "deviceIds",
36334
+ form: "array",
36335
+ optional: false
36336
+ }],
36207
36337
  "deviceManager.getChildren": [{
36208
36338
  name: "parentDeviceId",
36209
36339
  form: "single",
@@ -36249,6 +36379,11 @@ Object.freeze({
36249
36379
  form: "single",
36250
36380
  optional: false
36251
36381
  }],
36382
+ "deviceManager.getLinkedDevicesBatch": [{
36383
+ name: "deviceIds",
36384
+ form: "array",
36385
+ optional: false
36386
+ }],
36252
36387
  "deviceManager.getSettingsSchema": [{
36253
36388
  name: "deviceId",
36254
36389
  form: "single",
@@ -36269,6 +36404,11 @@ Object.freeze({
36269
36404
  form: "single",
36270
36405
  optional: false
36271
36406
  }],
36407
+ "deviceManager.listAll": [{
36408
+ name: "deviceIds",
36409
+ form: "array",
36410
+ optional: true
36411
+ }],
36272
36412
  "deviceManager.loadConfig": [{
36273
36413
  name: "deviceId",
36274
36414
  form: "single",
@@ -36842,6 +36982,11 @@ Object.freeze({
36842
36982
  form: "single",
36843
36983
  optional: false
36844
36984
  }],
36985
+ "pipelineAnalytics.getGroup": [{
36986
+ name: "deviceId",
36987
+ form: "single",
36988
+ optional: false
36989
+ }],
36845
36990
  "pipelineAnalytics.getKeyEvents": [{
36846
36991
  name: "deviceId",
36847
36992
  form: "single",
@@ -36897,6 +37042,11 @@ Object.freeze({
36897
37042
  form: "array",
36898
37043
  optional: false
36899
37044
  }],
37045
+ "pipelineAnalytics.listGroups": [{
37046
+ name: "deviceIds",
37047
+ form: "array",
37048
+ optional: false
37049
+ }],
36900
37050
  "pipelineAnalytics.listOpsLog": [{
36901
37051
  name: "deviceId",
36902
37052
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-import-alexa",
3
- "version": "0.2.26",
3
+ "version": "0.2.27",
4
4
  "description": "Alexa device-import provider for CamStack — imports the smart-home devices in a user's Alexa account via the unofficial alexa-remote2 cookie/token client (the inverse of the Alexa exporter)",
5
5
  "keywords": [
6
6
  "camstack",