@camstack/addon-provider-rtsp 1.2.27 → 1.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
@@ -5825,6 +5825,13 @@ var BaseAddon = class {
5825
5825
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5826
5826
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5827
5827
  _registeredCapNames = [];
5828
+ /**
5829
+ * True only after `readAddonStore` actually answered. Constructor
5830
+ * defaults look like stored config when the store is down — a forked
5831
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5832
+ * mode, 2026-08-25) is not "the operator chose this".
5833
+ */
5834
+ settingsStoreReady = false;
5828
5835
  /** Default config values. Provided via constructor. */
5829
5836
  defaults;
5830
5837
  constructor(defaults) {
@@ -6225,7 +6232,9 @@ var BaseAddon = class {
6225
6232
  ];
6226
6233
  let lastErr;
6227
6234
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6228
- return await settings.readAddonStore() ?? {};
6235
+ const stored = await settings.readAddonStore() ?? {};
6236
+ this.settingsStoreReady = true;
6237
+ return stored;
6229
6238
  } catch (err) {
6230
6239
  lastErr = err;
6231
6240
  const msg = err instanceof Error ? err.message : String(err);
@@ -6233,6 +6242,7 @@ var BaseAddon = class {
6233
6242
  if (attempt === delaysMs.length) break;
6234
6243
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6235
6244
  }
6245
+ this.settingsStoreReady = false;
6236
6246
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6237
6247
  return {};
6238
6248
  }
@@ -8168,6 +8178,12 @@ var ModelVariantGroupSchema = object({
8168
8178
  */
8169
8179
  resolution: number().int().positive().optional()
8170
8180
  });
8181
+ var ModelProviderIdSchema = _enum([
8182
+ "camstack",
8183
+ "frigate",
8184
+ "scrypted",
8185
+ "custom"
8186
+ ]);
8171
8187
  var ModelCatalogEntrySchema = object({
8172
8188
  id: string(),
8173
8189
  name: string(),
@@ -8265,6 +8281,12 @@ var ModelCatalogEntrySchema = object({
8265
8281
  */
8266
8282
  group: ModelVariantGroupSchema.optional(),
8267
8283
  /**
8284
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8285
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8286
+ * persisted before this field existed (`inferModelProvider` fills those).
8287
+ */
8288
+ provider: ModelProviderIdSchema.optional(),
8289
+ /**
8268
8290
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8269
8291
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8270
8292
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12056,6 +12078,27 @@ var LinkedDeviceSchema = object({
12056
12078
  features: array(string()),
12057
12079
  producesTrackedEvents: boolean().optional()
12058
12080
  });
12081
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12082
+ * The batch answer needs the tag; the single-device answer already has it
12083
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12084
+ var LinkedDevicesForDeviceSchema = object({
12085
+ deviceId: number(),
12086
+ mode: LinkedDevicesModeSchema,
12087
+ devices: array(LinkedDeviceSchema)
12088
+ });
12089
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12090
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12091
+ * object literal is exactly how the three drift apart. */
12092
+ var DeviceBindingsForDeviceSchema = object({
12093
+ deviceId: number(),
12094
+ entries: array(object({
12095
+ capName: string(),
12096
+ kind: _enum(["native", "wrapped"]),
12097
+ providerAddonId: string(),
12098
+ providerNodeId: string(),
12099
+ nativeAddonId: string()
12100
+ }))
12101
+ });
12059
12102
  var SavedDeviceRowSchema = object({
12060
12103
  /** Numeric id reserved at allocateDeviceId time. */
12061
12104
  id: number(),
@@ -12281,11 +12324,25 @@ method(object({
12281
12324
  projection: _enum(["full", "slim"]).optional(),
12282
12325
  /** Return only camera devices. Filtering server-side instead of
12283
12326
  * shipping 293 rows to find 12. */
12284
- isCamera: boolean().optional()
12327
+ isCamera: boolean().optional(),
12328
+ /**
12329
+ * Return only these device ids. For the caller that already KNOWS the
12330
+ * handful it wants and needs a field the id-bearing answer does not
12331
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12332
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12333
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12334
+ * refetches on the reconcile interval, on a phone.
12335
+ *
12336
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12337
+ * keys rather than rejecting them (verified against the live hub
12338
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12339
+ * it answers today and the caller filters as it already does.
12340
+ */
12341
+ deviceIds: array(number()).optional()
12285
12342
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12286
12343
  mode: LinkedDevicesModeSchema,
12287
12344
  devices: array(LinkedDeviceSchema)
12288
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12345
+ })), 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({
12289
12346
  deviceId: number(),
12290
12347
  values: record(string(), unknown())
12291
12348
  }), object({ success: literal(true) }), {
@@ -12312,25 +12369,7 @@ method(object({
12312
12369
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12313
12370
  kind: "mutation",
12314
12371
  auth: "admin"
12315
- }), method(object({ deviceId: number() }), object({
12316
- deviceId: number(),
12317
- entries: array(object({
12318
- capName: string(),
12319
- kind: _enum(["native", "wrapped"]),
12320
- providerAddonId: string(),
12321
- providerNodeId: string(),
12322
- nativeAddonId: string()
12323
- }))
12324
- })), method(object({}), array(object({
12325
- deviceId: number(),
12326
- entries: array(object({
12327
- capName: string(),
12328
- kind: _enum(["native", "wrapped"]),
12329
- providerAddonId: string(),
12330
- providerNodeId: string(),
12331
- nativeAddonId: string()
12332
- }))
12333
- }))), method(object({
12372
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12334
12373
  deviceId: number(),
12335
12374
  capName: string(),
12336
12375
  wrapperAddonId: string(),
@@ -14740,12 +14779,15 @@ var NcOccupancyConditionSchema = object({
14740
14779
  * there is no second switch that can disagree with the first and every rule
14741
14780
  * authored before the decision migrates for free (`audioModeOf`):
14742
14781
  *
14743
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14744
- * classifier labels with one of them. No window, no percentage:
14745
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14746
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14747
- * the analyzer's (`classificationMinScore`, per device) a label only
14748
- * reaches this condition if the classifier was already confident enough.
14782
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14783
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14784
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14785
+ * frames is the wrong question for a classifier that labels 1–3 frames
14786
+ * per episode. The count window is the brake that drops a single-frame
14787
+ * false positive; the rule's own `throttle` cooldown is the other. The
14788
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14789
+ * per device) — a label only reaches this condition if the classifier was
14790
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14749
14791
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14750
14792
  * the condition: at least `hitPercent`% of the samples over
14751
14793
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14772,14 +14814,22 @@ var NcOccupancyConditionSchema = object({
14772
14814
  * an operator who typed `dog` mean the same thing.
14773
14815
  */
14774
14816
  var NcAudioConditionSchema = object({
14775
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14817
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14776
14818
  labels: array(string().min(1)).min(1).optional(),
14777
14819
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14778
14820
  dbThreshold: number().min(-96).max(0).optional(),
14779
14821
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14780
14822
  hitPercent: number().int().min(1).max(100).default(60),
14781
14823
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14782
- samplingSeconds: number().int().min(1).max(300).default(10)
14824
+ samplingSeconds: number().int().min(1).max(300).default(10),
14825
+ /**
14826
+ * LABEL MODE: how many labelled frames must land inside
14827
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14828
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14829
+ */
14830
+ confirmHits: number().int().min(1).max(20).optional(),
14831
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14832
+ confirmWindowSec: number().int().min(1).max(60).optional()
14783
14833
  });
14784
14834
  /**
14785
14835
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17153,6 +17203,46 @@ var RecentTracksPageSchema = object({
17153
17203
  /** Cursor for the next page, or null when this page is the last. */
17154
17204
  nextCursor: string().nullable()
17155
17205
  });
17206
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17207
+ var LIST_GROUPS_MAX_LIMIT = 100;
17208
+ var AnalyticsGroupRecordSchema = object({
17209
+ id: string(),
17210
+ deviceId: number().int(),
17211
+ openedAt: number().int(),
17212
+ closedAt: number().int(),
17213
+ timestamp: number().int(),
17214
+ memberCount: number().int(),
17215
+ memberTrackIds: array(string()).readonly(),
17216
+ className: string(),
17217
+ classes: array(string()).readonly(),
17218
+ /** Relative event-media path, or null when the group has no picture yet. */
17219
+ mediaUrl: string().nullable(),
17220
+ singleton: boolean()
17221
+ });
17222
+ var AnalyticsGroupMemberSchema = object({
17223
+ trackId: string(),
17224
+ deviceId: number().int(),
17225
+ className: string(),
17226
+ firstSeen: number().int(),
17227
+ lastSeen: number().int(),
17228
+ mediaUrl: string().nullable()
17229
+ });
17230
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17231
+ var ListGroupsQueryInput = object({
17232
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17233
+ deviceIds: array(number()),
17234
+ /** Window lower bound on `closedAt` (inclusive). */
17235
+ since: number().optional(),
17236
+ /** Window upper bound on `openedAt` (inclusive). */
17237
+ until: number().optional(),
17238
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17239
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17240
+ cursor: string().optional()
17241
+ });
17242
+ var ListGroupsPageSchema = object({
17243
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17244
+ nextCursor: string().nullable()
17245
+ });
17156
17246
  var KeyEventQueryInput = object({
17157
17247
  deviceId: number(),
17158
17248
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17228,7 +17318,9 @@ var TrackCascadeCountsSchema = object({
17228
17318
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17229
17319
  plates: number().int(),
17230
17320
  /** Per-track CLIP search vectors removed (best-effort). */
17231
- embeddings: number().int()
17321
+ embeddings: number().int(),
17322
+ /** Group membership + group rows removed with their last member (best-effort). */
17323
+ groups: number().int()
17232
17324
  });
17233
17325
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17234
17326
  var DiskReconcileCountsSchema = object({
@@ -17374,7 +17466,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17374
17466
  * stationary registry). Default false: the timeline lists passages,
17375
17467
  * not parking records (operator decision, 2026-08-15). */
17376
17468
  includeStationary: boolean().optional()
17377
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17469
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17470
+ deviceId: number(),
17471
+ groupId: string().min(1)
17472
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17378
17473
  kind: "mutation",
17379
17474
  auth: "admin"
17380
17475
  }), 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({
@@ -17684,7 +17779,8 @@ var PipelineModelOptionSchema = object({
17684
17779
  sizeMB: number()
17685
17780
  })),
17686
17781
  group: ModelVariantGroupSchema.optional(),
17687
- legacy: boolean().optional()
17782
+ legacy: boolean().optional(),
17783
+ provider: ModelProviderIdSchema.optional()
17688
17784
  });
17689
17785
  var ConfigFieldBridge = custom();
17690
17786
  var PipelineAddonSchemaSchema = object({
@@ -25893,7 +25989,12 @@ var PlateInfoSchema = object({
25893
25989
  plateBbox: BoundingBoxSchema.optional(),
25894
25990
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25895
25991
  keyFrameMediaKey: string().optional(),
25896
- base64: string().optional()
25992
+ base64: string().optional(),
25993
+ /**
25994
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25995
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25996
+ */
25997
+ cropUrl: string().optional()
25897
25998
  });
25898
25999
  var MediaFileLiteSchema = object({
25899
26000
  key: string(),
@@ -31483,6 +31584,12 @@ Object.freeze({
31483
31584
  addonId: null,
31484
31585
  access: "view"
31485
31586
  },
31587
+ "deviceManager.getBindingsBatch": {
31588
+ capName: "device-manager",
31589
+ capScope: "system",
31590
+ addonId: null,
31591
+ access: "view"
31592
+ },
31486
31593
  "deviceManager.getChildren": {
31487
31594
  capName: "device-manager",
31488
31595
  capScope: "system",
@@ -31543,6 +31650,12 @@ Object.freeze({
31543
31650
  addonId: null,
31544
31651
  access: "view"
31545
31652
  },
31653
+ "deviceManager.getLinkedDevicesBatch": {
31654
+ capName: "device-manager",
31655
+ capScope: "system",
31656
+ addonId: null,
31657
+ access: "view"
31658
+ },
31546
31659
  "deviceManager.getRoleDisplayDefaults": {
31547
31660
  capName: "device-manager",
31548
31661
  capScope: "system",
@@ -33313,6 +33426,12 @@ Object.freeze({
33313
33426
  addonId: null,
33314
33427
  access: "view"
33315
33428
  },
33429
+ "pipelineAnalytics.getGroup": {
33430
+ capName: "pipeline-analytics",
33431
+ capScope: "device",
33432
+ addonId: null,
33433
+ access: "view"
33434
+ },
33316
33435
  "pipelineAnalytics.getKeyEvents": {
33317
33436
  capName: "pipeline-analytics",
33318
33437
  capScope: "device",
@@ -33397,6 +33516,12 @@ Object.freeze({
33397
33516
  addonId: null,
33398
33517
  access: "view"
33399
33518
  },
33519
+ "pipelineAnalytics.listGroups": {
33520
+ capName: "pipeline-analytics",
33521
+ capScope: "device",
33522
+ addonId: null,
33523
+ access: "view"
33524
+ },
33400
33525
  "pipelineAnalytics.listOpsLog": {
33401
33526
  capName: "pipeline-analytics",
33402
33527
  capScope: "device",
@@ -36178,6 +36303,11 @@ Object.freeze({
36178
36303
  form: "single",
36179
36304
  optional: false
36180
36305
  }],
36306
+ "deviceManager.getBindingsBatch": [{
36307
+ name: "deviceIds",
36308
+ form: "array",
36309
+ optional: false
36310
+ }],
36181
36311
  "deviceManager.getChildren": [{
36182
36312
  name: "parentDeviceId",
36183
36313
  form: "single",
@@ -36223,6 +36353,11 @@ Object.freeze({
36223
36353
  form: "single",
36224
36354
  optional: false
36225
36355
  }],
36356
+ "deviceManager.getLinkedDevicesBatch": [{
36357
+ name: "deviceIds",
36358
+ form: "array",
36359
+ optional: false
36360
+ }],
36226
36361
  "deviceManager.getSettingsSchema": [{
36227
36362
  name: "deviceId",
36228
36363
  form: "single",
@@ -36243,6 +36378,11 @@ Object.freeze({
36243
36378
  form: "single",
36244
36379
  optional: false
36245
36380
  }],
36381
+ "deviceManager.listAll": [{
36382
+ name: "deviceIds",
36383
+ form: "array",
36384
+ optional: true
36385
+ }],
36246
36386
  "deviceManager.loadConfig": [{
36247
36387
  name: "deviceId",
36248
36388
  form: "single",
@@ -36816,6 +36956,11 @@ Object.freeze({
36816
36956
  form: "single",
36817
36957
  optional: false
36818
36958
  }],
36959
+ "pipelineAnalytics.getGroup": [{
36960
+ name: "deviceId",
36961
+ form: "single",
36962
+ optional: false
36963
+ }],
36819
36964
  "pipelineAnalytics.getKeyEvents": [{
36820
36965
  name: "deviceId",
36821
36966
  form: "single",
@@ -36871,6 +37016,11 @@ Object.freeze({
36871
37016
  form: "array",
36872
37017
  optional: false
36873
37018
  }],
37019
+ "pipelineAnalytics.listGroups": [{
37020
+ name: "deviceIds",
37021
+ form: "array",
37022
+ optional: false
37023
+ }],
36874
37024
  "pipelineAnalytics.listOpsLog": [{
36875
37025
  name: "deviceId",
36876
37026
  form: "single",
package/dist/addon.mjs CHANGED
@@ -5801,6 +5801,13 @@ var BaseAddon = class {
5801
5801
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5802
5802
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5803
5803
  _registeredCapNames = [];
5804
+ /**
5805
+ * True only after `readAddonStore` actually answered. Constructor
5806
+ * defaults look like stored config when the store is down — a forked
5807
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5808
+ * mode, 2026-08-25) is not "the operator chose this".
5809
+ */
5810
+ settingsStoreReady = false;
5804
5811
  /** Default config values. Provided via constructor. */
5805
5812
  defaults;
5806
5813
  constructor(defaults) {
@@ -6201,7 +6208,9 @@ var BaseAddon = class {
6201
6208
  ];
6202
6209
  let lastErr;
6203
6210
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6204
- return await settings.readAddonStore() ?? {};
6211
+ const stored = await settings.readAddonStore() ?? {};
6212
+ this.settingsStoreReady = true;
6213
+ return stored;
6205
6214
  } catch (err) {
6206
6215
  lastErr = err;
6207
6216
  const msg = err instanceof Error ? err.message : String(err);
@@ -6209,6 +6218,7 @@ var BaseAddon = class {
6209
6218
  if (attempt === delaysMs.length) break;
6210
6219
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6211
6220
  }
6221
+ this.settingsStoreReady = false;
6212
6222
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6213
6223
  return {};
6214
6224
  }
@@ -8144,6 +8154,12 @@ var ModelVariantGroupSchema = object({
8144
8154
  */
8145
8155
  resolution: number().int().positive().optional()
8146
8156
  });
8157
+ var ModelProviderIdSchema = _enum([
8158
+ "camstack",
8159
+ "frigate",
8160
+ "scrypted",
8161
+ "custom"
8162
+ ]);
8147
8163
  var ModelCatalogEntrySchema = object({
8148
8164
  id: string(),
8149
8165
  name: string(),
@@ -8241,6 +8257,12 @@ var ModelCatalogEntrySchema = object({
8241
8257
  */
8242
8258
  group: ModelVariantGroupSchema.optional(),
8243
8259
  /**
8260
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8261
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8262
+ * persisted before this field existed (`inferModelProvider` fills those).
8263
+ */
8264
+ provider: ModelProviderIdSchema.optional(),
8265
+ /**
8244
8266
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8245
8267
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8246
8268
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12032,6 +12054,27 @@ var LinkedDeviceSchema = object({
12032
12054
  features: array(string()),
12033
12055
  producesTrackedEvents: boolean().optional()
12034
12056
  });
12057
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12058
+ * The batch answer needs the tag; the single-device answer already has it
12059
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12060
+ var LinkedDevicesForDeviceSchema = object({
12061
+ deviceId: number(),
12062
+ mode: LinkedDevicesModeSchema,
12063
+ devices: array(LinkedDeviceSchema)
12064
+ });
12065
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12066
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12067
+ * object literal is exactly how the three drift apart. */
12068
+ var DeviceBindingsForDeviceSchema = object({
12069
+ deviceId: number(),
12070
+ entries: array(object({
12071
+ capName: string(),
12072
+ kind: _enum(["native", "wrapped"]),
12073
+ providerAddonId: string(),
12074
+ providerNodeId: string(),
12075
+ nativeAddonId: string()
12076
+ }))
12077
+ });
12035
12078
  var SavedDeviceRowSchema = object({
12036
12079
  /** Numeric id reserved at allocateDeviceId time. */
12037
12080
  id: number(),
@@ -12257,11 +12300,25 @@ method(object({
12257
12300
  projection: _enum(["full", "slim"]).optional(),
12258
12301
  /** Return only camera devices. Filtering server-side instead of
12259
12302
  * shipping 293 rows to find 12. */
12260
- isCamera: boolean().optional()
12303
+ isCamera: boolean().optional(),
12304
+ /**
12305
+ * Return only these device ids. For the caller that already KNOWS the
12306
+ * handful it wants and needs a field the id-bearing answer does not
12307
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12308
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12309
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12310
+ * refetches on the reconcile interval, on a phone.
12311
+ *
12312
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12313
+ * keys rather than rejecting them (verified against the live hub
12314
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12315
+ * it answers today and the caller filters as it already does.
12316
+ */
12317
+ deviceIds: array(number()).optional()
12261
12318
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12262
12319
  mode: LinkedDevicesModeSchema,
12263
12320
  devices: array(LinkedDeviceSchema)
12264
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12321
+ })), 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({
12265
12322
  deviceId: number(),
12266
12323
  values: record(string(), unknown())
12267
12324
  }), object({ success: literal(true) }), {
@@ -12288,25 +12345,7 @@ method(object({
12288
12345
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12289
12346
  kind: "mutation",
12290
12347
  auth: "admin"
12291
- }), method(object({ deviceId: number() }), object({
12292
- deviceId: number(),
12293
- entries: array(object({
12294
- capName: string(),
12295
- kind: _enum(["native", "wrapped"]),
12296
- providerAddonId: string(),
12297
- providerNodeId: string(),
12298
- nativeAddonId: string()
12299
- }))
12300
- })), method(object({}), array(object({
12301
- deviceId: number(),
12302
- entries: array(object({
12303
- capName: string(),
12304
- kind: _enum(["native", "wrapped"]),
12305
- providerAddonId: string(),
12306
- providerNodeId: string(),
12307
- nativeAddonId: string()
12308
- }))
12309
- }))), method(object({
12348
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12310
12349
  deviceId: number(),
12311
12350
  capName: string(),
12312
12351
  wrapperAddonId: string(),
@@ -14716,12 +14755,15 @@ var NcOccupancyConditionSchema = object({
14716
14755
  * there is no second switch that can disagree with the first and every rule
14717
14756
  * authored before the decision migrates for free (`audioModeOf`):
14718
14757
  *
14719
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14720
- * classifier labels with one of them. No window, no percentage:
14721
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14722
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14723
- * the analyzer's (`classificationMinScore`, per device) a label only
14724
- * reaches this condition if the classifier was already confident enough.
14758
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14759
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14760
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14761
+ * frames is the wrong question for a classifier that labels 1–3 frames
14762
+ * per episode. The count window is the brake that drops a single-frame
14763
+ * false positive; the rule's own `throttle` cooldown is the other. The
14764
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14765
+ * per device) — a label only reaches this condition if the classifier was
14766
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14725
14767
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14726
14768
  * the condition: at least `hitPercent`% of the samples over
14727
14769
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14748,14 +14790,22 @@ var NcOccupancyConditionSchema = object({
14748
14790
  * an operator who typed `dog` mean the same thing.
14749
14791
  */
14750
14792
  var NcAudioConditionSchema = object({
14751
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14793
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14752
14794
  labels: array(string().min(1)).min(1).optional(),
14753
14795
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14754
14796
  dbThreshold: number().min(-96).max(0).optional(),
14755
14797
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14756
14798
  hitPercent: number().int().min(1).max(100).default(60),
14757
14799
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14758
- samplingSeconds: number().int().min(1).max(300).default(10)
14800
+ samplingSeconds: number().int().min(1).max(300).default(10),
14801
+ /**
14802
+ * LABEL MODE: how many labelled frames must land inside
14803
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14804
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14805
+ */
14806
+ confirmHits: number().int().min(1).max(20).optional(),
14807
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14808
+ confirmWindowSec: number().int().min(1).max(60).optional()
14759
14809
  });
14760
14810
  /**
14761
14811
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17129,6 +17179,46 @@ var RecentTracksPageSchema = object({
17129
17179
  /** Cursor for the next page, or null when this page is the last. */
17130
17180
  nextCursor: string().nullable()
17131
17181
  });
17182
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17183
+ var LIST_GROUPS_MAX_LIMIT = 100;
17184
+ var AnalyticsGroupRecordSchema = object({
17185
+ id: string(),
17186
+ deviceId: number().int(),
17187
+ openedAt: number().int(),
17188
+ closedAt: number().int(),
17189
+ timestamp: number().int(),
17190
+ memberCount: number().int(),
17191
+ memberTrackIds: array(string()).readonly(),
17192
+ className: string(),
17193
+ classes: array(string()).readonly(),
17194
+ /** Relative event-media path, or null when the group has no picture yet. */
17195
+ mediaUrl: string().nullable(),
17196
+ singleton: boolean()
17197
+ });
17198
+ var AnalyticsGroupMemberSchema = object({
17199
+ trackId: string(),
17200
+ deviceId: number().int(),
17201
+ className: string(),
17202
+ firstSeen: number().int(),
17203
+ lastSeen: number().int(),
17204
+ mediaUrl: string().nullable()
17205
+ });
17206
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17207
+ var ListGroupsQueryInput = object({
17208
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17209
+ deviceIds: array(number()),
17210
+ /** Window lower bound on `closedAt` (inclusive). */
17211
+ since: number().optional(),
17212
+ /** Window upper bound on `openedAt` (inclusive). */
17213
+ until: number().optional(),
17214
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17215
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17216
+ cursor: string().optional()
17217
+ });
17218
+ var ListGroupsPageSchema = object({
17219
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17220
+ nextCursor: string().nullable()
17221
+ });
17132
17222
  var KeyEventQueryInput = object({
17133
17223
  deviceId: number(),
17134
17224
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17204,7 +17294,9 @@ var TrackCascadeCountsSchema = object({
17204
17294
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17205
17295
  plates: number().int(),
17206
17296
  /** Per-track CLIP search vectors removed (best-effort). */
17207
- embeddings: number().int()
17297
+ embeddings: number().int(),
17298
+ /** Group membership + group rows removed with their last member (best-effort). */
17299
+ groups: number().int()
17208
17300
  });
17209
17301
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17210
17302
  var DiskReconcileCountsSchema = object({
@@ -17350,7 +17442,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17350
17442
  * stationary registry). Default false: the timeline lists passages,
17351
17443
  * not parking records (operator decision, 2026-08-15). */
17352
17444
  includeStationary: boolean().optional()
17353
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17445
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17446
+ deviceId: number(),
17447
+ groupId: string().min(1)
17448
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17354
17449
  kind: "mutation",
17355
17450
  auth: "admin"
17356
17451
  }), 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({
@@ -17660,7 +17755,8 @@ var PipelineModelOptionSchema = object({
17660
17755
  sizeMB: number()
17661
17756
  })),
17662
17757
  group: ModelVariantGroupSchema.optional(),
17663
- legacy: boolean().optional()
17758
+ legacy: boolean().optional(),
17759
+ provider: ModelProviderIdSchema.optional()
17664
17760
  });
17665
17761
  var ConfigFieldBridge = custom();
17666
17762
  var PipelineAddonSchemaSchema = object({
@@ -25869,7 +25965,12 @@ var PlateInfoSchema = object({
25869
25965
  plateBbox: BoundingBoxSchema.optional(),
25870
25966
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25871
25967
  keyFrameMediaKey: string().optional(),
25872
- base64: string().optional()
25968
+ base64: string().optional(),
25969
+ /**
25970
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25971
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25972
+ */
25973
+ cropUrl: string().optional()
25873
25974
  });
25874
25975
  var MediaFileLiteSchema = object({
25875
25976
  key: string(),
@@ -31459,6 +31560,12 @@ Object.freeze({
31459
31560
  addonId: null,
31460
31561
  access: "view"
31461
31562
  },
31563
+ "deviceManager.getBindingsBatch": {
31564
+ capName: "device-manager",
31565
+ capScope: "system",
31566
+ addonId: null,
31567
+ access: "view"
31568
+ },
31462
31569
  "deviceManager.getChildren": {
31463
31570
  capName: "device-manager",
31464
31571
  capScope: "system",
@@ -31519,6 +31626,12 @@ Object.freeze({
31519
31626
  addonId: null,
31520
31627
  access: "view"
31521
31628
  },
31629
+ "deviceManager.getLinkedDevicesBatch": {
31630
+ capName: "device-manager",
31631
+ capScope: "system",
31632
+ addonId: null,
31633
+ access: "view"
31634
+ },
31522
31635
  "deviceManager.getRoleDisplayDefaults": {
31523
31636
  capName: "device-manager",
31524
31637
  capScope: "system",
@@ -33289,6 +33402,12 @@ Object.freeze({
33289
33402
  addonId: null,
33290
33403
  access: "view"
33291
33404
  },
33405
+ "pipelineAnalytics.getGroup": {
33406
+ capName: "pipeline-analytics",
33407
+ capScope: "device",
33408
+ addonId: null,
33409
+ access: "view"
33410
+ },
33292
33411
  "pipelineAnalytics.getKeyEvents": {
33293
33412
  capName: "pipeline-analytics",
33294
33413
  capScope: "device",
@@ -33373,6 +33492,12 @@ Object.freeze({
33373
33492
  addonId: null,
33374
33493
  access: "view"
33375
33494
  },
33495
+ "pipelineAnalytics.listGroups": {
33496
+ capName: "pipeline-analytics",
33497
+ capScope: "device",
33498
+ addonId: null,
33499
+ access: "view"
33500
+ },
33376
33501
  "pipelineAnalytics.listOpsLog": {
33377
33502
  capName: "pipeline-analytics",
33378
33503
  capScope: "device",
@@ -36154,6 +36279,11 @@ Object.freeze({
36154
36279
  form: "single",
36155
36280
  optional: false
36156
36281
  }],
36282
+ "deviceManager.getBindingsBatch": [{
36283
+ name: "deviceIds",
36284
+ form: "array",
36285
+ optional: false
36286
+ }],
36157
36287
  "deviceManager.getChildren": [{
36158
36288
  name: "parentDeviceId",
36159
36289
  form: "single",
@@ -36199,6 +36329,11 @@ Object.freeze({
36199
36329
  form: "single",
36200
36330
  optional: false
36201
36331
  }],
36332
+ "deviceManager.getLinkedDevicesBatch": [{
36333
+ name: "deviceIds",
36334
+ form: "array",
36335
+ optional: false
36336
+ }],
36202
36337
  "deviceManager.getSettingsSchema": [{
36203
36338
  name: "deviceId",
36204
36339
  form: "single",
@@ -36219,6 +36354,11 @@ Object.freeze({
36219
36354
  form: "single",
36220
36355
  optional: false
36221
36356
  }],
36357
+ "deviceManager.listAll": [{
36358
+ name: "deviceIds",
36359
+ form: "array",
36360
+ optional: true
36361
+ }],
36222
36362
  "deviceManager.loadConfig": [{
36223
36363
  name: "deviceId",
36224
36364
  form: "single",
@@ -36792,6 +36932,11 @@ Object.freeze({
36792
36932
  form: "single",
36793
36933
  optional: false
36794
36934
  }],
36935
+ "pipelineAnalytics.getGroup": [{
36936
+ name: "deviceId",
36937
+ form: "single",
36938
+ optional: false
36939
+ }],
36795
36940
  "pipelineAnalytics.getKeyEvents": [{
36796
36941
  name: "deviceId",
36797
36942
  form: "single",
@@ -36847,6 +36992,11 @@ Object.freeze({
36847
36992
  form: "array",
36848
36993
  optional: false
36849
36994
  }],
36995
+ "pipelineAnalytics.listGroups": [{
36996
+ name: "deviceIds",
36997
+ form: "array",
36998
+ optional: false
36999
+ }],
36850
37000
  "pipelineAnalytics.listOpsLog": [{
36851
37001
  name: "deviceId",
36852
37002
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-rtsp",
3
- "version": "1.2.27",
3
+ "version": "1.2.28",
4
4
  "description": "Generic RTSP camera device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",