@camstack/addon-provider-velux 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
@@ -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
  }
@@ -8112,6 +8122,12 @@ var ModelVariantGroupSchema = object({
8112
8122
  */
8113
8123
  resolution: number().int().positive().optional()
8114
8124
  });
8125
+ var ModelProviderIdSchema = _enum([
8126
+ "camstack",
8127
+ "frigate",
8128
+ "scrypted",
8129
+ "custom"
8130
+ ]);
8115
8131
  var ModelCatalogEntrySchema = object({
8116
8132
  id: string(),
8117
8133
  name: string(),
@@ -8209,6 +8225,12 @@ var ModelCatalogEntrySchema = object({
8209
8225
  */
8210
8226
  group: ModelVariantGroupSchema.optional(),
8211
8227
  /**
8228
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8229
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8230
+ * persisted before this field existed (`inferModelProvider` fills those).
8231
+ */
8232
+ provider: ModelProviderIdSchema.optional(),
8233
+ /**
8212
8234
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8213
8235
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8214
8236
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12000,6 +12022,27 @@ var LinkedDeviceSchema = object({
12000
12022
  features: array(string()),
12001
12023
  producesTrackedEvents: boolean().optional()
12002
12024
  });
12025
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12026
+ * The batch answer needs the tag; the single-device answer already has it
12027
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12028
+ var LinkedDevicesForDeviceSchema = object({
12029
+ deviceId: number(),
12030
+ mode: LinkedDevicesModeSchema,
12031
+ devices: array(LinkedDeviceSchema)
12032
+ });
12033
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12034
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12035
+ * object literal is exactly how the three drift apart. */
12036
+ var DeviceBindingsForDeviceSchema = object({
12037
+ deviceId: number(),
12038
+ entries: array(object({
12039
+ capName: string(),
12040
+ kind: _enum(["native", "wrapped"]),
12041
+ providerAddonId: string(),
12042
+ providerNodeId: string(),
12043
+ nativeAddonId: string()
12044
+ }))
12045
+ });
12003
12046
  var SavedDeviceRowSchema = object({
12004
12047
  /** Numeric id reserved at allocateDeviceId time. */
12005
12048
  id: number(),
@@ -12225,11 +12268,25 @@ method(object({
12225
12268
  projection: _enum(["full", "slim"]).optional(),
12226
12269
  /** Return only camera devices. Filtering server-side instead of
12227
12270
  * shipping 293 rows to find 12. */
12228
- isCamera: boolean().optional()
12271
+ isCamera: boolean().optional(),
12272
+ /**
12273
+ * Return only these device ids. For the caller that already KNOWS the
12274
+ * handful it wants and needs a field the id-bearing answer does not
12275
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12276
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12277
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12278
+ * refetches on the reconcile interval, on a phone.
12279
+ *
12280
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12281
+ * keys rather than rejecting them (verified against the live hub
12282
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12283
+ * it answers today and the caller filters as it already does.
12284
+ */
12285
+ deviceIds: array(number()).optional()
12229
12286
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12230
12287
  mode: LinkedDevicesModeSchema,
12231
12288
  devices: array(LinkedDeviceSchema)
12232
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12289
+ })), 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({
12233
12290
  deviceId: number(),
12234
12291
  values: record(string(), unknown())
12235
12292
  }), object({ success: literal(true) }), {
@@ -12256,25 +12313,7 @@ method(object({
12256
12313
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12257
12314
  kind: "mutation",
12258
12315
  auth: "admin"
12259
- }), method(object({ deviceId: number() }), object({
12260
- deviceId: number(),
12261
- entries: array(object({
12262
- capName: string(),
12263
- kind: _enum(["native", "wrapped"]),
12264
- providerAddonId: string(),
12265
- providerNodeId: string(),
12266
- nativeAddonId: string()
12267
- }))
12268
- })), method(object({}), array(object({
12269
- deviceId: number(),
12270
- entries: array(object({
12271
- capName: string(),
12272
- kind: _enum(["native", "wrapped"]),
12273
- providerAddonId: string(),
12274
- providerNodeId: string(),
12275
- nativeAddonId: string()
12276
- }))
12277
- }))), method(object({
12316
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12278
12317
  deviceId: number(),
12279
12318
  capName: string(),
12280
12319
  wrapperAddonId: string(),
@@ -14684,12 +14723,15 @@ var NcOccupancyConditionSchema = object({
14684
14723
  * there is no second switch that can disagree with the first and every rule
14685
14724
  * authored before the decision migrates for free (`audioModeOf`):
14686
14725
  *
14687
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14688
- * classifier labels with one of them. No window, no percentage:
14689
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14690
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14691
- * the analyzer's (`classificationMinScore`, per device) a label only
14692
- * reaches this condition if the classifier was already confident enough.
14726
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14727
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14728
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14729
+ * frames is the wrong question for a classifier that labels 1–3 frames
14730
+ * per episode. The count window is the brake that drops a single-frame
14731
+ * false positive; the rule's own `throttle` cooldown is the other. The
14732
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14733
+ * per device) — a label only reaches this condition if the classifier was
14734
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14693
14735
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14694
14736
  * the condition: at least `hitPercent`% of the samples over
14695
14737
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14716,14 +14758,22 @@ var NcOccupancyConditionSchema = object({
14716
14758
  * an operator who typed `dog` mean the same thing.
14717
14759
  */
14718
14760
  var NcAudioConditionSchema = object({
14719
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14761
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14720
14762
  labels: array(string().min(1)).min(1).optional(),
14721
14763
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14722
14764
  dbThreshold: number().min(-96).max(0).optional(),
14723
14765
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14724
14766
  hitPercent: number().int().min(1).max(100).default(60),
14725
14767
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14726
- samplingSeconds: number().int().min(1).max(300).default(10)
14768
+ samplingSeconds: number().int().min(1).max(300).default(10),
14769
+ /**
14770
+ * LABEL MODE: how many labelled frames must land inside
14771
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14772
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14773
+ */
14774
+ confirmHits: number().int().min(1).max(20).optional(),
14775
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14776
+ confirmWindowSec: number().int().min(1).max(60).optional()
14727
14777
  });
14728
14778
  /**
14729
14779
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17097,6 +17147,46 @@ var RecentTracksPageSchema = object({
17097
17147
  /** Cursor for the next page, or null when this page is the last. */
17098
17148
  nextCursor: string().nullable()
17099
17149
  });
17150
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17151
+ var LIST_GROUPS_MAX_LIMIT = 100;
17152
+ var AnalyticsGroupRecordSchema = object({
17153
+ id: string(),
17154
+ deviceId: number().int(),
17155
+ openedAt: number().int(),
17156
+ closedAt: number().int(),
17157
+ timestamp: number().int(),
17158
+ memberCount: number().int(),
17159
+ memberTrackIds: array(string()).readonly(),
17160
+ className: string(),
17161
+ classes: array(string()).readonly(),
17162
+ /** Relative event-media path, or null when the group has no picture yet. */
17163
+ mediaUrl: string().nullable(),
17164
+ singleton: boolean()
17165
+ });
17166
+ var AnalyticsGroupMemberSchema = object({
17167
+ trackId: string(),
17168
+ deviceId: number().int(),
17169
+ className: string(),
17170
+ firstSeen: number().int(),
17171
+ lastSeen: number().int(),
17172
+ mediaUrl: string().nullable()
17173
+ });
17174
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17175
+ var ListGroupsQueryInput = object({
17176
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17177
+ deviceIds: array(number()),
17178
+ /** Window lower bound on `closedAt` (inclusive). */
17179
+ since: number().optional(),
17180
+ /** Window upper bound on `openedAt` (inclusive). */
17181
+ until: number().optional(),
17182
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17183
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17184
+ cursor: string().optional()
17185
+ });
17186
+ var ListGroupsPageSchema = object({
17187
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17188
+ nextCursor: string().nullable()
17189
+ });
17100
17190
  var KeyEventQueryInput = object({
17101
17191
  deviceId: number(),
17102
17192
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17172,7 +17262,9 @@ var TrackCascadeCountsSchema = object({
17172
17262
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17173
17263
  plates: number().int(),
17174
17264
  /** Per-track CLIP search vectors removed (best-effort). */
17175
- embeddings: number().int()
17265
+ embeddings: number().int(),
17266
+ /** Group membership + group rows removed with their last member (best-effort). */
17267
+ groups: number().int()
17176
17268
  });
17177
17269
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17178
17270
  var DiskReconcileCountsSchema = object({
@@ -17318,7 +17410,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17318
17410
  * stationary registry). Default false: the timeline lists passages,
17319
17411
  * not parking records (operator decision, 2026-08-15). */
17320
17412
  includeStationary: boolean().optional()
17321
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17413
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17414
+ deviceId: number(),
17415
+ groupId: string().min(1)
17416
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17322
17417
  kind: "mutation",
17323
17418
  auth: "admin"
17324
17419
  }), 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({
@@ -17628,7 +17723,8 @@ var PipelineModelOptionSchema = object({
17628
17723
  sizeMB: number()
17629
17724
  })),
17630
17725
  group: ModelVariantGroupSchema.optional(),
17631
- legacy: boolean().optional()
17726
+ legacy: boolean().optional(),
17727
+ provider: ModelProviderIdSchema.optional()
17632
17728
  });
17633
17729
  var ConfigFieldBridge = custom();
17634
17730
  var PipelineAddonSchemaSchema = object({
@@ -25725,7 +25821,12 @@ var PlateInfoSchema = object({
25725
25821
  plateBbox: BoundingBoxSchema.optional(),
25726
25822
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25727
25823
  keyFrameMediaKey: string().optional(),
25728
- base64: string().optional()
25824
+ base64: string().optional(),
25825
+ /**
25826
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25827
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25828
+ */
25829
+ cropUrl: string().optional()
25729
25830
  });
25730
25831
  var MediaFileLiteSchema = object({
25731
25832
  key: string(),
@@ -31249,6 +31350,12 @@ Object.freeze({
31249
31350
  addonId: null,
31250
31351
  access: "view"
31251
31352
  },
31353
+ "deviceManager.getBindingsBatch": {
31354
+ capName: "device-manager",
31355
+ capScope: "system",
31356
+ addonId: null,
31357
+ access: "view"
31358
+ },
31252
31359
  "deviceManager.getChildren": {
31253
31360
  capName: "device-manager",
31254
31361
  capScope: "system",
@@ -31309,6 +31416,12 @@ Object.freeze({
31309
31416
  addonId: null,
31310
31417
  access: "view"
31311
31418
  },
31419
+ "deviceManager.getLinkedDevicesBatch": {
31420
+ capName: "device-manager",
31421
+ capScope: "system",
31422
+ addonId: null,
31423
+ access: "view"
31424
+ },
31312
31425
  "deviceManager.getRoleDisplayDefaults": {
31313
31426
  capName: "device-manager",
31314
31427
  capScope: "system",
@@ -33079,6 +33192,12 @@ Object.freeze({
33079
33192
  addonId: null,
33080
33193
  access: "view"
33081
33194
  },
33195
+ "pipelineAnalytics.getGroup": {
33196
+ capName: "pipeline-analytics",
33197
+ capScope: "device",
33198
+ addonId: null,
33199
+ access: "view"
33200
+ },
33082
33201
  "pipelineAnalytics.getKeyEvents": {
33083
33202
  capName: "pipeline-analytics",
33084
33203
  capScope: "device",
@@ -33163,6 +33282,12 @@ Object.freeze({
33163
33282
  addonId: null,
33164
33283
  access: "view"
33165
33284
  },
33285
+ "pipelineAnalytics.listGroups": {
33286
+ capName: "pipeline-analytics",
33287
+ capScope: "device",
33288
+ addonId: null,
33289
+ access: "view"
33290
+ },
33166
33291
  "pipelineAnalytics.listOpsLog": {
33167
33292
  capName: "pipeline-analytics",
33168
33293
  capScope: "device",
@@ -35944,6 +36069,11 @@ Object.freeze({
35944
36069
  form: "single",
35945
36070
  optional: false
35946
36071
  }],
36072
+ "deviceManager.getBindingsBatch": [{
36073
+ name: "deviceIds",
36074
+ form: "array",
36075
+ optional: false
36076
+ }],
35947
36077
  "deviceManager.getChildren": [{
35948
36078
  name: "parentDeviceId",
35949
36079
  form: "single",
@@ -35989,6 +36119,11 @@ Object.freeze({
35989
36119
  form: "single",
35990
36120
  optional: false
35991
36121
  }],
36122
+ "deviceManager.getLinkedDevicesBatch": [{
36123
+ name: "deviceIds",
36124
+ form: "array",
36125
+ optional: false
36126
+ }],
35992
36127
  "deviceManager.getSettingsSchema": [{
35993
36128
  name: "deviceId",
35994
36129
  form: "single",
@@ -36009,6 +36144,11 @@ Object.freeze({
36009
36144
  form: "single",
36010
36145
  optional: false
36011
36146
  }],
36147
+ "deviceManager.listAll": [{
36148
+ name: "deviceIds",
36149
+ form: "array",
36150
+ optional: true
36151
+ }],
36012
36152
  "deviceManager.loadConfig": [{
36013
36153
  name: "deviceId",
36014
36154
  form: "single",
@@ -36582,6 +36722,11 @@ Object.freeze({
36582
36722
  form: "single",
36583
36723
  optional: false
36584
36724
  }],
36725
+ "pipelineAnalytics.getGroup": [{
36726
+ name: "deviceId",
36727
+ form: "single",
36728
+ optional: false
36729
+ }],
36585
36730
  "pipelineAnalytics.getKeyEvents": [{
36586
36731
  name: "deviceId",
36587
36732
  form: "single",
@@ -36637,6 +36782,11 @@ Object.freeze({
36637
36782
  form: "array",
36638
36783
  optional: false
36639
36784
  }],
36785
+ "pipelineAnalytics.listGroups": [{
36786
+ name: "deviceIds",
36787
+ form: "array",
36788
+ optional: false
36789
+ }],
36640
36790
  "pipelineAnalytics.listOpsLog": [{
36641
36791
  name: "deviceId",
36642
36792
  form: "single",
package/dist/addon.mjs CHANGED
@@ -5800,6 +5800,13 @@ var BaseAddon = class {
5800
5800
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5801
5801
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5802
5802
  _registeredCapNames = [];
5803
+ /**
5804
+ * True only after `readAddonStore` actually answered. Constructor
5805
+ * defaults look like stored config when the store is down — a forked
5806
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5807
+ * mode, 2026-08-25) is not "the operator chose this".
5808
+ */
5809
+ settingsStoreReady = false;
5803
5810
  /** Default config values. Provided via constructor. */
5804
5811
  defaults;
5805
5812
  constructor(defaults) {
@@ -6200,7 +6207,9 @@ var BaseAddon = class {
6200
6207
  ];
6201
6208
  let lastErr;
6202
6209
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6203
- return await settings.readAddonStore() ?? {};
6210
+ const stored = await settings.readAddonStore() ?? {};
6211
+ this.settingsStoreReady = true;
6212
+ return stored;
6204
6213
  } catch (err) {
6205
6214
  lastErr = err;
6206
6215
  const msg = err instanceof Error ? err.message : String(err);
@@ -6208,6 +6217,7 @@ var BaseAddon = class {
6208
6217
  if (attempt === delaysMs.length) break;
6209
6218
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6210
6219
  }
6220
+ this.settingsStoreReady = false;
6211
6221
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6212
6222
  return {};
6213
6223
  }
@@ -8111,6 +8121,12 @@ var ModelVariantGroupSchema = object({
8111
8121
  */
8112
8122
  resolution: number().int().positive().optional()
8113
8123
  });
8124
+ var ModelProviderIdSchema = _enum([
8125
+ "camstack",
8126
+ "frigate",
8127
+ "scrypted",
8128
+ "custom"
8129
+ ]);
8114
8130
  var ModelCatalogEntrySchema = object({
8115
8131
  id: string(),
8116
8132
  name: string(),
@@ -8208,6 +8224,12 @@ var ModelCatalogEntrySchema = object({
8208
8224
  */
8209
8225
  group: ModelVariantGroupSchema.optional(),
8210
8226
  /**
8227
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8228
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8229
+ * persisted before this field existed (`inferModelProvider` fills those).
8230
+ */
8231
+ provider: ModelProviderIdSchema.optional(),
8232
+ /**
8211
8233
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8212
8234
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8213
8235
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -11999,6 +12021,27 @@ var LinkedDeviceSchema = object({
11999
12021
  features: array(string()),
12000
12022
  producesTrackedEvents: boolean().optional()
12001
12023
  });
12024
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12025
+ * The batch answer needs the tag; the single-device answer already has it
12026
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12027
+ var LinkedDevicesForDeviceSchema = object({
12028
+ deviceId: number(),
12029
+ mode: LinkedDevicesModeSchema,
12030
+ devices: array(LinkedDeviceSchema)
12031
+ });
12032
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12033
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12034
+ * object literal is exactly how the three drift apart. */
12035
+ var DeviceBindingsForDeviceSchema = object({
12036
+ deviceId: number(),
12037
+ entries: array(object({
12038
+ capName: string(),
12039
+ kind: _enum(["native", "wrapped"]),
12040
+ providerAddonId: string(),
12041
+ providerNodeId: string(),
12042
+ nativeAddonId: string()
12043
+ }))
12044
+ });
12002
12045
  var SavedDeviceRowSchema = object({
12003
12046
  /** Numeric id reserved at allocateDeviceId time. */
12004
12047
  id: number(),
@@ -12224,11 +12267,25 @@ method(object({
12224
12267
  projection: _enum(["full", "slim"]).optional(),
12225
12268
  /** Return only camera devices. Filtering server-side instead of
12226
12269
  * shipping 293 rows to find 12. */
12227
- isCamera: boolean().optional()
12270
+ isCamera: boolean().optional(),
12271
+ /**
12272
+ * Return only these device ids. For the caller that already KNOWS the
12273
+ * handful it wants and needs a field the id-bearing answer does not
12274
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12275
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12276
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12277
+ * refetches on the reconcile interval, on a phone.
12278
+ *
12279
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12280
+ * keys rather than rejecting them (verified against the live hub
12281
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12282
+ * it answers today and the caller filters as it already does.
12283
+ */
12284
+ deviceIds: array(number()).optional()
12228
12285
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12229
12286
  mode: LinkedDevicesModeSchema,
12230
12287
  devices: array(LinkedDeviceSchema)
12231
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12288
+ })), 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({
12232
12289
  deviceId: number(),
12233
12290
  values: record(string(), unknown())
12234
12291
  }), object({ success: literal(true) }), {
@@ -12255,25 +12312,7 @@ method(object({
12255
12312
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12256
12313
  kind: "mutation",
12257
12314
  auth: "admin"
12258
- }), method(object({ deviceId: number() }), object({
12259
- deviceId: number(),
12260
- entries: array(object({
12261
- capName: string(),
12262
- kind: _enum(["native", "wrapped"]),
12263
- providerAddonId: string(),
12264
- providerNodeId: string(),
12265
- nativeAddonId: string()
12266
- }))
12267
- })), method(object({}), array(object({
12268
- deviceId: number(),
12269
- entries: array(object({
12270
- capName: string(),
12271
- kind: _enum(["native", "wrapped"]),
12272
- providerAddonId: string(),
12273
- providerNodeId: string(),
12274
- nativeAddonId: string()
12275
- }))
12276
- }))), method(object({
12315
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12277
12316
  deviceId: number(),
12278
12317
  capName: string(),
12279
12318
  wrapperAddonId: string(),
@@ -14683,12 +14722,15 @@ var NcOccupancyConditionSchema = object({
14683
14722
  * there is no second switch that can disagree with the first and every rule
14684
14723
  * authored before the decision migrates for free (`audioModeOf`):
14685
14724
  *
14686
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14687
- * classifier labels with one of them. No window, no percentage:
14688
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14689
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14690
- * the analyzer's (`classificationMinScore`, per device) a label only
14691
- * reaches this condition if the classifier was already confident enough.
14725
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14726
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14727
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14728
+ * frames is the wrong question for a classifier that labels 1–3 frames
14729
+ * per episode. The count window is the brake that drops a single-frame
14730
+ * false positive; the rule's own `throttle` cooldown is the other. The
14731
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14732
+ * per device) — a label only reaches this condition if the classifier was
14733
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14692
14734
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14693
14735
  * the condition: at least `hitPercent`% of the samples over
14694
14736
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14715,14 +14757,22 @@ var NcOccupancyConditionSchema = object({
14715
14757
  * an operator who typed `dog` mean the same thing.
14716
14758
  */
14717
14759
  var NcAudioConditionSchema = object({
14718
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14760
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14719
14761
  labels: array(string().min(1)).min(1).optional(),
14720
14762
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14721
14763
  dbThreshold: number().min(-96).max(0).optional(),
14722
14764
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14723
14765
  hitPercent: number().int().min(1).max(100).default(60),
14724
14766
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14725
- samplingSeconds: number().int().min(1).max(300).default(10)
14767
+ samplingSeconds: number().int().min(1).max(300).default(10),
14768
+ /**
14769
+ * LABEL MODE: how many labelled frames must land inside
14770
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14771
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14772
+ */
14773
+ confirmHits: number().int().min(1).max(20).optional(),
14774
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14775
+ confirmWindowSec: number().int().min(1).max(60).optional()
14726
14776
  });
14727
14777
  /**
14728
14778
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17096,6 +17146,46 @@ var RecentTracksPageSchema = object({
17096
17146
  /** Cursor for the next page, or null when this page is the last. */
17097
17147
  nextCursor: string().nullable()
17098
17148
  });
17149
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17150
+ var LIST_GROUPS_MAX_LIMIT = 100;
17151
+ var AnalyticsGroupRecordSchema = object({
17152
+ id: string(),
17153
+ deviceId: number().int(),
17154
+ openedAt: number().int(),
17155
+ closedAt: number().int(),
17156
+ timestamp: number().int(),
17157
+ memberCount: number().int(),
17158
+ memberTrackIds: array(string()).readonly(),
17159
+ className: string(),
17160
+ classes: array(string()).readonly(),
17161
+ /** Relative event-media path, or null when the group has no picture yet. */
17162
+ mediaUrl: string().nullable(),
17163
+ singleton: boolean()
17164
+ });
17165
+ var AnalyticsGroupMemberSchema = object({
17166
+ trackId: string(),
17167
+ deviceId: number().int(),
17168
+ className: string(),
17169
+ firstSeen: number().int(),
17170
+ lastSeen: number().int(),
17171
+ mediaUrl: string().nullable()
17172
+ });
17173
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17174
+ var ListGroupsQueryInput = object({
17175
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17176
+ deviceIds: array(number()),
17177
+ /** Window lower bound on `closedAt` (inclusive). */
17178
+ since: number().optional(),
17179
+ /** Window upper bound on `openedAt` (inclusive). */
17180
+ until: number().optional(),
17181
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17182
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17183
+ cursor: string().optional()
17184
+ });
17185
+ var ListGroupsPageSchema = object({
17186
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17187
+ nextCursor: string().nullable()
17188
+ });
17099
17189
  var KeyEventQueryInput = object({
17100
17190
  deviceId: number(),
17101
17191
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17171,7 +17261,9 @@ var TrackCascadeCountsSchema = object({
17171
17261
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17172
17262
  plates: number().int(),
17173
17263
  /** Per-track CLIP search vectors removed (best-effort). */
17174
- embeddings: number().int()
17264
+ embeddings: number().int(),
17265
+ /** Group membership + group rows removed with their last member (best-effort). */
17266
+ groups: number().int()
17175
17267
  });
17176
17268
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17177
17269
  var DiskReconcileCountsSchema = object({
@@ -17317,7 +17409,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17317
17409
  * stationary registry). Default false: the timeline lists passages,
17318
17410
  * not parking records (operator decision, 2026-08-15). */
17319
17411
  includeStationary: boolean().optional()
17320
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17412
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17413
+ deviceId: number(),
17414
+ groupId: string().min(1)
17415
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17321
17416
  kind: "mutation",
17322
17417
  auth: "admin"
17323
17418
  }), 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({
@@ -17627,7 +17722,8 @@ var PipelineModelOptionSchema = object({
17627
17722
  sizeMB: number()
17628
17723
  })),
17629
17724
  group: ModelVariantGroupSchema.optional(),
17630
- legacy: boolean().optional()
17725
+ legacy: boolean().optional(),
17726
+ provider: ModelProviderIdSchema.optional()
17631
17727
  });
17632
17728
  var ConfigFieldBridge = custom();
17633
17729
  var PipelineAddonSchemaSchema = object({
@@ -25724,7 +25820,12 @@ var PlateInfoSchema = object({
25724
25820
  plateBbox: BoundingBoxSchema.optional(),
25725
25821
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25726
25822
  keyFrameMediaKey: string().optional(),
25727
- base64: string().optional()
25823
+ base64: string().optional(),
25824
+ /**
25825
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25826
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25827
+ */
25828
+ cropUrl: string().optional()
25728
25829
  });
25729
25830
  var MediaFileLiteSchema = object({
25730
25831
  key: string(),
@@ -31248,6 +31349,12 @@ Object.freeze({
31248
31349
  addonId: null,
31249
31350
  access: "view"
31250
31351
  },
31352
+ "deviceManager.getBindingsBatch": {
31353
+ capName: "device-manager",
31354
+ capScope: "system",
31355
+ addonId: null,
31356
+ access: "view"
31357
+ },
31251
31358
  "deviceManager.getChildren": {
31252
31359
  capName: "device-manager",
31253
31360
  capScope: "system",
@@ -31308,6 +31415,12 @@ Object.freeze({
31308
31415
  addonId: null,
31309
31416
  access: "view"
31310
31417
  },
31418
+ "deviceManager.getLinkedDevicesBatch": {
31419
+ capName: "device-manager",
31420
+ capScope: "system",
31421
+ addonId: null,
31422
+ access: "view"
31423
+ },
31311
31424
  "deviceManager.getRoleDisplayDefaults": {
31312
31425
  capName: "device-manager",
31313
31426
  capScope: "system",
@@ -33078,6 +33191,12 @@ Object.freeze({
33078
33191
  addonId: null,
33079
33192
  access: "view"
33080
33193
  },
33194
+ "pipelineAnalytics.getGroup": {
33195
+ capName: "pipeline-analytics",
33196
+ capScope: "device",
33197
+ addonId: null,
33198
+ access: "view"
33199
+ },
33081
33200
  "pipelineAnalytics.getKeyEvents": {
33082
33201
  capName: "pipeline-analytics",
33083
33202
  capScope: "device",
@@ -33162,6 +33281,12 @@ Object.freeze({
33162
33281
  addonId: null,
33163
33282
  access: "view"
33164
33283
  },
33284
+ "pipelineAnalytics.listGroups": {
33285
+ capName: "pipeline-analytics",
33286
+ capScope: "device",
33287
+ addonId: null,
33288
+ access: "view"
33289
+ },
33165
33290
  "pipelineAnalytics.listOpsLog": {
33166
33291
  capName: "pipeline-analytics",
33167
33292
  capScope: "device",
@@ -35943,6 +36068,11 @@ Object.freeze({
35943
36068
  form: "single",
35944
36069
  optional: false
35945
36070
  }],
36071
+ "deviceManager.getBindingsBatch": [{
36072
+ name: "deviceIds",
36073
+ form: "array",
36074
+ optional: false
36075
+ }],
35946
36076
  "deviceManager.getChildren": [{
35947
36077
  name: "parentDeviceId",
35948
36078
  form: "single",
@@ -35988,6 +36118,11 @@ Object.freeze({
35988
36118
  form: "single",
35989
36119
  optional: false
35990
36120
  }],
36121
+ "deviceManager.getLinkedDevicesBatch": [{
36122
+ name: "deviceIds",
36123
+ form: "array",
36124
+ optional: false
36125
+ }],
35991
36126
  "deviceManager.getSettingsSchema": [{
35992
36127
  name: "deviceId",
35993
36128
  form: "single",
@@ -36008,6 +36143,11 @@ Object.freeze({
36008
36143
  form: "single",
36009
36144
  optional: false
36010
36145
  }],
36146
+ "deviceManager.listAll": [{
36147
+ name: "deviceIds",
36148
+ form: "array",
36149
+ optional: true
36150
+ }],
36011
36151
  "deviceManager.loadConfig": [{
36012
36152
  name: "deviceId",
36013
36153
  form: "single",
@@ -36581,6 +36721,11 @@ Object.freeze({
36581
36721
  form: "single",
36582
36722
  optional: false
36583
36723
  }],
36724
+ "pipelineAnalytics.getGroup": [{
36725
+ name: "deviceId",
36726
+ form: "single",
36727
+ optional: false
36728
+ }],
36584
36729
  "pipelineAnalytics.getKeyEvents": [{
36585
36730
  name: "deviceId",
36586
36731
  form: "single",
@@ -36636,6 +36781,11 @@ Object.freeze({
36636
36781
  form: "array",
36637
36782
  optional: false
36638
36783
  }],
36784
+ "pipelineAnalytics.listGroups": [{
36785
+ name: "deviceIds",
36786
+ form: "array",
36787
+ optional: false
36788
+ }],
36639
36789
  "pipelineAnalytics.listOpsLog": [{
36640
36790
  name: "deviceId",
36641
36791
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-velux",
3
- "version": "0.2.27",
3
+ "version": "0.2.28",
4
4
  "description": "Velux KLF-200 device-provider addon for CamStack — local-gateway window + shutter covers (positional, no tilt)",
5
5
  "keywords": [
6
6
  "camstack",