@camstack/addon-provider-dreo 0.2.27 → 0.2.28

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
@@ -5837,6 +5837,13 @@ var BaseAddon = class {
5837
5837
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5838
5838
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5839
5839
  _registeredCapNames = [];
5840
+ /**
5841
+ * True only after `readAddonStore` actually answered. Constructor
5842
+ * defaults look like stored config when the store is down — a forked
5843
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5844
+ * mode, 2026-08-25) is not "the operator chose this".
5845
+ */
5846
+ settingsStoreReady = false;
5840
5847
  /** Default config values. Provided via constructor. */
5841
5848
  defaults;
5842
5849
  constructor(defaults) {
@@ -6237,7 +6244,9 @@ var BaseAddon = class {
6237
6244
  ];
6238
6245
  let lastErr;
6239
6246
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6240
- return await settings.readAddonStore() ?? {};
6247
+ const stored = await settings.readAddonStore() ?? {};
6248
+ this.settingsStoreReady = true;
6249
+ return stored;
6241
6250
  } catch (err) {
6242
6251
  lastErr = err;
6243
6252
  const msg = err instanceof Error ? err.message : String(err);
@@ -6245,6 +6254,7 @@ var BaseAddon = class {
6245
6254
  if (attempt === delaysMs.length) break;
6246
6255
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6247
6256
  }
6257
+ this.settingsStoreReady = false;
6248
6258
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6249
6259
  return {};
6250
6260
  }
@@ -8148,6 +8158,12 @@ var ModelVariantGroupSchema = object({
8148
8158
  */
8149
8159
  resolution: number().int().positive().optional()
8150
8160
  });
8161
+ var ModelProviderIdSchema = _enum([
8162
+ "camstack",
8163
+ "frigate",
8164
+ "scrypted",
8165
+ "custom"
8166
+ ]);
8151
8167
  var ModelCatalogEntrySchema = object({
8152
8168
  id: string(),
8153
8169
  name: string(),
@@ -8245,6 +8261,12 @@ var ModelCatalogEntrySchema = object({
8245
8261
  */
8246
8262
  group: ModelVariantGroupSchema.optional(),
8247
8263
  /**
8264
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8265
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8266
+ * persisted before this field existed (`inferModelProvider` fills those).
8267
+ */
8268
+ provider: ModelProviderIdSchema.optional(),
8269
+ /**
8248
8270
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8249
8271
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8250
8272
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12053,6 +12075,27 @@ var LinkedDeviceSchema = object({
12053
12075
  features: array(string()),
12054
12076
  producesTrackedEvents: boolean().optional()
12055
12077
  });
12078
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12079
+ * The batch answer needs the tag; the single-device answer already has it
12080
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12081
+ var LinkedDevicesForDeviceSchema = object({
12082
+ deviceId: number(),
12083
+ mode: LinkedDevicesModeSchema,
12084
+ devices: array(LinkedDeviceSchema)
12085
+ });
12086
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12087
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12088
+ * object literal is exactly how the three drift apart. */
12089
+ var DeviceBindingsForDeviceSchema = object({
12090
+ deviceId: number(),
12091
+ entries: array(object({
12092
+ capName: string(),
12093
+ kind: _enum(["native", "wrapped"]),
12094
+ providerAddonId: string(),
12095
+ providerNodeId: string(),
12096
+ nativeAddonId: string()
12097
+ }))
12098
+ });
12056
12099
  var SavedDeviceRowSchema = object({
12057
12100
  /** Numeric id reserved at allocateDeviceId time. */
12058
12101
  id: number(),
@@ -12278,11 +12321,25 @@ method(object({
12278
12321
  projection: _enum(["full", "slim"]).optional(),
12279
12322
  /** Return only camera devices. Filtering server-side instead of
12280
12323
  * shipping 293 rows to find 12. */
12281
- isCamera: boolean().optional()
12324
+ isCamera: boolean().optional(),
12325
+ /**
12326
+ * Return only these device ids. For the caller that already KNOWS the
12327
+ * handful it wants and needs a field the id-bearing answer does not
12328
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12329
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12330
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12331
+ * refetches on the reconcile interval, on a phone.
12332
+ *
12333
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12334
+ * keys rather than rejecting them (verified against the live hub
12335
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12336
+ * it answers today and the caller filters as it already does.
12337
+ */
12338
+ deviceIds: array(number()).optional()
12282
12339
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12283
12340
  mode: LinkedDevicesModeSchema,
12284
12341
  devices: array(LinkedDeviceSchema)
12285
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12342
+ })), 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({
12286
12343
  deviceId: number(),
12287
12344
  values: record(string(), unknown())
12288
12345
  }), object({ success: literal(true) }), {
@@ -12309,25 +12366,7 @@ method(object({
12309
12366
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12310
12367
  kind: "mutation",
12311
12368
  auth: "admin"
12312
- }), method(object({ deviceId: number() }), object({
12313
- deviceId: number(),
12314
- entries: array(object({
12315
- capName: string(),
12316
- kind: _enum(["native", "wrapped"]),
12317
- providerAddonId: string(),
12318
- providerNodeId: string(),
12319
- nativeAddonId: string()
12320
- }))
12321
- })), method(object({}), array(object({
12322
- deviceId: number(),
12323
- entries: array(object({
12324
- capName: string(),
12325
- kind: _enum(["native", "wrapped"]),
12326
- providerAddonId: string(),
12327
- providerNodeId: string(),
12328
- nativeAddonId: string()
12329
- }))
12330
- }))), method(object({
12369
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12331
12370
  deviceId: number(),
12332
12371
  capName: string(),
12333
12372
  wrapperAddonId: string(),
@@ -14737,12 +14776,15 @@ var NcOccupancyConditionSchema = object({
14737
14776
  * there is no second switch that can disagree with the first and every rule
14738
14777
  * authored before the decision migrates for free (`audioModeOf`):
14739
14778
  *
14740
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14741
- * classifier labels with one of them. No window, no percentage:
14742
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14743
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14744
- * the analyzer's (`classificationMinScore`, per device) a label only
14745
- * reaches this condition if the classifier was already confident enough.
14779
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14780
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14781
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14782
+ * frames is the wrong question for a classifier that labels 1–3 frames
14783
+ * per episode. The count window is the brake that drops a single-frame
14784
+ * false positive; the rule's own `throttle` cooldown is the other. The
14785
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14786
+ * per device) — a label only reaches this condition if the classifier was
14787
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14746
14788
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14747
14789
  * the condition: at least `hitPercent`% of the samples over
14748
14790
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14769,14 +14811,22 @@ var NcOccupancyConditionSchema = object({
14769
14811
  * an operator who typed `dog` mean the same thing.
14770
14812
  */
14771
14813
  var NcAudioConditionSchema = object({
14772
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14814
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14773
14815
  labels: array(string().min(1)).min(1).optional(),
14774
14816
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14775
14817
  dbThreshold: number().min(-96).max(0).optional(),
14776
14818
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14777
14819
  hitPercent: number().int().min(1).max(100).default(60),
14778
14820
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14779
- samplingSeconds: number().int().min(1).max(300).default(10)
14821
+ samplingSeconds: number().int().min(1).max(300).default(10),
14822
+ /**
14823
+ * LABEL MODE: how many labelled frames must land inside
14824
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14825
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14826
+ */
14827
+ confirmHits: number().int().min(1).max(20).optional(),
14828
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14829
+ confirmWindowSec: number().int().min(1).max(60).optional()
14780
14830
  });
14781
14831
  /**
14782
14832
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17150,6 +17200,46 @@ var RecentTracksPageSchema = object({
17150
17200
  /** Cursor for the next page, or null when this page is the last. */
17151
17201
  nextCursor: string().nullable()
17152
17202
  });
17203
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17204
+ var LIST_GROUPS_MAX_LIMIT = 100;
17205
+ var AnalyticsGroupRecordSchema = object({
17206
+ id: string(),
17207
+ deviceId: number().int(),
17208
+ openedAt: number().int(),
17209
+ closedAt: number().int(),
17210
+ timestamp: number().int(),
17211
+ memberCount: number().int(),
17212
+ memberTrackIds: array(string()).readonly(),
17213
+ className: string(),
17214
+ classes: array(string()).readonly(),
17215
+ /** Relative event-media path, or null when the group has no picture yet. */
17216
+ mediaUrl: string().nullable(),
17217
+ singleton: boolean()
17218
+ });
17219
+ var AnalyticsGroupMemberSchema = object({
17220
+ trackId: string(),
17221
+ deviceId: number().int(),
17222
+ className: string(),
17223
+ firstSeen: number().int(),
17224
+ lastSeen: number().int(),
17225
+ mediaUrl: string().nullable()
17226
+ });
17227
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17228
+ var ListGroupsQueryInput = object({
17229
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17230
+ deviceIds: array(number()),
17231
+ /** Window lower bound on `closedAt` (inclusive). */
17232
+ since: number().optional(),
17233
+ /** Window upper bound on `openedAt` (inclusive). */
17234
+ until: number().optional(),
17235
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17236
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17237
+ cursor: string().optional()
17238
+ });
17239
+ var ListGroupsPageSchema = object({
17240
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17241
+ nextCursor: string().nullable()
17242
+ });
17153
17243
  var KeyEventQueryInput = object({
17154
17244
  deviceId: number(),
17155
17245
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17225,7 +17315,9 @@ var TrackCascadeCountsSchema = object({
17225
17315
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17226
17316
  plates: number().int(),
17227
17317
  /** Per-track CLIP search vectors removed (best-effort). */
17228
- embeddings: number().int()
17318
+ embeddings: number().int(),
17319
+ /** Group membership + group rows removed with their last member (best-effort). */
17320
+ groups: number().int()
17229
17321
  });
17230
17322
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17231
17323
  var DiskReconcileCountsSchema = object({
@@ -17371,7 +17463,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17371
17463
  * stationary registry). Default false: the timeline lists passages,
17372
17464
  * not parking records (operator decision, 2026-08-15). */
17373
17465
  includeStationary: boolean().optional()
17374
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17466
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17467
+ deviceId: number(),
17468
+ groupId: string().min(1)
17469
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17375
17470
  kind: "mutation",
17376
17471
  auth: "admin"
17377
17472
  }), 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({
@@ -17681,7 +17776,8 @@ var PipelineModelOptionSchema = object({
17681
17776
  sizeMB: number()
17682
17777
  })),
17683
17778
  group: ModelVariantGroupSchema.optional(),
17684
- legacy: boolean().optional()
17779
+ legacy: boolean().optional(),
17780
+ provider: ModelProviderIdSchema.optional()
17685
17781
  });
17686
17782
  var ConfigFieldBridge = custom();
17687
17783
  var PipelineAddonSchemaSchema = object({
@@ -25786,7 +25882,12 @@ var PlateInfoSchema = object({
25786
25882
  plateBbox: BoundingBoxSchema.optional(),
25787
25883
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25788
25884
  keyFrameMediaKey: string().optional(),
25789
- base64: string().optional()
25885
+ base64: string().optional(),
25886
+ /**
25887
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25888
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25889
+ */
25890
+ cropUrl: string().optional()
25790
25891
  });
25791
25892
  var MediaFileLiteSchema = object({
25792
25893
  key: string(),
@@ -31310,6 +31411,12 @@ Object.freeze({
31310
31411
  addonId: null,
31311
31412
  access: "view"
31312
31413
  },
31414
+ "deviceManager.getBindingsBatch": {
31415
+ capName: "device-manager",
31416
+ capScope: "system",
31417
+ addonId: null,
31418
+ access: "view"
31419
+ },
31313
31420
  "deviceManager.getChildren": {
31314
31421
  capName: "device-manager",
31315
31422
  capScope: "system",
@@ -31370,6 +31477,12 @@ Object.freeze({
31370
31477
  addonId: null,
31371
31478
  access: "view"
31372
31479
  },
31480
+ "deviceManager.getLinkedDevicesBatch": {
31481
+ capName: "device-manager",
31482
+ capScope: "system",
31483
+ addonId: null,
31484
+ access: "view"
31485
+ },
31373
31486
  "deviceManager.getRoleDisplayDefaults": {
31374
31487
  capName: "device-manager",
31375
31488
  capScope: "system",
@@ -33140,6 +33253,12 @@ Object.freeze({
33140
33253
  addonId: null,
33141
33254
  access: "view"
33142
33255
  },
33256
+ "pipelineAnalytics.getGroup": {
33257
+ capName: "pipeline-analytics",
33258
+ capScope: "device",
33259
+ addonId: null,
33260
+ access: "view"
33261
+ },
33143
33262
  "pipelineAnalytics.getKeyEvents": {
33144
33263
  capName: "pipeline-analytics",
33145
33264
  capScope: "device",
@@ -33224,6 +33343,12 @@ Object.freeze({
33224
33343
  addonId: null,
33225
33344
  access: "view"
33226
33345
  },
33346
+ "pipelineAnalytics.listGroups": {
33347
+ capName: "pipeline-analytics",
33348
+ capScope: "device",
33349
+ addonId: null,
33350
+ access: "view"
33351
+ },
33227
33352
  "pipelineAnalytics.listOpsLog": {
33228
33353
  capName: "pipeline-analytics",
33229
33354
  capScope: "device",
@@ -36005,6 +36130,11 @@ Object.freeze({
36005
36130
  form: "single",
36006
36131
  optional: false
36007
36132
  }],
36133
+ "deviceManager.getBindingsBatch": [{
36134
+ name: "deviceIds",
36135
+ form: "array",
36136
+ optional: false
36137
+ }],
36008
36138
  "deviceManager.getChildren": [{
36009
36139
  name: "parentDeviceId",
36010
36140
  form: "single",
@@ -36050,6 +36180,11 @@ Object.freeze({
36050
36180
  form: "single",
36051
36181
  optional: false
36052
36182
  }],
36183
+ "deviceManager.getLinkedDevicesBatch": [{
36184
+ name: "deviceIds",
36185
+ form: "array",
36186
+ optional: false
36187
+ }],
36053
36188
  "deviceManager.getSettingsSchema": [{
36054
36189
  name: "deviceId",
36055
36190
  form: "single",
@@ -36070,6 +36205,11 @@ Object.freeze({
36070
36205
  form: "single",
36071
36206
  optional: false
36072
36207
  }],
36208
+ "deviceManager.listAll": [{
36209
+ name: "deviceIds",
36210
+ form: "array",
36211
+ optional: true
36212
+ }],
36073
36213
  "deviceManager.loadConfig": [{
36074
36214
  name: "deviceId",
36075
36215
  form: "single",
@@ -36643,6 +36783,11 @@ Object.freeze({
36643
36783
  form: "single",
36644
36784
  optional: false
36645
36785
  }],
36786
+ "pipelineAnalytics.getGroup": [{
36787
+ name: "deviceId",
36788
+ form: "single",
36789
+ optional: false
36790
+ }],
36646
36791
  "pipelineAnalytics.getKeyEvents": [{
36647
36792
  name: "deviceId",
36648
36793
  form: "single",
@@ -36698,6 +36843,11 @@ Object.freeze({
36698
36843
  form: "array",
36699
36844
  optional: false
36700
36845
  }],
36846
+ "pipelineAnalytics.listGroups": [{
36847
+ name: "deviceIds",
36848
+ form: "array",
36849
+ optional: false
36850
+ }],
36701
36851
  "pipelineAnalytics.listOpsLog": [{
36702
36852
  name: "deviceId",
36703
36853
  form: "single",
package/dist/addon.mjs CHANGED
@@ -5838,6 +5838,13 @@ var BaseAddon = class {
5838
5838
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5839
5839
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5840
5840
  _registeredCapNames = [];
5841
+ /**
5842
+ * True only after `readAddonStore` actually answered. Constructor
5843
+ * defaults look like stored config when the store is down — a forked
5844
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5845
+ * mode, 2026-08-25) is not "the operator chose this".
5846
+ */
5847
+ settingsStoreReady = false;
5841
5848
  /** Default config values. Provided via constructor. */
5842
5849
  defaults;
5843
5850
  constructor(defaults) {
@@ -6238,7 +6245,9 @@ var BaseAddon = class {
6238
6245
  ];
6239
6246
  let lastErr;
6240
6247
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6241
- return await settings.readAddonStore() ?? {};
6248
+ const stored = await settings.readAddonStore() ?? {};
6249
+ this.settingsStoreReady = true;
6250
+ return stored;
6242
6251
  } catch (err) {
6243
6252
  lastErr = err;
6244
6253
  const msg = err instanceof Error ? err.message : String(err);
@@ -6246,6 +6255,7 @@ var BaseAddon = class {
6246
6255
  if (attempt === delaysMs.length) break;
6247
6256
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6248
6257
  }
6258
+ this.settingsStoreReady = false;
6249
6259
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6250
6260
  return {};
6251
6261
  }
@@ -8149,6 +8159,12 @@ var ModelVariantGroupSchema = object({
8149
8159
  */
8150
8160
  resolution: number().int().positive().optional()
8151
8161
  });
8162
+ var ModelProviderIdSchema = _enum([
8163
+ "camstack",
8164
+ "frigate",
8165
+ "scrypted",
8166
+ "custom"
8167
+ ]);
8152
8168
  var ModelCatalogEntrySchema = object({
8153
8169
  id: string(),
8154
8170
  name: string(),
@@ -8246,6 +8262,12 @@ var ModelCatalogEntrySchema = object({
8246
8262
  */
8247
8263
  group: ModelVariantGroupSchema.optional(),
8248
8264
  /**
8265
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8266
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8267
+ * persisted before this field existed (`inferModelProvider` fills those).
8268
+ */
8269
+ provider: ModelProviderIdSchema.optional(),
8270
+ /**
8249
8271
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8250
8272
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8251
8273
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12054,6 +12076,27 @@ var LinkedDeviceSchema = object({
12054
12076
  features: array(string()),
12055
12077
  producesTrackedEvents: boolean().optional()
12056
12078
  });
12079
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12080
+ * The batch answer needs the tag; the single-device answer already has it
12081
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12082
+ var LinkedDevicesForDeviceSchema = object({
12083
+ deviceId: number(),
12084
+ mode: LinkedDevicesModeSchema,
12085
+ devices: array(LinkedDeviceSchema)
12086
+ });
12087
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12088
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12089
+ * object literal is exactly how the three drift apart. */
12090
+ var DeviceBindingsForDeviceSchema = object({
12091
+ deviceId: number(),
12092
+ entries: array(object({
12093
+ capName: string(),
12094
+ kind: _enum(["native", "wrapped"]),
12095
+ providerAddonId: string(),
12096
+ providerNodeId: string(),
12097
+ nativeAddonId: string()
12098
+ }))
12099
+ });
12057
12100
  var SavedDeviceRowSchema = object({
12058
12101
  /** Numeric id reserved at allocateDeviceId time. */
12059
12102
  id: number(),
@@ -12279,11 +12322,25 @@ method(object({
12279
12322
  projection: _enum(["full", "slim"]).optional(),
12280
12323
  /** Return only camera devices. Filtering server-side instead of
12281
12324
  * shipping 293 rows to find 12. */
12282
- isCamera: boolean().optional()
12325
+ isCamera: boolean().optional(),
12326
+ /**
12327
+ * Return only these device ids. For the caller that already KNOWS the
12328
+ * handful it wants and needs a field the id-bearing answer does not
12329
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12330
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12331
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12332
+ * refetches on the reconcile interval, on a phone.
12333
+ *
12334
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12335
+ * keys rather than rejecting them (verified against the live hub
12336
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12337
+ * it answers today and the caller filters as it already does.
12338
+ */
12339
+ deviceIds: array(number()).optional()
12283
12340
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12284
12341
  mode: LinkedDevicesModeSchema,
12285
12342
  devices: array(LinkedDeviceSchema)
12286
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12343
+ })), 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({
12287
12344
  deviceId: number(),
12288
12345
  values: record(string(), unknown())
12289
12346
  }), object({ success: literal(true) }), {
@@ -12310,25 +12367,7 @@ method(object({
12310
12367
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12311
12368
  kind: "mutation",
12312
12369
  auth: "admin"
12313
- }), method(object({ deviceId: number() }), object({
12314
- deviceId: number(),
12315
- entries: array(object({
12316
- capName: string(),
12317
- kind: _enum(["native", "wrapped"]),
12318
- providerAddonId: string(),
12319
- providerNodeId: string(),
12320
- nativeAddonId: string()
12321
- }))
12322
- })), method(object({}), array(object({
12323
- deviceId: number(),
12324
- entries: array(object({
12325
- capName: string(),
12326
- kind: _enum(["native", "wrapped"]),
12327
- providerAddonId: string(),
12328
- providerNodeId: string(),
12329
- nativeAddonId: string()
12330
- }))
12331
- }))), method(object({
12370
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12332
12371
  deviceId: number(),
12333
12372
  capName: string(),
12334
12373
  wrapperAddonId: string(),
@@ -14738,12 +14777,15 @@ var NcOccupancyConditionSchema = object({
14738
14777
  * there is no second switch that can disagree with the first and every rule
14739
14778
  * authored before the decision migrates for free (`audioModeOf`):
14740
14779
  *
14741
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14742
- * classifier labels with one of them. No window, no percentage:
14743
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14744
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14745
- * the analyzer's (`classificationMinScore`, per device) a label only
14746
- * reaches this condition if the classifier was already confident enough.
14780
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14781
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14782
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14783
+ * frames is the wrong question for a classifier that labels 1–3 frames
14784
+ * per episode. The count window is the brake that drops a single-frame
14785
+ * false positive; the rule's own `throttle` cooldown is the other. The
14786
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14787
+ * per device) — a label only reaches this condition if the classifier was
14788
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14747
14789
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14748
14790
  * the condition: at least `hitPercent`% of the samples over
14749
14791
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14770,14 +14812,22 @@ var NcOccupancyConditionSchema = object({
14770
14812
  * an operator who typed `dog` mean the same thing.
14771
14813
  */
14772
14814
  var NcAudioConditionSchema = object({
14773
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14815
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14774
14816
  labels: array(string().min(1)).min(1).optional(),
14775
14817
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14776
14818
  dbThreshold: number().min(-96).max(0).optional(),
14777
14819
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14778
14820
  hitPercent: number().int().min(1).max(100).default(60),
14779
14821
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14780
- samplingSeconds: number().int().min(1).max(300).default(10)
14822
+ samplingSeconds: number().int().min(1).max(300).default(10),
14823
+ /**
14824
+ * LABEL MODE: how many labelled frames must land inside
14825
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14826
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14827
+ */
14828
+ confirmHits: number().int().min(1).max(20).optional(),
14829
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14830
+ confirmWindowSec: number().int().min(1).max(60).optional()
14781
14831
  });
14782
14832
  /**
14783
14833
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17151,6 +17201,46 @@ var RecentTracksPageSchema = object({
17151
17201
  /** Cursor for the next page, or null when this page is the last. */
17152
17202
  nextCursor: string().nullable()
17153
17203
  });
17204
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17205
+ var LIST_GROUPS_MAX_LIMIT = 100;
17206
+ var AnalyticsGroupRecordSchema = object({
17207
+ id: string(),
17208
+ deviceId: number().int(),
17209
+ openedAt: number().int(),
17210
+ closedAt: number().int(),
17211
+ timestamp: number().int(),
17212
+ memberCount: number().int(),
17213
+ memberTrackIds: array(string()).readonly(),
17214
+ className: string(),
17215
+ classes: array(string()).readonly(),
17216
+ /** Relative event-media path, or null when the group has no picture yet. */
17217
+ mediaUrl: string().nullable(),
17218
+ singleton: boolean()
17219
+ });
17220
+ var AnalyticsGroupMemberSchema = object({
17221
+ trackId: string(),
17222
+ deviceId: number().int(),
17223
+ className: string(),
17224
+ firstSeen: number().int(),
17225
+ lastSeen: number().int(),
17226
+ mediaUrl: string().nullable()
17227
+ });
17228
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17229
+ var ListGroupsQueryInput = object({
17230
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17231
+ deviceIds: array(number()),
17232
+ /** Window lower bound on `closedAt` (inclusive). */
17233
+ since: number().optional(),
17234
+ /** Window upper bound on `openedAt` (inclusive). */
17235
+ until: number().optional(),
17236
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17237
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17238
+ cursor: string().optional()
17239
+ });
17240
+ var ListGroupsPageSchema = object({
17241
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17242
+ nextCursor: string().nullable()
17243
+ });
17154
17244
  var KeyEventQueryInput = object({
17155
17245
  deviceId: number(),
17156
17246
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17226,7 +17316,9 @@ var TrackCascadeCountsSchema = object({
17226
17316
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17227
17317
  plates: number().int(),
17228
17318
  /** Per-track CLIP search vectors removed (best-effort). */
17229
- embeddings: number().int()
17319
+ embeddings: number().int(),
17320
+ /** Group membership + group rows removed with their last member (best-effort). */
17321
+ groups: number().int()
17230
17322
  });
17231
17323
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17232
17324
  var DiskReconcileCountsSchema = object({
@@ -17372,7 +17464,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17372
17464
  * stationary registry). Default false: the timeline lists passages,
17373
17465
  * not parking records (operator decision, 2026-08-15). */
17374
17466
  includeStationary: boolean().optional()
17375
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17467
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17468
+ deviceId: number(),
17469
+ groupId: string().min(1)
17470
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17376
17471
  kind: "mutation",
17377
17472
  auth: "admin"
17378
17473
  }), 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({
@@ -17682,7 +17777,8 @@ var PipelineModelOptionSchema = object({
17682
17777
  sizeMB: number()
17683
17778
  })),
17684
17779
  group: ModelVariantGroupSchema.optional(),
17685
- legacy: boolean().optional()
17780
+ legacy: boolean().optional(),
17781
+ provider: ModelProviderIdSchema.optional()
17686
17782
  });
17687
17783
  var ConfigFieldBridge = custom();
17688
17784
  var PipelineAddonSchemaSchema = object({
@@ -25787,7 +25883,12 @@ var PlateInfoSchema = object({
25787
25883
  plateBbox: BoundingBoxSchema.optional(),
25788
25884
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25789
25885
  keyFrameMediaKey: string().optional(),
25790
- base64: string().optional()
25886
+ base64: string().optional(),
25887
+ /**
25888
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25889
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25890
+ */
25891
+ cropUrl: string().optional()
25791
25892
  });
25792
25893
  var MediaFileLiteSchema = object({
25793
25894
  key: string(),
@@ -31311,6 +31412,12 @@ Object.freeze({
31311
31412
  addonId: null,
31312
31413
  access: "view"
31313
31414
  },
31415
+ "deviceManager.getBindingsBatch": {
31416
+ capName: "device-manager",
31417
+ capScope: "system",
31418
+ addonId: null,
31419
+ access: "view"
31420
+ },
31314
31421
  "deviceManager.getChildren": {
31315
31422
  capName: "device-manager",
31316
31423
  capScope: "system",
@@ -31371,6 +31478,12 @@ Object.freeze({
31371
31478
  addonId: null,
31372
31479
  access: "view"
31373
31480
  },
31481
+ "deviceManager.getLinkedDevicesBatch": {
31482
+ capName: "device-manager",
31483
+ capScope: "system",
31484
+ addonId: null,
31485
+ access: "view"
31486
+ },
31374
31487
  "deviceManager.getRoleDisplayDefaults": {
31375
31488
  capName: "device-manager",
31376
31489
  capScope: "system",
@@ -33141,6 +33254,12 @@ Object.freeze({
33141
33254
  addonId: null,
33142
33255
  access: "view"
33143
33256
  },
33257
+ "pipelineAnalytics.getGroup": {
33258
+ capName: "pipeline-analytics",
33259
+ capScope: "device",
33260
+ addonId: null,
33261
+ access: "view"
33262
+ },
33144
33263
  "pipelineAnalytics.getKeyEvents": {
33145
33264
  capName: "pipeline-analytics",
33146
33265
  capScope: "device",
@@ -33225,6 +33344,12 @@ Object.freeze({
33225
33344
  addonId: null,
33226
33345
  access: "view"
33227
33346
  },
33347
+ "pipelineAnalytics.listGroups": {
33348
+ capName: "pipeline-analytics",
33349
+ capScope: "device",
33350
+ addonId: null,
33351
+ access: "view"
33352
+ },
33228
33353
  "pipelineAnalytics.listOpsLog": {
33229
33354
  capName: "pipeline-analytics",
33230
33355
  capScope: "device",
@@ -36006,6 +36131,11 @@ Object.freeze({
36006
36131
  form: "single",
36007
36132
  optional: false
36008
36133
  }],
36134
+ "deviceManager.getBindingsBatch": [{
36135
+ name: "deviceIds",
36136
+ form: "array",
36137
+ optional: false
36138
+ }],
36009
36139
  "deviceManager.getChildren": [{
36010
36140
  name: "parentDeviceId",
36011
36141
  form: "single",
@@ -36051,6 +36181,11 @@ Object.freeze({
36051
36181
  form: "single",
36052
36182
  optional: false
36053
36183
  }],
36184
+ "deviceManager.getLinkedDevicesBatch": [{
36185
+ name: "deviceIds",
36186
+ form: "array",
36187
+ optional: false
36188
+ }],
36054
36189
  "deviceManager.getSettingsSchema": [{
36055
36190
  name: "deviceId",
36056
36191
  form: "single",
@@ -36071,6 +36206,11 @@ Object.freeze({
36071
36206
  form: "single",
36072
36207
  optional: false
36073
36208
  }],
36209
+ "deviceManager.listAll": [{
36210
+ name: "deviceIds",
36211
+ form: "array",
36212
+ optional: true
36213
+ }],
36074
36214
  "deviceManager.loadConfig": [{
36075
36215
  name: "deviceId",
36076
36216
  form: "single",
@@ -36644,6 +36784,11 @@ Object.freeze({
36644
36784
  form: "single",
36645
36785
  optional: false
36646
36786
  }],
36787
+ "pipelineAnalytics.getGroup": [{
36788
+ name: "deviceId",
36789
+ form: "single",
36790
+ optional: false
36791
+ }],
36647
36792
  "pipelineAnalytics.getKeyEvents": [{
36648
36793
  name: "deviceId",
36649
36794
  form: "single",
@@ -36699,6 +36844,11 @@ Object.freeze({
36699
36844
  form: "array",
36700
36845
  optional: false
36701
36846
  }],
36847
+ "pipelineAnalytics.listGroups": [{
36848
+ name: "deviceIds",
36849
+ form: "array",
36850
+ optional: false
36851
+ }],
36702
36852
  "pipelineAnalytics.listOpsLog": [{
36703
36853
  name: "deviceId",
36704
36854
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-dreo",
3
- "version": "0.2.27",
3
+ "version": "0.2.28",
4
4
  "description": "Dreo smart-device (fan / air-circulator / purifier / heater / humidifier) device-provider addon for CamStack — wraps the @apocaliss92/nodedreo Dreo cloud client (REST + WebSocket)",
5
5
  "keywords": [
6
6
  "camstack",