@camstack/addon-provider-wyze 0.2.31 → 0.2.32

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
@@ -5833,6 +5833,13 @@ var BaseAddon = class {
5833
5833
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5834
5834
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5835
5835
  _registeredCapNames = [];
5836
+ /**
5837
+ * True only after `readAddonStore` actually answered. Constructor
5838
+ * defaults look like stored config when the store is down — a forked
5839
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5840
+ * mode, 2026-08-25) is not "the operator chose this".
5841
+ */
5842
+ settingsStoreReady = false;
5836
5843
  /** Default config values. Provided via constructor. */
5837
5844
  defaults;
5838
5845
  constructor(defaults) {
@@ -6233,7 +6240,9 @@ var BaseAddon = class {
6233
6240
  ];
6234
6241
  let lastErr;
6235
6242
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6236
- return await settings.readAddonStore() ?? {};
6243
+ const stored = await settings.readAddonStore() ?? {};
6244
+ this.settingsStoreReady = true;
6245
+ return stored;
6237
6246
  } catch (err) {
6238
6247
  lastErr = err;
6239
6248
  const msg = err instanceof Error ? err.message : String(err);
@@ -6241,6 +6250,7 @@ var BaseAddon = class {
6241
6250
  if (attempt === delaysMs.length) break;
6242
6251
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6243
6252
  }
6253
+ this.settingsStoreReady = false;
6244
6254
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6245
6255
  return {};
6246
6256
  }
@@ -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).
@@ -12049,6 +12071,27 @@ var LinkedDeviceSchema = object({
12049
12071
  features: array(string()),
12050
12072
  producesTrackedEvents: boolean().optional()
12051
12073
  });
