@camstack/addon-matter-broker 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
@@ -5815,6 +5815,13 @@ var BaseAddon = class {
5815
5815
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5816
5816
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5817
5817
  _registeredCapNames = [];
5818
+ /**
5819
+ * True only after `readAddonStore` actually answered. Constructor
5820
+ * defaults look like stored config when the store is down — a forked
5821
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5822
+ * mode, 2026-08-25) is not "the operator chose this".
5823
+ */
5824
+ settingsStoreReady = false;
5818
5825
  /** Default config values. Provided via constructor. */
5819
5826
  defaults;
5820
5827
  constructor(defaults) {
@@ -6215,7 +6222,9 @@ var BaseAddon = class {
6215
6222
  ];
6216
6223
  let lastErr;
6217
6224
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6218
- return await settings.readAddonStore() ?? {};
6225
+ const stored = await settings.readAddonStore() ?? {};
6226
+ this.settingsStoreReady = true;
6227
+ return stored;
6219
6228
  } catch (err) {
6220
6229
  lastErr = err;
6221
6230
  const msg = err instanceof Error ? err.message : String(err);
@@ -6223,6 +6232,7 @@ var BaseAddon = class {
6223
6232
  if (attempt === delaysMs.length) break;
6224
6233
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6225
6234
  }
6235
+ this.settingsStoreReady = false;
6226
6236
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6227
6237
  return {};
6228
6238
  }
@@ -8126,6 +8136,12 @@ var ModelVariantGroupSchema = object({
8126
8136
  */
8127
8137
  resolution: number().int().positive().optional()
8128
8138
  });
8139
+ var ModelProviderIdSchema = _enum([
8140
+ "camstack",
8141
+ "frigate",
8142
+ "scrypted",
8143
+ "custom"
8144
+ ]);
8129
8145
  var ModelCatalogEntrySchema = object({
8130
8146
  id: string$2(),
8131
8147
  name: string$2(),
@@ -8223,6 +8239,12 @@ var ModelCatalogEntrySchema = object({
8223
8239
  */
8224
8240
  group: ModelVariantGroupSchema.optional(),
8225
8241
  /**
8242
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8243
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8244
+ * persisted before this field existed (`inferModelProvider` fills those).
8245
+ */
8246
+ provider: ModelProviderIdSchema.optional(),
8247
+ /**
8226
8248
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8227
8249
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8228
8250
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12080,6 +12102,27 @@ var LinkedDeviceSchema = object({
12080
12102
  features: array(string$2()),
12081
12103
  producesTrackedEvents: boolean().optional()
12082
12104
  });
12105
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12106
+ * The batch answer needs the tag; the single-device answer already has it
12107
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12108
+ var LinkedDevicesForDeviceSchema = object({
12109
+ deviceId: number(),
12110
+ mode: LinkedDevicesModeSchema,
12111
+ devices: array(LinkedDeviceSchema)
12112
+ });
12113
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12114
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12115
+ * object literal is exactly how the three drift apart. */
12116
+ var DeviceBindingsForDeviceSchema = object({
12117
+ deviceId: number(),
12118
+ entries: array(object({
12119
+ capName: string$2(),
12120
+ kind: _enum(["native", "wrapped"]),
12121
+ providerAddonId: string$2(),
12122
+ providerNodeId: string$2(),
12123
+ nativeAddonId: string$2()
12124
+ }))
12125
+ });
12083
12126
  var SavedDeviceRowSchema = object({
12084
12127
  /** Numeric id reserved at allocateDeviceId time. */
12085
12128
  id: number(),
@@ -12305,11 +12348,25 @@ method(object({
12305
12348
  projection: _enum(["full", "slim"]).optional(),
12306
12349
  /** Return only camera devices. Filtering server-side instead of
12307
12350
  * shipping 293 rows to find 12. */
12308
- isCamera: boolean().optional()
12351
+ isCamera: boolean().optional(),
12352
+ /**
12353
+ * Return only these device ids. For the caller that already KNOWS the
12354
+ * handful it wants and needs a field the id-bearing answer does not
12355
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12356
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12357
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12358
+ * refetches on the reconcile interval, on a phone.
12359
+ *
12360
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12361
+ * keys rather than rejecting them (verified against the live hub
12362
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12363
+ * it answers today and the caller filters as it already does.
12364
+ */
12365
+ deviceIds: array(number()).optional()
12309
12366
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12310
12367
  mode: LinkedDevicesModeSchema,
12311
12368
  devices: array(LinkedDeviceSchema)
12312
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12369
+ })), 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({
12313
12370
  deviceId: number(),
12314
12371
  values: record(string$2(), unknown())
12315
12372
  }), object({ success: literal(true) }), {
@@ -12336,25 +12393,7 @@ method(object({
12336
12393
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12337
12394
  kind: "mutation",
12338
12395
  auth: "admin"
12339
- }), method(object({ deviceId: number() }), object({
12340
- deviceId: number(),
12341
- entries: array(object({
12342
- capName: string$2(),
12343
- kind: _enum(["native", "wrapped"]),
12344
- providerAddonId: string$2(),
12345
- providerNodeId: string$2(),
12346
- nativeAddonId: string$2()
12347
- }))
12348
- })), method(object({}), array(object({
12349
- deviceId: number(),
12350
- entries: array(object({
12351
- capName: string$2(),
12352
- kind: _enum(["native", "wrapped"]),
12353
- providerAddonId: string$2(),
12354
- providerNodeId: string$2(),
12355
- nativeAddonId: string$2()
12356
- }))
12357
- }))), method(object({
12396
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12358
12397
  deviceId: number(),
12359
12398
  capName: string$2(),
12360
12399
  wrapperAddonId: string$2(),
@@ -14764,12 +14803,15 @@ var NcOccupancyConditionSchema = object({
14764
14803
  * there is no second switch that can disagree with the first and every rule
14765
14804
  * authored before the decision migrates for free (`audioModeOf`):
14766
14805
  *
14767
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14768
- * classifier labels with one of them. No window, no percentage:
14769
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14770
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14771
- * the analyzer's (`classificationMinScore`, per device) — a label only
14772
- * reaches this condition if the classifier was already confident enough.
14806
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14807
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14808
+ * `hitPercent` and `samplingSeconds` are still ignored — a percentage of
14809
+ * frames is the wrong question for a classifier that labels 1–3 frames
14810
+ * per episode. The count window is the brake that drops a single-frame
14811
+ * false positive; the rule's own `throttle` cooldown is the other. The
14812
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14813
+ * per device) — a label only reaches this condition if the classifier was
14814
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14773
14815
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14774
14816
  * the condition: at least `hitPercent`% of the samples over
14775
14817
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14796,14 +14838,22 @@ var NcOccupancyConditionSchema = object({
14796
14838
  * an operator who typed `dog` mean the same thing.
14797
14839
  */
14798
14840
  var NcAudioConditionSchema = object({
14799
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14841
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14800
14842
  labels: array(string$2().min(1)).min(1).optional(),
14801
14843
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14802
14844
  dbThreshold: number().min(-96).max(0).optional(),
14803
14845
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14804
14846
  hitPercent: number().int().min(1).max(100).default(60),
14805
14847
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14806
- samplingSeconds: number().int().min(1).max(300).default(10)
14848
+ samplingSeconds: number().int().min(1).max(300).default(10),
14849
+ /**
14850
+ * LABEL MODE: how many labelled frames must land inside
14851
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14852
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14853
+ */
14854
+ confirmHits: number().int().min(1).max(20).optional(),
14855
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14856
+ confirmWindowSec: number().int().min(1).max(60).optional()
14807
14857
  });
14808
14858
  /**
14809
14859
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17177,6 +17227,46 @@ var RecentTracksPageSchema = object({
17177
17227
  /** Cursor for the next page, or null when this page is the last. */
17178
17228
  nextCursor: string$2().nullable()
17179
17229
  });
17230
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17231
+ var LIST_GROUPS_MAX_LIMIT = 100;
17232
+ var AnalyticsGroupRecordSchema = object({
17233
+ id: string$2(),
17234
+ deviceId: number().int(),
17235
+ openedAt: number().int(),
17236
+ closedAt: number().int(),
17237
+ timestamp: number().int(),
17238
+ memberCount: number().int(),
17239
+ memberTrackIds: array(string$2()).readonly(),
17240
+ className: string$2(),
17241
+ classes: array(string$2()).readonly(),
17242
+ /** Relative event-media path, or null when the group has no picture yet. */
17243
+ mediaUrl: string$2().nullable(),
17244
+ singleton: boolean()
17245
+ });
17246
+ var AnalyticsGroupMemberSchema = object({
17247
+ trackId: string$2(),
17248
+ deviceId: number().int(),
17249
+ className: string$2(),
17250
+ firstSeen: number().int(),
17251
+ lastSeen: number().int(),
17252
+ mediaUrl: string$2().nullable()
17253
+ });
17254
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17255
+ var ListGroupsQueryInput = object({
17256
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17257
+ deviceIds: array(number()),
17258
+ /** Window lower bound on `closedAt` (inclusive). */
17259
+ since: number().optional(),
17260
+ /** Window upper bound on `openedAt` (inclusive). */
17261
+ until: number().optional(),
17262
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17263
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17264
+ cursor: string$2().optional()
17265
+ });
17266
+ var ListGroupsPageSchema = object({
17267
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17268
+ nextCursor: string$2().nullable()
17269
+ });
17180
17270
  var KeyEventQueryInput = object({
17181
17271
  deviceId: number(),
17182
17272
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17252,7 +17342,9 @@ var TrackCascadeCountsSchema = object({
17252
17342
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17253
17343
  plates: number().int(),
17254
17344
  /** Per-track CLIP search vectors removed (best-effort). */
17255
- embeddings: number().int()
17345
+ embeddings: number().int(),
17346
+ /** Group membership + group rows removed with their last member (best-effort). */
17347
+ groups: number().int()
17256
17348
  });
17257
17349
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17258
17350
  var DiskReconcileCountsSchema = object({
@@ -17398,7 +17490,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17398
17490
  * stationary registry). Default false: the timeline lists passages,
17399
17491
  * not parking records (operator decision, 2026-08-15). */
17400
17492
  includeStationary: boolean().optional()
17401
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17493
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17494
+ deviceId: number(),
17495
+ groupId: string$2().min(1)
17496
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17402
17497
  kind: "mutation",
17403
17498
  auth: "admin"
17404
17499
  }), 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({
@@ -17708,7 +17803,8 @@ var PipelineModelOptionSchema = object({
17708
17803
  sizeMB: number()
17709
17804
  })),
17710
17805
  group: ModelVariantGroupSchema.optional(),
17711
- legacy: boolean().optional()
17806
+ legacy: boolean().optional(),
17807
+ provider: ModelProviderIdSchema.optional()
17712
17808
  });
17713
17809
  var ConfigFieldBridge = custom();
17714
17810
  var PipelineAddonSchemaSchema = object({
@@ -25822,7 +25918,12 @@ var PlateInfoSchema = object({
25822
25918
  plateBbox: BoundingBoxSchema.optional(),
25823
25919
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25824
25920
  keyFrameMediaKey: string$2().optional(),
25825
- base64: string$2().optional()
25921
+ base64: string$2().optional(),
25922
+ /**
25923
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25924
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25925
+ */
25926
+ cropUrl: string$2().optional()
25826
25927
  });
25827
25928
  var MediaFileLiteSchema = object({
25828
25929
  key: string$2(),
@@ -31346,6 +31447,12 @@ Object.freeze({
31346
31447
  addonId: null,
31347
31448
  access: "view"
31348
31449
  },
31450
+ "deviceManager.getBindingsBatch": {
31451
+ capName: "device-manager",
31452
+ capScope: "system",
31453
+ addonId: null,
31454
+ access: "view"
31455
+ },
31349
31456
  "deviceManager.getChildren": {
31350
31457
  capName: "device-manager",
31351
31458
  capScope: "system",
@@ -31406,6 +31513,12 @@ Object.freeze({
31406
31513
  addonId: null,
31407
31514
  access: "view"
31408
31515
  },
31516
+ "deviceManager.getLinkedDevicesBatch": {
31517
+ capName: "device-manager",
31518
+ capScope: "system",
31519
+ addonId: null,
31520
+ access: "view"
31521
+ },
31409
31522
  "deviceManager.getRoleDisplayDefaults": {
31410
31523
  capName: "device-manager",
31411
31524
  capScope: "system",
@@ -33176,6 +33289,12 @@ Object.freeze({
33176
33289
  addonId: null,
33177
33290
  access: "view"
33178
33291
  },
33292
+ "pipelineAnalytics.getGroup": {
33293
+ capName: "pipeline-analytics",
33294
+ capScope: "device",
33295
+ addonId: null,
33296
+ access: "view"
33297
+ },
33179
33298
  "pipelineAnalytics.getKeyEvents": {
33180
33299
  capName: "pipeline-analytics",
33181
33300
  capScope: "device",
@@ -33260,6 +33379,12 @@ Object.freeze({
33260
33379
  addonId: null,
33261
33380
  access: "view"
33262
33381
  },
33382
+ "pipelineAnalytics.listGroups": {
33383
+ capName: "pipeline-analytics",
33384
+ capScope: "device",
33385
+ addonId: null,
33386
+ access: "view"
33387
+ },
33263
33388
  "pipelineAnalytics.listOpsLog": {
33264
33389
  capName: "pipeline-analytics",
33265
33390
  capScope: "device",
@@ -36041,6 +36166,11 @@ Object.freeze({
36041
36166
  form: "single",
36042
36167
  optional: false
36043
36168
  }],
36169
+ "deviceManager.getBindingsBatch": [{
36170
+ name: "deviceIds",
36171
+ form: "array",
36172
+ optional: false
36173
+ }],
36044
36174
  "deviceManager.getChildren": [{
36045
36175
  name: "parentDeviceId",
36046
36176
  form: "single",
@@ -36086,6 +36216,11 @@ Object.freeze({
36086
36216
  form: "single",
36087
36217
  optional: false
36088
36218
  }],
36219
+ "deviceManager.getLinkedDevicesBatch": [{
36220
+ name: "deviceIds",
36221
+ form: "array",
36222
+ optional: false
36223
+ }],
36089
36224
  "deviceManager.getSettingsSchema": [{
36090
36225
  name: "deviceId",
36091
36226
  form: "single",
@@ -36106,6 +36241,11 @@ Object.freeze({
36106
36241
  form: "single",
36107
36242
  optional: false
36108
36243
  }],
36244
+ "deviceManager.listAll": [{
36245
+ name: "deviceIds",
36246
+ form: "array",
36247
+ optional: true
36248
+ }],
36109
36249
  "deviceManager.loadConfig": [{
36110
36250
  name: "deviceId",
36111
36251
  form: "single",
@@ -36679,6 +36819,11 @@ Object.freeze({
36679
36819
  form: "single",
36680
36820
  optional: false
36681
36821
  }],
36822
+ "pipelineAnalytics.getGroup": [{
36823
+ name: "deviceId",
36824
+ form: "single",
36825
+ optional: false
36826
+ }],
36682
36827
  "pipelineAnalytics.getKeyEvents": [{
36683
36828
  name: "deviceId",
36684
36829
  form: "single",
@@ -36734,6 +36879,11 @@ Object.freeze({
36734
36879
  form: "array",
36735
36880
  optional: false
36736
36881
  }],
36882
+ "pipelineAnalytics.listGroups": [{
36883
+ name: "deviceIds",
36884
+ form: "array",
36885
+ optional: false
36886
+ }],
36737
36887
  "pipelineAnalytics.listOpsLog": [{
36738
36888
  name: "deviceId",
36739
36889
  form: "single",
package/dist/addon.mjs CHANGED
@@ -5813,6 +5813,13 @@ var BaseAddon = class {
5813
5813
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5814
5814
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5815
5815
  _registeredCapNames = [];
5816
+ /**
5817
+ * True only after `readAddonStore` actually answered. Constructor
5818
+ * defaults look like stored config when the store is down — a forked
5819
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5820
+ * mode, 2026-08-25) is not "the operator chose this".
5821
+ */
5822
+ settingsStoreReady = false;
5816
5823
  /** Default config values. Provided via constructor. */
5817
5824
  defaults;
5818
5825
  constructor(defaults) {
@@ -6213,7 +6220,9 @@ var BaseAddon = class {
6213
6220
  ];
6214
6221
  let lastErr;
6215
6222
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6216
- return await settings.readAddonStore() ?? {};
6223
+ const stored = await settings.readAddonStore() ?? {};
6224
+ this.settingsStoreReady = true;
6225
+ return stored;
6217
6226
  } catch (err) {
6218
6227
  lastErr = err;
6219
6228
  const msg = err instanceof Error ? err.message : String(err);
@@ -6221,6 +6230,7 @@ var BaseAddon = class {
6221
6230
  if (attempt === delaysMs.length) break;
6222
6231
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6223
6232
  }
6233
+ this.settingsStoreReady = false;
6224
6234
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6225
6235
  return {};
6226
6236
  }
@@ -8124,6 +8134,12 @@ var ModelVariantGroupSchema = object({
8124
8134
  */
8125
8135
  resolution: number().int().positive().optional()
8126
8136
  });
8137
+ var ModelProviderIdSchema = _enum([
8138
+ "camstack",
8139
+ "frigate",
8140
+ "scrypted",
8141
+ "custom"
8142
+ ]);
8127
8143
  var ModelCatalogEntrySchema = object({
8128
8144
  id: string$2(),
8129
8145
  name: string$2(),
@@ -8221,6 +8237,12 @@ var ModelCatalogEntrySchema = object({
8221
8237
  */
8222
8238
  group: ModelVariantGroupSchema.optional(),
8223
8239
  /**
8240
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8241
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8242
+ * persisted before this field existed (`inferModelProvider` fills those).
8243
+ */
8244
+ provider: ModelProviderIdSchema.optional(),
8245
+ /**
8224
8246
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8225
8247
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8226
8248
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12078,6 +12100,27 @@ var LinkedDeviceSchema = object({
12078
12100
  features: array(string$2()),
12079
12101
  producesTrackedEvents: boolean().optional()
12080
12102
  });
12103
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12104
+ * The batch answer needs the tag; the single-device answer already has it
12105
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12106
+ var LinkedDevicesForDeviceSchema = object({
12107
+ deviceId: number(),
12108
+ mode: LinkedDevicesModeSchema,
12109
+ devices: array(LinkedDeviceSchema)
12110
+ });
12111
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12112
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12113
+ * object literal is exactly how the three drift apart. */
12114
+ var DeviceBindingsForDeviceSchema = object({
12115
+ deviceId: number(),
12116
+ entries: array(object({
12117
+ capName: string$2(),
12118
+ kind: _enum(["native", "wrapped"]),
12119
+ providerAddonId: string$2(),
12120
+ providerNodeId: string$2(),
12121
+ nativeAddonId: string$2()
12122
+ }))
12123
+ });
12081
12124
  var SavedDeviceRowSchema = object({
12082
12125
  /** Numeric id reserved at allocateDeviceId time. */
12083
12126
  id: number(),
@@ -12303,11 +12346,25 @@ method(object({
12303
12346
  projection: _enum(["full", "slim"]).optional(),
12304
12347
  /** Return only camera devices. Filtering server-side instead of
12305
12348
  * shipping 293 rows to find 12. */
12306
- isCamera: boolean().optional()
12349
+ isCamera: boolean().optional(),
12350
+ /**
12351
+ * Return only these device ids. For the caller that already KNOWS the
12352
+ * handful it wants and needs a field the id-bearing answer does not
12353
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12354
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12355
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12356
+ * refetches on the reconcile interval, on a phone.
12357
+ *
12358
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12359
+ * keys rather than rejecting them (verified against the live hub
12360
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12361
+ * it answers today and the caller filters as it already does.
12362
+ */
12363
+ deviceIds: array(number()).optional()
12307
12364
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12308
12365
  mode: LinkedDevicesModeSchema,
12309
12366
  devices: array(LinkedDeviceSchema)
12310
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12367
+ })), 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({
12311
12368
  deviceId: number(),
12312
12369
  values: record(string$2(), unknown())
12313
12370
  }), object({ success: literal(true) }), {
@@ -12334,25 +12391,7 @@ method(object({
12334
12391
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12335
12392
  kind: "mutation",
12336
12393
  auth: "admin"
12337
- }), method(object({ deviceId: number() }), object({
12338
- deviceId: number(),
12339
- entries: array(object({
12340
- capName: string$2(),
12341
- kind: _enum(["native", "wrapped"]),
12342
- providerAddonId: string$2(),
12343
- providerNodeId: string$2(),
12344
- nativeAddonId: string$2()
12345
- }))
12346
- })), method(object({}), array(object({
12347
- deviceId: number(),
12348
- entries: array(object({
12349
- capName: string$2(),
12350
- kind: _enum(["native", "wrapped"]),
12351
- providerAddonId: string$2(),
12352
- providerNodeId: string$2(),
12353
- nativeAddonId: string$2()
12354
- }))
12355
- }))), method(object({
12394
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12356
12395
  deviceId: number(),
12357
12396
  capName: string$2(),
12358
12397
  wrapperAddonId: string$2(),
@@ -14762,12 +14801,15 @@ var NcOccupancyConditionSchema = object({
14762
14801
  * there is no second switch that can disagree with the first and every rule
14763
14802
  * authored before the decision migrates for free (`audioModeOf`):
14764
14803
  *
14765
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14766
- * classifier labels with one of them. No window, no percentage:
14767
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14768
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14769
- * the analyzer's (`classificationMinScore`, per device) — a label only
14770
- * reaches this condition if the classifier was already confident enough.
14804
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14805
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14806
+ * `hitPercent` and `samplingSeconds` are still ignored — a percentage of
14807
+ * frames is the wrong question for a classifier that labels 1–3 frames
14808
+ * per episode. The count window is the brake that drops a single-frame
14809
+ * false positive; the rule's own `throttle` cooldown is the other. The
14810
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14811
+ * per device) — a label only reaches this condition if the classifier was
14812
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14771
14813
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14772
14814
  * the condition: at least `hitPercent`% of the samples over
14773
14815
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14794,14 +14836,22 @@ var NcOccupancyConditionSchema = object({
14794
14836
  * an operator who typed `dog` mean the same thing.
14795
14837
  */
14796
14838
  var NcAudioConditionSchema = object({
14797
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14839
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14798
14840
  labels: array(string$2().min(1)).min(1).optional(),
14799
14841
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14800
14842
  dbThreshold: number().min(-96).max(0).optional(),
14801
14843
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14802
14844
  hitPercent: number().int().min(1).max(100).default(60),
14803
14845
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14804
- samplingSeconds: number().int().min(1).max(300).default(10)
14846
+ samplingSeconds: number().int().min(1).max(300).default(10),
14847
+ /**
14848
+ * LABEL MODE: how many labelled frames must land inside
14849
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14850
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14851
+ */
14852
+ confirmHits: number().int().min(1).max(20).optional(),
14853
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14854
+ confirmWindowSec: number().int().min(1).max(60).optional()
14805
14855
  });
14806
14856
  /**
14807
14857
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17175,6 +17225,46 @@ var RecentTracksPageSchema = object({
17175
17225
  /** Cursor for the next page, or null when this page is the last. */
17176
17226
  nextCursor: string$2().nullable()
17177
17227
  });
17228
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17229
+ var LIST_GROUPS_MAX_LIMIT = 100;
17230
+ var AnalyticsGroupRecordSchema = object({
17231
+ id: string$2(),
17232
+ deviceId: number().int(),
17233
+ openedAt: number().int(),
17234
+ closedAt: number().int(),
17235
+ timestamp: number().int(),
17236
+ memberCount: number().int(),
17237
+ memberTrackIds: array(string$2()).readonly(),
17238
+ className: string$2(),
17239
+ classes: array(string$2()).readonly(),
17240
+ /** Relative event-media path, or null when the group has no picture yet. */
17241
+ mediaUrl: string$2().nullable(),
17242
+ singleton: boolean()
17243
+ });
17244
+ var AnalyticsGroupMemberSchema = object({
17245
+ trackId: string$2(),
17246
+ deviceId: number().int(),
17247
+ className: string$2(),
17248
+ firstSeen: number().int(),
17249
+ lastSeen: number().int(),
17250
+ mediaUrl: string$2().nullable()
17251
+ });
17252
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17253
+ var ListGroupsQueryInput = object({
17254
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17255
+ deviceIds: array(number()),
17256
+ /** Window lower bound on `closedAt` (inclusive). */
17257
+ since: number().optional(),
17258
+ /** Window upper bound on `openedAt` (inclusive). */
17259
+ until: number().optional(),
17260
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17261
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17262
+ cursor: string$2().optional()
17263
+ });
17264
+ var ListGroupsPageSchema = object({
17265
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17266
+ nextCursor: string$2().nullable()
17267
+ });
17178
17268
  var KeyEventQueryInput = object({
17179
17269
  deviceId: number(),
17180
17270
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17250,7 +17340,9 @@ var TrackCascadeCountsSchema = object({
17250
17340
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17251
17341
  plates: number().int(),
17252
17342
  /** Per-track CLIP search vectors removed (best-effort). */
17253
- embeddings: number().int()
17343
+ embeddings: number().int(),
17344
+ /** Group membership + group rows removed with their last member (best-effort). */
17345
+ groups: number().int()
17254
17346
  });
17255
17347
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17256
17348
  var DiskReconcileCountsSchema = object({
@@ -17396,7 +17488,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17396
17488
  * stationary registry). Default false: the timeline lists passages,
17397
17489
  * not parking records (operator decision, 2026-08-15). */
17398
17490
  includeStationary: boolean().optional()
17399
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17491
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17492
+ deviceId: number(),
17493
+ groupId: string$2().min(1)
17494
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17400
17495
  kind: "mutation",
17401
17496
  auth: "admin"
17402
17497
  }), 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({
@@ -17706,7 +17801,8 @@ var PipelineModelOptionSchema = object({
17706
17801
  sizeMB: number()
17707
17802
  })),
17708
17803
  group: ModelVariantGroupSchema.optional(),
17709
- legacy: boolean().optional()
17804
+ legacy: boolean().optional(),
17805
+ provider: ModelProviderIdSchema.optional()
17710
17806
  });
17711
17807
  var ConfigFieldBridge = custom();
17712
17808
  var PipelineAddonSchemaSchema = object({
@@ -25820,7 +25916,12 @@ var PlateInfoSchema = object({
25820
25916
  plateBbox: BoundingBoxSchema.optional(),
25821
25917
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25822
25918
  keyFrameMediaKey: string$2().optional(),
25823
- base64: string$2().optional()
25919
+ base64: string$2().optional(),
25920
+ /**
25921
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25922
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25923
+ */
25924
+ cropUrl: string$2().optional()
25824
25925
  });
25825
25926
  var MediaFileLiteSchema = object({
25826
25927
  key: string$2(),
@@ -31344,6 +31445,12 @@ Object.freeze({
31344
31445
  addonId: null,
31345
31446
  access: "view"
31346
31447
  },
31448
+ "deviceManager.getBindingsBatch": {
31449
+ capName: "device-manager",
31450
+ capScope: "system",
31451
+ addonId: null,
31452
+ access: "view"
31453
+ },
31347
31454
  "deviceManager.getChildren": {
31348
31455
  capName: "device-manager",
31349
31456
  capScope: "system",
@@ -31404,6 +31511,12 @@ Object.freeze({
31404
31511
  addonId: null,
31405
31512
  access: "view"
31406
31513
  },
31514
+ "deviceManager.getLinkedDevicesBatch": {
31515
+ capName: "device-manager",
31516
+ capScope: "system",
31517
+ addonId: null,
31518
+ access: "view"
31519
+ },
31407
31520
  "deviceManager.getRoleDisplayDefaults": {
31408
31521
  capName: "device-manager",
31409
31522
  capScope: "system",
@@ -33174,6 +33287,12 @@ Object.freeze({
33174
33287
  addonId: null,
33175
33288
  access: "view"
33176
33289
  },
33290
+ "pipelineAnalytics.getGroup": {
33291
+ capName: "pipeline-analytics",
33292
+ capScope: "device",
33293
+ addonId: null,
33294
+ access: "view"
33295
+ },
33177
33296
  "pipelineAnalytics.getKeyEvents": {
33178
33297
  capName: "pipeline-analytics",
33179
33298
  capScope: "device",
@@ -33258,6 +33377,12 @@ Object.freeze({
33258
33377
  addonId: null,
33259
33378
  access: "view"
33260
33379
  },
33380
+ "pipelineAnalytics.listGroups": {
33381
+ capName: "pipeline-analytics",
33382
+ capScope: "device",
33383
+ addonId: null,
33384
+ access: "view"
33385
+ },
33261
33386
  "pipelineAnalytics.listOpsLog": {
33262
33387
  capName: "pipeline-analytics",
33263
33388
  capScope: "device",
@@ -36039,6 +36164,11 @@ Object.freeze({
36039
36164
  form: "single",
36040
36165
  optional: false
36041
36166
  }],
36167
+ "deviceManager.getBindingsBatch": [{
36168
+ name: "deviceIds",
36169
+ form: "array",
36170
+ optional: false
36171
+ }],
36042
36172
  "deviceManager.getChildren": [{
36043
36173
  name: "parentDeviceId",
36044
36174
  form: "single",
@@ -36084,6 +36214,11 @@ Object.freeze({
36084
36214
  form: "single",
36085
36215
  optional: false
36086
36216
  }],
36217
+ "deviceManager.getLinkedDevicesBatch": [{
36218
+ name: "deviceIds",
36219
+ form: "array",
36220
+ optional: false
36221
+ }],
36087
36222
  "deviceManager.getSettingsSchema": [{
36088
36223
  name: "deviceId",
36089
36224
  form: "single",
@@ -36104,6 +36239,11 @@ Object.freeze({
36104
36239
  form: "single",
36105
36240
  optional: false
36106
36241
  }],
36242
+ "deviceManager.listAll": [{
36243
+ name: "deviceIds",
36244
+ form: "array",
36245
+ optional: true
36246
+ }],
36107
36247
  "deviceManager.loadConfig": [{
36108
36248
  name: "deviceId",
36109
36249
  form: "single",
@@ -36677,6 +36817,11 @@ Object.freeze({
36677
36817
  form: "single",
36678
36818
  optional: false
36679
36819
  }],
36820
+ "pipelineAnalytics.getGroup": [{
36821
+ name: "deviceId",
36822
+ form: "single",
36823
+ optional: false
36824
+ }],
36680
36825
  "pipelineAnalytics.getKeyEvents": [{
36681
36826
  name: "deviceId",
36682
36827
  form: "single",
@@ -36732,6 +36877,11 @@ Object.freeze({
36732
36877
  form: "array",
36733
36878
  optional: false
36734
36879
  }],
36880
+ "pipelineAnalytics.listGroups": [{
36881
+ name: "deviceIds",
36882
+ form: "array",
36883
+ optional: false
36884
+ }],
36735
36885
  "pipelineAnalytics.listOpsLog": [{
36736
36886
  name: "deviceId",
36737
36887
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-matter-broker",
3
- "version": "0.2.26",
3
+ "version": "0.2.27",
4
4
  "description": "Matter broker addon for CamStack — owns a Matter fabric (commissioning + the long-lived controller) via the matter.js controller and brokers commissioned Matter nodes into CamStack",
5
5
  "keywords": [
6
6
  "camstack",