12074
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12075
+ * The batch answer needs the tag; the single-device answer already has it
12076
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12077
+ var LinkedDevicesForDeviceSchema = object({
12078
+ deviceId: number(),
12079
+ mode: LinkedDevicesModeSchema,
12080
+ devices: array(LinkedDeviceSchema)
12081
+ });
12082
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12083
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12084
+ * object literal is exactly how the three drift apart. */
12085
+ var DeviceBindingsForDeviceSchema = object({
12086
+ deviceId: number(),
12087
+ entries: array(object({
12088
+ capName: string(),
12089
+ kind: _enum(["native", "wrapped"]),
12090
+ providerAddonId: string(),
12091
+ providerNodeId: string(),
12092
+ nativeAddonId: string()
12093
+ }))
12094
+ });
12052
12095
  var SavedDeviceRowSchema = object({
12053
12096
  /** Numeric id reserved at allocateDeviceId time. */
12054
12097
  id: number(),
@@ -12274,11 +12317,25 @@ method(object({
12274
12317
  projection: _enum(["full", "slim"]).optional(),
12275
12318
  /** Return only camera devices. Filtering server-side instead of
12276
12319
  * shipping 293 rows to find 12. */
12277
- isCamera: boolean().optional()
12320
+ isCamera: boolean().optional(),
12321
+ /**
12322
+ * Return only these device ids. For the caller that already KNOWS the
12323
+ * handful it wants and needs a field the id-bearing answer does not
12324
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12325
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12326
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12327
+ * refetches on the reconcile interval, on a phone.
12328
+ *
12329
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12330
+ * keys rather than rejecting them (verified against the live hub
12331
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12332
+ * it answers today and the caller filters as it already does.
12333
+ */
12334
+ deviceIds: array(number()).optional()
12278
12335
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12279
12336
  mode: LinkedDevicesModeSchema,
12280
12337
  devices: array(LinkedDeviceSchema)
12281
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12338
+ })), 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({
12282
12339
  deviceId: number(),
12283
12340
  values: record(string(), unknown())
12284
12341
  }), object({ success: literal(true) }), {
@@ -12305,25 +12362,7 @@ method(object({
12305
12362
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12306
12363
  kind: "mutation",
12307
12364
  auth: "admin"
12308
- }), method(object({ deviceId: number() }), object({
12309
- deviceId: number(),
12310
- entries: array(object({
12311
- capName: string(),
12312
- kind: _enum(["native", "wrapped"]),
12313
- providerAddonId: string(),
12314
- providerNodeId: string(),
12315
- nativeAddonId: string()
12316
- }))
12317
- })), method(object({}), array(object({
12318
- deviceId: number(),
12319
- entries: array(object({
12320
- capName: string(),
12321
- kind: _enum(["native", "wrapped"]),
12322
- providerAddonId: string(),
12323
- providerNodeId: string(),
12324
- nativeAddonId: string()
12325
- }))
12326
- }))), method(object({
12365
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12327
12366
  deviceId: number(),
12328
12367
  capName: string(),
12329
12368
  wrapperAddonId: string(),
@@ -14733,12 +14772,15 @@ var NcOccupancyConditionSchema = object({
14733
14772
  * there is no second switch that can disagree with the first and every rule
14734
14773
  * authored before the decision migrates for free (`audioModeOf`):
14735
14774
  *
14736
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14737
- * classifier labels with one of them. No window, no percentage:
14738
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14739
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14740
- * the analyzer's (`classificationMinScore`, per device) a label only
14741
- * reaches this condition if the classifier was already confident enough.
14775
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14776
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14777
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14778
+ * frames is the wrong question for a classifier that labels 1–3 frames
14779
+ * per episode. The count window is the brake that drops a single-frame
14780
+ * false positive; the rule's own `throttle` cooldown is the other. The
14781
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14782
+ * per device) — a label only reaches this condition if the classifier was
14783
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14742
14784
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14743
14785
  * the condition: at least `hitPercent`% of the samples over
14744
14786
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14765,14 +14807,22 @@ var NcOccupancyConditionSchema = object({
14765
14807
  * an operator who typed `dog` mean the same thing.
14766
14808
  */
14767
14809
  var NcAudioConditionSchema = object({
14768
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14810
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14769
14811
  labels: array(string().min(1)).min(1).optional(),
14770
14812
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14771
14813
  dbThreshold: number().min(-96).max(0).optional(),
14772
14814
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14773
14815
  hitPercent: number().int().min(1).max(100).default(60),
14774
14816
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14775
- samplingSeconds: number().int().min(1).max(300).default(10)
14817
+ samplingSeconds: number().int().min(1).max(300).default(10),
14818
+ /**
14819
+ * LABEL MODE: how many labelled frames must land inside
14820
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14821
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14822
+ */
14823
+ confirmHits: number().int().min(1).max(20).optional(),
14824
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14825
+ confirmWindowSec: number().int().min(1).max(60).optional()
14776
14826
  });
14777
14827
  /**
14778
14828
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17146,6 +17196,46 @@ var RecentTracksPageSchema = object({
17146
17196
  /** Cursor for the next page, or null when this page is the last. */
17147
17197
  nextCursor: string().nullable()
17148
17198
  });
17199
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17200
+ var LIST_GROUPS_MAX_LIMIT = 100;
17201
+ var AnalyticsGroupRecordSchema = object({
17202
+ id: string(),
17203
+ deviceId: number().int(),
17204
+ openedAt: number().int(),
17205
+ closedAt: number().int(),
17206
+ timestamp: number().int(),
17207
+ memberCount: number().int(),
17208
+ memberTrackIds: array(string()).readonly(),
17209
+ className: string(),
17210
+ classes: array(string()).readonly(),
17211
+ /** Relative event-media path, or null when the group has no picture yet. */
17212
+ mediaUrl: string().nullable(),
17213
+ singleton: boolean()
17214
+ });
17215
+ var AnalyticsGroupMemberSchema = object({
17216
+ trackId: string(),
17217
+ deviceId: number().int(),
17218
+ className: string(),
17219
+ firstSeen: number().int(),
17220
+ lastSeen: number().int(),
17221
+ mediaUrl: string().nullable()
17222
+ });
17223
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17224
+ var ListGroupsQueryInput = object({
17225
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17226
+ deviceIds: array(number()),
17227
+ /** Window lower bound on `closedAt` (inclusive). */
17228
+ since: number().optional(),
17229
+ /** Window upper bound on `openedAt` (inclusive). */
17230
+ until: number().optional(),
17231
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17232
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17233
+ cursor: string().optional()
17234
+ });
17235
+ var ListGroupsPageSchema = object({
17236
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17237
+ nextCursor: string().nullable()
17238
+ });
17149
17239
  var KeyEventQueryInput = object({
17150
17240
  deviceId: number(),
17151
17241
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17221,7 +17311,9 @@ var TrackCascadeCountsSchema = object({
17221
17311
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17222
17312
  plates: number().int(),
17223
17313
  /** Per-track CLIP search vectors removed (best-effort). */
17224
- embeddings: number().int()
17314
+ embeddings: number().int(),
17315
+ /** Group membership + group rows removed with their last member (best-effort). */
17316
+ groups: number().int()
17225
17317
  });
17226
17318
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17227
17319
  var DiskReconcileCountsSchema = object({
@@ -17367,7 +17459,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17367
17459
  * stationary registry). Default false: the timeline lists passages,
17368
17460
  * not parking records (operator decision, 2026-08-15). */
17369
17461
  includeStationary: boolean().optional()
17370
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17462
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17463
+ deviceId: number(),
17464
+ groupId: string().min(1)
17465
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17371
17466
  kind: "mutation",
17372
17467
  auth: "admin"
17373
17468
  }), 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({
@@ -17677,7 +17772,8 @@ var PipelineModelOptionSchema = object({
17677
17772
  sizeMB: number()
17678
17773
  })),
17679
17774
  group: ModelVariantGroupSchema.optional(),
17680
- legacy: boolean().optional()
17775
+ legacy: boolean().optional(),
17776
+ provider: ModelProviderIdSchema.optional()
17681
17777
  });
17682
17778
  var ConfigFieldBridge = custom();
17683
17779
  var PipelineAddonSchemaSchema = object({
@@ -25886,7 +25982,12 @@ var PlateInfoSchema = object({
25886
25982
  plateBbox: BoundingBoxSchema.optional(),
25887
25983
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25888
25984
  keyFrameMediaKey: string().optional(),
25889
- base64: string().optional()
25985
+ base64: string().optional(),
25986
+ /**
25987
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25988
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25989
+ */
25990
+ cropUrl: string().optional()
25890
25991
  });
25891
25992
  var MediaFileLiteSchema = object({
25892
25993
  key: string(),
@@ -31410,6 +31511,12 @@ Object.freeze({
31410
31511
  addonId: null,
31411
31512
  access: "view"
31412
31513
  },
31514
+ "deviceManager.getBindingsBatch": {
31515
+ capName: "device-manager",
31516
+ capScope: "system",
31517
+ addonId: null,
31518
+ access: "view"
31519
+ },
31413
31520
  "deviceManager.getChildren": {
31414
31521
  capName: "device-manager",
31415
31522
  capScope: "system",
@@ -31470,6 +31577,12 @@ Object.freeze({
31470
31577
  addonId: null,
31471
31578
  access: "view"
31472
31579
  },
31580
+ "deviceManager.getLinkedDevicesBatch": {
31581
+ capName: "device-manager",
31582
+ capScope: "system",
31583
+ addonId: null,
31584
+ access: "view"
31585
+ },
31473
31586
  "deviceManager.getRoleDisplayDefaults": {
31474
31587
  capName: "device-manager",
31475
31588
  capScope: "system",
@@ -33240,6 +33353,12 @@ Object.freeze({
33240
33353
  addonId: null,
33241
33354
  access: "view"
33242
33355
  },
33356
+ "pipelineAnalytics.getGroup": {
33357
+ capName: "pipeline-analytics",
33358
+ capScope: "device",
33359
+ addonId: null,
33360
+ access: "view"
33361
+ },
33243
33362
  "pipelineAnalytics.getKeyEvents": {
33244
33363
  capName: "pipeline-analytics",
33245
33364
  capScope: "device",
@@ -33324,6 +33443,12 @@ Object.freeze({
33324
33443
  addonId: null,
33325
33444
  access: "view"
33326
33445
  },
33446
+ "pipelineAnalytics.listGroups": {
33447
+ capName: "pipeline-analytics",
33448
+ capScope: "device",
33449
+ addonId: null,
33450
+ access: "view"
33451
+ },
33327
33452
  "pipelineAnalytics.listOpsLog": {
33328
33453
  capName: "pipeline-analytics",
33329
33454
  capScope: "device",
@@ -36105,6 +36230,11 @@ Object.freeze({
36105
36230
  form: "single",
36106
36231
  optional: false
36107
36232
  }],
36233
+ "deviceManager.getBindingsBatch": [{
36234
+ name: "deviceIds",
36235
+ form: "array",
36236
+ optional: false
36237
+ }],
36108
36238
  "deviceManager.getChildren": [{
36109
36239
  name: "parentDeviceId",
36110
36240
  form: "single",
@@ -36150,6 +36280,11 @@ Object.freeze({
36150
36280
  form: "single",
36151
36281
  optional: false
36152
36282
  }],
36283
+ "deviceManager.getLinkedDevicesBatch": [{
36284
+ name: "deviceIds",
36285
+ form: "array",
36286
+ optional: false
36287
+ }],
36153
36288
  "deviceManager.getSettingsSchema": [{
36154
36289
  name: "deviceId",
36155
36290
  form: "single",
@@ -36170,6 +36305,11 @@ Object.freeze({
36170
36305
  form: "single",
36171
36306
  optional: false
36172
36307
  }],
36308
+ "deviceManager.listAll": [{
36309
+ name: "deviceIds",
36310
+ form: "array",
36311
+ optional: true
36312
+ }],
36173
36313
  "deviceManager.loadConfig": [{
36174
36314
  name: "deviceId",
36175
36315
  form: "single",
@@ -36743,6 +36883,11 @@ Object.freeze({
36743
36883
  form: "single",
36744
36884
  optional: false
36745
36885
  }],
36886
+ "pipelineAnalytics.getGroup": [{
36887
+ name: "deviceId",
36888
+ form: "single",
36889
+ optional: false
36890
+ }],
36746
36891
  "pipelineAnalytics.getKeyEvents": [{
36747
36892
  name: "deviceId",
36748
36893
  form: "single",
@@ -36798,6 +36943,11 @@ Object.freeze({
36798
36943
  form: "array",
36799
36944
  optional: false
36800
36945
  }],
36946
+ "pipelineAnalytics.listGroups": [{
36947
+ name: "deviceIds",
36948
+ form: "array",
36949
+ optional: false
36950
+ }],
36801
36951
  "pipelineAnalytics.listOpsLog": [{
36802
36952
  name: "deviceId",
36803
36953
  form: "single",
package/dist/addon.mjs CHANGED
@@ -5812,6 +5812,13 @@ var BaseAddon = class {
5812
5812
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5813
5813
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5814
5814
  _registeredCapNames = [];
5815
+ /**
5816
+ * True only after `readAddonStore` actually answered. Constructor
5817
+ * defaults look like stored config when the store is down — a forked
5818
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5819
+ * mode, 2026-08-25) is not "the operator chose this".
5820
+ */
5821
+ settingsStoreReady = false;
5815
5822
  /** Default config values. Provided via constructor. */
5816
5823
  defaults;
5817
5824
  constructor(defaults) {
@@ -6212,7 +6219,9 @@ var BaseAddon = class {
6212
6219
  ];
6213
6220
  let lastErr;
6214
6221
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6215
- return await settings.readAddonStore() ?? {};
6222
+ const stored = await settings.readAddonStore() ?? {};
6223
+ this.settingsStoreReady = true;
6224
+ return stored;
6216
6225
  } catch (err) {
6217
6226
  lastErr = err;
6218
6227
  const msg = err instanceof Error ? err.message : String(err);
@@ -6220,6 +6229,7 @@ var BaseAddon = class {
6220
6229
  if (attempt === delaysMs.length) break;
6221
6230
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6222
6231
  }
6232
+ this.settingsStoreReady = false;
6223
6233
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6224
6234
  return {};
6225
6235
  }
@@ -8123,6 +8133,12 @@ var ModelVariantGroupSchema = object({
8123
8133
  */
8124
8134
  resolution: number().int().positive().optional()
8125
8135
  });
8136
+ var ModelProviderIdSchema = _enum([
8137
+ "camstack",
8138
+ "frigate",
8139
+ "scrypted",
8140
+ "custom"
8141
+ ]);
8126
8142
  var ModelCatalogEntrySchema = object({
8127
8143
  id: string(),
8128
8144
  name: string(),
@@ -8220,6 +8236,12 @@ var ModelCatalogEntrySchema = object({
8220
8236
  */
8221
8237
  group: ModelVariantGroupSchema.optional(),
8222
8238
  /**
8239
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8240
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8241
+ * persisted before this field existed (`inferModelProvider` fills those).
8242
+ */
8243
+ provider: ModelProviderIdSchema.optional(),
8244
+ /**
8223
8245
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8224
8246
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8225
8247
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12028,6 +12050,27 @@ var LinkedDeviceSchema = object({
12028
12050
  features: array(string()),
12029
12051
  producesTrackedEvents: boolean().optional()
12030
12052
  });
12053
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12054
+ * The batch answer needs the tag; the single-device answer already has it
12055
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12056
+ var LinkedDevicesForDeviceSchema = object({
12057
+ deviceId: number(),
12058
+ mode: LinkedDevicesModeSchema,
12059
+ devices: array(LinkedDeviceSchema)
12060
+ });
12061
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12062
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12063
+ * object literal is exactly how the three drift apart. */
12064
+ var DeviceBindingsForDeviceSchema = object({
12065
+ deviceId: number(),
12066
+ entries: array(object({
12067
+ capName: string(),
12068
+ kind: _enum(["native", "wrapped"]),
12069
+ providerAddonId: string(),
12070
+ providerNodeId: string(),
12071
+ nativeAddonId: string()
12072
+ }))
12073
+ });
12031
12074
  var SavedDeviceRowSchema = object({
12032
12075
  /** Numeric id reserved at allocateDeviceId time. */
12033
12076
  id: number(),
@@ -12253,11 +12296,25 @@ method(object({
12253
12296
  projection: _enum(["full", "slim"]).optional(),
12254
12297
  /** Return only camera devices. Filtering server-side instead of
12255
12298
  * shipping 293 rows to find 12. */
12256
- isCamera: boolean().optional()
12299
+ isCamera: boolean().optional(),
12300
+ /**
12301
+ * Return only these device ids. For the caller that already KNOWS the
12302
+ * handful it wants and needs a field the id-bearing answer does not
12303
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12304
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12305
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12306
+ * refetches on the reconcile interval, on a phone.
12307
+ *
12308
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12309
+ * keys rather than rejecting them (verified against the live hub
12310
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12311
+ * it answers today and the caller filters as it already does.
12312
+ */
12313
+ deviceIds: array(number()).optional()
12257
12314
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12258
12315
  mode: LinkedDevicesModeSchema,
12259
12316
  devices: array(LinkedDeviceSchema)
12260
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12317
+ })), 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({
12261
12318
  deviceId: number(),
12262
12319
  values: record(string(), unknown())
12263
12320
  }), object({ success: literal(true) }), {
@@ -12284,25 +12341,7 @@ method(object({
12284
12341
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12285
12342
  kind: "mutation",
12286
12343
  auth: "admin"
12287
- }), method(object({ deviceId: number() }), object({
12288
- deviceId: number(),
12289
- entries: array(object({
12290
- capName: string(),
12291
- kind: _enum(["native", "wrapped"]),
12292
- providerAddonId: string(),
12293
- providerNodeId: string(),
12294
- nativeAddonId: string()
12295
- }))
12296
- })), method(object({}), array(object({
12297
- deviceId: number(),
12298
- entries: array(object({
12299
- capName: string(),
12300
- kind: _enum(["native", "wrapped"]),
12301
- providerAddonId: string(),
12302
- providerNodeId: string(),
12303
- nativeAddonId: string()
12304
- }))
12305
- }))), method(object({
12344
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12306
12345
  deviceId: number(),
12307
12346
  capName: string(),
12308
12347
  wrapperAddonId: string(),
@@ -14712,12 +14751,15 @@ var NcOccupancyConditionSchema = object({
14712
14751
  * there is no second switch that can disagree with the first and every rule
14713
14752
  * authored before the decision migrates for free (`audioModeOf`):
14714
14753
  *
14715
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14716
- * classifier labels with one of them. No window, no percentage:
14717
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14718
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14719
- * the analyzer's (`classificationMinScore`, per device) a label only
14720
- * reaches this condition if the classifier was already confident enough.
14754
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14755
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14756
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14757
+ * frames is the wrong question for a classifier that labels 1–3 frames
14758
+ * per episode. The count window is the brake that drops a single-frame
14759
+ * false positive; the rule's own `throttle` cooldown is the other. The
14760
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14761
+ * per device) — a label only reaches this condition if the classifier was
14762
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14721
14763
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14722
14764
  * the condition: at least `hitPercent`% of the samples over
14723
14765
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14744,14 +14786,22 @@ var NcOccupancyConditionSchema = object({
14744
14786
  * an operator who typed `dog` mean the same thing.
14745
14787
  */
14746
14788
  var NcAudioConditionSchema = object({
14747
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14789
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14748
14790
  labels: array(string().min(1)).min(1).optional(),
14749
14791
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14750
14792
  dbThreshold: number().min(-96).max(0).optional(),
14751
14793
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14752
14794
  hitPercent: number().int().min(1).max(100).default(60),
14753
14795
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14754
- samplingSeconds: number().int().min(1).max(300).default(10)
14796
+ samplingSeconds: number().int().min(1).max(300).default(10),
14797
+ /**
14798
+ * LABEL MODE: how many labelled frames must land inside
14799
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14800
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14801
+ */
14802
+ confirmHits: number().int().min(1).max(20).optional(),
14803
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14804
+ confirmWindowSec: number().int().min(1).max(60).optional()
14755
14805
  });
14756
14806
  /**
14757
14807
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17125,6 +17175,46 @@ var RecentTracksPageSchema = object({
17125
17175
  /** Cursor for the next page, or null when this page is the last. */
17126
17176
  nextCursor: string().nullable()
17127
17177
  });
17178
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17179
+ var LIST_GROUPS_MAX_LIMIT = 100;
17180
+ var AnalyticsGroupRecordSchema = object({
17181
+ id: string(),
17182
+ deviceId: number().int(),
17183
+ openedAt: number().int(),
17184
+ closedAt: number().int(),
17185
+ timestamp: number().int(),
17186
+ memberCount: number().int(),
17187
+ memberTrackIds: array(string()).readonly(),
17188
+ className: string(),
17189
+ classes: array(string()).readonly(),
17190
+ /** Relative event-media path, or null when the group has no picture yet. */
17191
+ mediaUrl: string().nullable(),
17192
+ singleton: boolean()
17193
+ });
17194
+ var AnalyticsGroupMemberSchema = object({
17195
+ trackId: string(),
17196
+ deviceId: number().int(),
17197
+ className: string(),
17198
+ firstSeen: number().int(),
17199
+ lastSeen: number().int(),
17200
+ mediaUrl: string().nullable()
17201
+ });
17202
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17203
+ var ListGroupsQueryInput = object({
17204
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17205
+ deviceIds: array(number()),
17206
+ /** Window lower bound on `closedAt` (inclusive). */
17207
+ since: number().optional(),
17208
+ /** Window upper bound on `openedAt` (inclusive). */
17209
+ until: number().optional(),
17210
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17211
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17212
+ cursor: string().optional()
17213
+ });
17214
+ var ListGroupsPageSchema = object({
17215
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17216
+ nextCursor: string().nullable()
17217
+ });
17128
17218
  var KeyEventQueryInput = object({
17129
17219
  deviceId: number(),
17130
17220
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17200,7 +17290,9 @@ var TrackCascadeCountsSchema = object({
17200
17290
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17201
17291
  plates: number().int(),
17202
17292
  /** Per-track CLIP search vectors removed (best-effort). */
17203
- embeddings: number().int()
17293
+ embeddings: number().int(),
17294
+ /** Group membership + group rows removed with their last member (best-effort). */
17295
+ groups: number().int()
17204
17296
  });
17205
17297
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17206
17298
  var DiskReconcileCountsSchema = object({
@@ -17346,7 +17438,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17346
17438
  * stationary registry). Default false: the timeline lists passages,
17347
17439
  * not parking records (operator decision, 2026-08-15). */
17348
17440
  includeStationary: boolean().optional()
17349
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17441
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17442
+ deviceId: number(),
17443
+ groupId: string().min(1)
17444
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17350
17445
  kind: "mutation",
17351
17446
  auth: "admin"
17352
17447
  }), 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({
@@ -17656,7 +17751,8 @@ var PipelineModelOptionSchema = object({
17656
17751
  sizeMB: number()
17657
17752
  })),
17658
17753
  group: ModelVariantGroupSchema.optional(),
17659
- legacy: boolean().optional()
17754
+ legacy: boolean().optional(),
17755
+ provider: ModelProviderIdSchema.optional()
17660
17756
  });
17661
17757
  var ConfigFieldBridge = custom();
17662
17758
  var PipelineAddonSchemaSchema = object({
@@ -25865,7 +25961,12 @@ var PlateInfoSchema = object({
25865
25961
  plateBbox: BoundingBoxSchema.optional(),
25866
25962
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25867
25963
  keyFrameMediaKey: string().optional(),
25868
- base64: string().optional()
25964
+ base64: string().optional(),
25965
+ /**
25966
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25967
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25968
+ */
25969
+ cropUrl: string().optional()
25869
25970
  });
25870
25971
  var MediaFileLiteSchema = object({
25871
25972
  key: string(),
@@ -31389,6 +31490,12 @@ Object.freeze({
31389
31490
  addonId: null,
31390
31491
  access: "view"
31391
31492
  },
31493
+ "deviceManager.getBindingsBatch": {
31494
+ capName: "device-manager",
31495
+ capScope: "system",
31496
+ addonId: null,
31497
+ access: "view"
31498
+ },
31392
31499
  "deviceManager.getChildren": {
31393
31500
  capName: "device-manager",
31394
31501
  capScope: "system",
@@ -31449,6 +31556,12 @@ Object.freeze({
31449
31556
  addonId: null,
31450
31557
  access: "view"
31451
31558
  },
31559
+ "deviceManager.getLinkedDevicesBatch": {
31560
+ capName: "device-manager",
31561
+ capScope: "system",
31562
+ addonId: null,
31563
+ access: "view"
31564
+ },
31452
31565
  "deviceManager.getRoleDisplayDefaults": {
31453
31566
  capName: "device-manager",
31454
31567
  capScope: "system",
@@ -33219,6 +33332,12 @@ Object.freeze({
33219
33332
  addonId: null,
33220
33333
  access: "view"
33221
33334
  },
33335
+ "pipelineAnalytics.getGroup": {
33336
+ capName: "pipeline-analytics",
33337
+ capScope: "device",
33338
+ addonId: null,
33339
+ access: "view"
33340
+ },
33222
33341
  "pipelineAnalytics.getKeyEvents": {
33223
33342
  capName: "pipeline-analytics",
33224
33343
  capScope: "device",
@@ -33303,6 +33422,12 @@ Object.freeze({
33303
33422
  addonId: null,
33304
33423
  access: "view"
33305
33424
  },
33425
+ "pipelineAnalytics.listGroups": {
33426
+ capName: "pipeline-analytics",
33427
+ capScope: "device",
33428
+ addonId: null,
33429
+ access: "view"
33430
+ },
33306
33431
  "pipelineAnalytics.listOpsLog": {
33307
33432
  capName: "pipeline-analytics",
33308
33433
  capScope: "device",
@@ -36084,6 +36209,11 @@ Object.freeze({
36084
36209
  form: "single",
36085
36210
  optional: false
36086
36211
  }],
36212
+ "deviceManager.getBindingsBatch": [{
36213
+ name: "deviceIds",
36214
+ form: "array",
36215
+ optional: false
36216
+ }],
36087
36217
  "deviceManager.getChildren": [{
36088
36218
  name: "parentDeviceId",
36089
36219
  form: "single",
@@ -36129,6 +36259,11 @@ Object.freeze({
36129
36259
  form: "single",
36130
36260
  optional: false
36131
36261
  }],
36262
+ "deviceManager.getLinkedDevicesBatch": [{
36263
+ name: "deviceIds",
36264
+ form: "array",
36265
+ optional: false
36266
+ }],
36132
36267
  "deviceManager.getSettingsSchema": [{
36133
36268
  name: "deviceId",
36134
36269
  form: "single",
@@ -36149,6 +36284,11 @@ Object.freeze({
36149
36284
  form: "single",
36150
36285
  optional: false
36151
36286
  }],
36287
+ "deviceManager.listAll": [{
36288
+ name: "deviceIds",
36289
+ form: "array",
36290
+ optional: true
36291
+ }],
36152
36292
  "deviceManager.loadConfig": [{
36153
36293
  name: "deviceId",
36154
36294
  form: "single",
@@ -36722,6 +36862,11 @@ Object.freeze({
36722
36862
  form: "single",
36723
36863
  optional: false
36724
36864
  }],
36865
+ "pipelineAnalytics.getGroup": [{
36866
+ name: "deviceId",
36867
+ form: "single",
36868
+ optional: false
36869
+ }],
36725
36870
  "pipelineAnalytics.getKeyEvents": [{
36726
36871
  name: "deviceId",
36727
36872
  form: "single",
@@ -36777,6 +36922,11 @@ Object.freeze({
36777
36922
  form: "array",
36778
36923
  optional: false
36779
36924
  }],
36925
+ "pipelineAnalytics.listGroups": [{
36926
+ name: "deviceIds",
36927
+ form: "array",
36928
+ optional: false
36929
+ }],
36780
36930
  "pipelineAnalytics.listOpsLog": [{
36781
36931
  name: "deviceId",
36782
36932
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-wyze",
3
- "version": "0.2.31",
3
+ "version": "0.2.32",
4
4
  "description": "Wyze camera device-provider addon for CamStack — wraps the @apocaliss92/wyze-bridge-js P2P/DTLS client, feeding the stream-broker via the pull-rfc4571 lazy-publish path (a structural twin of addon-provider-reolink)",
5
5
  "keywords": [
6
6
  "camstack",