@camstack/addon-provider-petkit 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
@@ -6903,6 +6903,13 @@ var BaseAddon = class {
6903
6903
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
6904
6904
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
6905
6905
  _registeredCapNames = [];
6906
+ /**
6907
+ * True only after `readAddonStore` actually answered. Constructor
6908
+ * defaults look like stored config when the store is down — a forked
6909
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
6910
+ * mode, 2026-08-25) is not "the operator chose this".
6911
+ */
6912
+ settingsStoreReady = false;
6906
6913
  /** Default config values. Provided via constructor. */
6907
6914
  defaults;
6908
6915
  constructor(defaults) {
@@ -7303,7 +7310,9 @@ var BaseAddon = class {
7303
7310
  ];
7304
7311
  let lastErr;
7305
7312
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
7306
- return await settings.readAddonStore() ?? {};
7313
+ const stored = await settings.readAddonStore() ?? {};
7314
+ this.settingsStoreReady = true;
7315
+ return stored;
7307
7316
  } catch (err) {
7308
7317
  lastErr = err;
7309
7318
  const msg = err instanceof Error ? err.message : String(err);
@@ -7311,6 +7320,7 @@ var BaseAddon = class {
7311
7320
  if (attempt === delaysMs.length) break;
7312
7321
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
7313
7322
  }
7323
+ this.settingsStoreReady = false;
7314
7324
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
7315
7325
  return {};
7316
7326
  }
@@ -9214,6 +9224,12 @@ var ModelVariantGroupSchema = object({
9214
9224
  */
9215
9225
  resolution: number().int().positive().optional()
9216
9226
  });
9227
+ var ModelProviderIdSchema = _enum([
9228
+ "camstack",
9229
+ "frigate",
9230
+ "scrypted",
9231
+ "custom"
9232
+ ]);
9217
9233
  var ModelCatalogEntrySchema = object({
9218
9234
  id: string(),
9219
9235
  name: string(),
@@ -9311,6 +9327,12 @@ var ModelCatalogEntrySchema = object({
9311
9327
  */
9312
9328
  group: ModelVariantGroupSchema.optional(),
9313
9329
  /**
9330
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
9331
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
9332
+ * persisted before this field existed (`inferModelProvider` fills those).
9333
+ */
9334
+ provider: ModelProviderIdSchema.optional(),
9335
+ /**
9314
9336
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
9315
9337
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
9316
9338
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -13119,6 +13141,27 @@ var LinkedDeviceSchema = object({
13119
13141
  features: array(string()),
13120
13142
  producesTrackedEvents: boolean().optional()
13121
13143
  });
13144
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
13145
+ * The batch answer needs the tag; the single-device answer already has it
13146
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
13147
+ var LinkedDevicesForDeviceSchema = object({
13148
+ deviceId: number(),
13149
+ mode: LinkedDevicesModeSchema,
13150
+ devices: array(LinkedDeviceSchema)
13151
+ });
13152
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
13153
+ * `getAllBindings` all answer in. Declared once: three copies of the same
13154
+ * object literal is exactly how the three drift apart. */
13155
+ var DeviceBindingsForDeviceSchema = object({
13156
+ deviceId: number(),
13157
+ entries: array(object({
13158
+ capName: string(),
13159
+ kind: _enum(["native", "wrapped"]),
13160
+ providerAddonId: string(),
13161
+ providerNodeId: string(),
13162
+ nativeAddonId: string()
13163
+ }))
13164
+ });
13122
13165
  var SavedDeviceRowSchema = object({
13123
13166
  /** Numeric id reserved at allocateDeviceId time. */
13124
13167
  id: number(),
@@ -13344,11 +13387,25 @@ method(object({
13344
13387
  projection: _enum(["full", "slim"]).optional(),
13345
13388
  /** Return only camera devices. Filtering server-side instead of
13346
13389
  * shipping 293 rows to find 12. */
13347
- isCamera: boolean().optional()
13390
+ isCamera: boolean().optional(),
13391
+ /**
13392
+ * Return only these device ids. For the caller that already KNOWS the
13393
+ * handful it wants and needs a field the id-bearing answer does not
13394
+ * carry — the viewer's linked-devices panel joins `type` and `online`
13395
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
13396
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
13397
+ * refetches on the reconcile interval, on a phone.
13398
+ *
13399
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
13400
+ * keys rather than rejecting them (verified against the live hub
13401
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
13402
+ * it answers today and the caller filters as it already does.
13403
+ */
13404
+ deviceIds: array(number()).optional()
13348
13405
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
13349
13406
  mode: LinkedDevicesModeSchema,
13350
13407
  devices: array(LinkedDeviceSchema)
13351
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
13408
+ })), 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({
13352
13409
  deviceId: number(),
13353
13410
  values: record(string(), unknown())
13354
13411
  }), object({ success: literal(true) }), {
@@ -13375,25 +13432,7 @@ method(object({
13375
13432
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
13376
13433
  kind: "mutation",
13377
13434
  auth: "admin"
13378
- }), method(object({ deviceId: number() }), object({
13379
- deviceId: number(),
13380
- entries: array(object({
13381
- capName: string(),
13382
- kind: _enum(["native", "wrapped"]),
13383
- providerAddonId: string(),
13384
- providerNodeId: string(),
13385
- nativeAddonId: string()
13386
- }))
13387
- })), method(object({}), array(object({
13388
- deviceId: number(),
13389
- entries: array(object({
13390
- capName: string(),
13391
- kind: _enum(["native", "wrapped"]),
13392
- providerAddonId: string(),
13393
- providerNodeId: string(),
13394
- nativeAddonId: string()
13395
- }))
13396
- }))), method(object({
13435
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
13397
13436
  deviceId: number(),
13398
13437
  capName: string(),
13399
13438
  wrapperAddonId: string(),
@@ -15803,12 +15842,15 @@ var NcOccupancyConditionSchema = object({
15803
15842
  * there is no second switch that can disagree with the first and every rule
15804
15843
  * authored before the decision migrates for free (`audioModeOf`):
15805
15844
  *
15806
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
15807
- * classifier labels with one of them. No window, no percentage:
15808
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
15809
- * `throttle` cooldown is the only brake. The per-label confidence floor is
15810
- * the analyzer's (`classificationMinScore`, per device) a label only
15811
- * reaches this condition if the classifier was already confident enough.
15845
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
15846
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
15847
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
15848
+ * frames is the wrong question for a classifier that labels 1–3 frames
15849
+ * per episode. The count window is the brake that drops a single-frame
15850
+ * false positive; the rule's own `throttle` cooldown is the other. The
15851
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
15852
+ * per device) — a label only reaches this condition if the classifier was
15853
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
15812
15854
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
15813
15855
  * the condition: at least `hitPercent`% of the samples over
15814
15856
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -15835,14 +15877,22 @@ var NcOccupancyConditionSchema = object({
15835
15877
  * an operator who typed `dog` mean the same thing.
15836
15878
  */
15837
15879
  var NcAudioConditionSchema = object({
15838
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
15880
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
15839
15881
  labels: array(string().min(1)).min(1).optional(),
15840
15882
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
15841
15883
  dbThreshold: number().min(-96).max(0).optional(),
15842
15884
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
15843
15885
  hitPercent: number().int().min(1).max(100).default(60),
15844
15886
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
15845
- samplingSeconds: number().int().min(1).max(300).default(10)
15887
+ samplingSeconds: number().int().min(1).max(300).default(10),
15888
+ /**
15889
+ * LABEL MODE: how many labelled frames must land inside
15890
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
15891
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
15892
+ */
15893
+ confirmHits: number().int().min(1).max(20).optional(),
15894
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
15895
+ confirmWindowSec: number().int().min(1).max(60).optional()
15846
15896
  });
15847
15897
  /**
15848
15898
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -18216,6 +18266,46 @@ var RecentTracksPageSchema = object({
18216
18266
  /** Cursor for the next page, or null when this page is the last. */
18217
18267
  nextCursor: string().nullable()
18218
18268
  });
18269
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
18270
+ var LIST_GROUPS_MAX_LIMIT = 100;
18271
+ var AnalyticsGroupRecordSchema = object({
18272
+ id: string(),
18273
+ deviceId: number().int(),
18274
+ openedAt: number().int(),
18275
+ closedAt: number().int(),
18276
+ timestamp: number().int(),
18277
+ memberCount: number().int(),
18278
+ memberTrackIds: array(string()).readonly(),
18279
+ className: string(),
18280
+ classes: array(string()).readonly(),
18281
+ /** Relative event-media path, or null when the group has no picture yet. */
18282
+ mediaUrl: string().nullable(),
18283
+ singleton: boolean()
18284
+ });
18285
+ var AnalyticsGroupMemberSchema = object({
18286
+ trackId: string(),
18287
+ deviceId: number().int(),
18288
+ className: string(),
18289
+ firstSeen: number().int(),
18290
+ lastSeen: number().int(),
18291
+ mediaUrl: string().nullable()
18292
+ });
18293
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
18294
+ var ListGroupsQueryInput = object({
18295
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
18296
+ deviceIds: array(number()),
18297
+ /** Window lower bound on `closedAt` (inclusive). */
18298
+ since: number().optional(),
18299
+ /** Window upper bound on `openedAt` (inclusive). */
18300
+ until: number().optional(),
18301
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
18302
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
18303
+ cursor: string().optional()
18304
+ });
18305
+ var ListGroupsPageSchema = object({
18306
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
18307
+ nextCursor: string().nullable()
18308
+ });
18219
18309
  var KeyEventQueryInput = object({
18220
18310
  deviceId: number(),
18221
18311
  /** Window lower bound (track firstSeen ≥ since). */
@@ -18291,7 +18381,9 @@ var TrackCascadeCountsSchema = object({
18291
18381
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
18292
18382
  plates: number().int(),
18293
18383
  /** Per-track CLIP search vectors removed (best-effort). */
18294
- embeddings: number().int()
18384
+ embeddings: number().int(),
18385
+ /** Group membership + group rows removed with their last member (best-effort). */
18386
+ groups: number().int()
18295
18387
  });
18296
18388
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
18297
18389
  var DiskReconcileCountsSchema = object({
@@ -18437,7 +18529,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18437
18529
  * stationary registry). Default false: the timeline lists passages,
18438
18530
  * not parking records (operator decision, 2026-08-15). */
18439
18531
  includeStationary: boolean().optional()
18440
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
18532
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
18533
+ deviceId: number(),
18534
+ groupId: string().min(1)
18535
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
18441
18536
  kind: "mutation",
18442
18537
  auth: "admin"
18443
18538
  }), 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({
@@ -18747,7 +18842,8 @@ var PipelineModelOptionSchema = object({
18747
18842
  sizeMB: number()
18748
18843
  })),
18749
18844
  group: ModelVariantGroupSchema.optional(),
18750
- legacy: boolean().optional()
18845
+ legacy: boolean().optional(),
18846
+ provider: ModelProviderIdSchema.optional()
18751
18847
  });
18752
18848
  var ConfigFieldBridge = custom();
18753
18849
  var PipelineAddonSchemaSchema = object({
@@ -26852,7 +26948,12 @@ var PlateInfoSchema = object({
26852
26948
  plateBbox: BoundingBoxSchema.optional(),
26853
26949
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
26854
26950
  keyFrameMediaKey: string().optional(),
26855
- base64: string().optional()
26951
+ base64: string().optional(),
26952
+ /**
26953
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
26954
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
26955
+ */
26956
+ cropUrl: string().optional()
26856
26957
  });
26857
26958
  var MediaFileLiteSchema = object({
26858
26959
  key: string(),
@@ -32376,6 +32477,12 @@ Object.freeze({
32376
32477
  addonId: null,
32377
32478
  access: "view"
32378
32479
  },
32480
+ "deviceManager.getBindingsBatch": {
32481
+ capName: "device-manager",
32482
+ capScope: "system",
32483
+ addonId: null,
32484
+ access: "view"
32485
+ },
32379
32486
  "deviceManager.getChildren": {
32380
32487
  capName: "device-manager",
32381
32488
  capScope: "system",
@@ -32436,6 +32543,12 @@ Object.freeze({
32436
32543
  addonId: null,
32437
32544
  access: "view"
32438
32545
  },
32546
+ "deviceManager.getLinkedDevicesBatch": {
32547
+ capName: "device-manager",
32548
+ capScope: "system",
32549
+ addonId: null,
32550
+ access: "view"
32551
+ },
32439
32552
  "deviceManager.getRoleDisplayDefaults": {
32440
32553
  capName: "device-manager",
32441
32554
  capScope: "system",
@@ -34206,6 +34319,12 @@ Object.freeze({
34206
34319
  addonId: null,
34207
34320
  access: "view"
34208
34321
  },
34322
+ "pipelineAnalytics.getGroup": {
34323
+ capName: "pipeline-analytics",
34324
+ capScope: "device",
34325
+ addonId: null,
34326
+ access: "view"
34327
+ },
34209
34328
  "pipelineAnalytics.getKeyEvents": {
34210
34329
  capName: "pipeline-analytics",
34211
34330
  capScope: "device",
@@ -34290,6 +34409,12 @@ Object.freeze({
34290
34409
  addonId: null,
34291
34410
  access: "view"
34292
34411
  },
34412
+ "pipelineAnalytics.listGroups": {
34413
+ capName: "pipeline-analytics",
34414
+ capScope: "device",
34415
+ addonId: null,
34416
+ access: "view"
34417
+ },
34293
34418
  "pipelineAnalytics.listOpsLog": {
34294
34419
  capName: "pipeline-analytics",
34295
34420
  capScope: "device",
@@ -37071,6 +37196,11 @@ Object.freeze({
37071
37196
  form: "single",
37072
37197
  optional: false
37073
37198
  }],
37199
+ "deviceManager.getBindingsBatch": [{
37200
+ name: "deviceIds",
37201
+ form: "array",
37202
+ optional: false
37203
+ }],
37074
37204
  "deviceManager.getChildren": [{
37075
37205
  name: "parentDeviceId",
37076
37206
  form: "single",
@@ -37116,6 +37246,11 @@ Object.freeze({
37116
37246
  form: "single",
37117
37247
  optional: false
37118
37248
  }],
37249
+ "deviceManager.getLinkedDevicesBatch": [{
37250
+ name: "deviceIds",
37251
+ form: "array",
37252
+ optional: false
37253
+ }],
37119
37254
  "deviceManager.getSettingsSchema": [{
37120
37255
  name: "deviceId",
37121
37256
  form: "single",
@@ -37136,6 +37271,11 @@ Object.freeze({
37136
37271
  form: "single",
37137
37272
  optional: false
37138
37273
  }],
37274
+ "deviceManager.listAll": [{
37275
+ name: "deviceIds",
37276
+ form: "array",
37277
+ optional: true
37278
+ }],
37139
37279
  "deviceManager.loadConfig": [{
37140
37280
  name: "deviceId",
37141
37281
  form: "single",
@@ -37709,6 +37849,11 @@ Object.freeze({
37709
37849
  form: "single",
37710
37850
  optional: false
37711
37851
  }],
37852
+ "pipelineAnalytics.getGroup": [{
37853
+ name: "deviceId",
37854
+ form: "single",
37855
+ optional: false
37856
+ }],
37712
37857
  "pipelineAnalytics.getKeyEvents": [{
37713
37858
  name: "deviceId",
37714
37859
  form: "single",
@@ -37764,6 +37909,11 @@ Object.freeze({
37764
37909
  form: "array",
37765
37910
  optional: false
37766
37911
  }],
37912
+ "pipelineAnalytics.listGroups": [{
37913
+ name: "deviceIds",
37914
+ form: "array",
37915
+ optional: false
37916
+ }],
37767
37917
  "pipelineAnalytics.listOpsLog": [{
37768
37918
  name: "deviceId",
37769
37919
  form: "single",
package/dist/addon.mjs CHANGED
@@ -6902,6 +6902,13 @@ var BaseAddon = class {
6902
6902
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
6903
6903
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
6904
6904
  _registeredCapNames = [];
6905
+ /**
6906
+ * True only after `readAddonStore` actually answered. Constructor
6907
+ * defaults look like stored config when the store is down — a forked
6908
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
6909
+ * mode, 2026-08-25) is not "the operator chose this".
6910
+ */
6911
+ settingsStoreReady = false;
6905
6912
  /** Default config values. Provided via constructor. */
6906
6913
  defaults;
6907
6914
  constructor(defaults) {
@@ -7302,7 +7309,9 @@ var BaseAddon = class {
7302
7309
  ];
7303
7310
  let lastErr;
7304
7311
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
7305
- return await settings.readAddonStore() ?? {};
7312
+ const stored = await settings.readAddonStore() ?? {};
7313
+ this.settingsStoreReady = true;
7314
+ return stored;
7306
7315
  } catch (err) {
7307
7316
  lastErr = err;
7308
7317
  const msg = err instanceof Error ? err.message : String(err);
@@ -7310,6 +7319,7 @@ var BaseAddon = class {
7310
7319
  if (attempt === delaysMs.length) break;
7311
7320
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
7312
7321
  }
7322
+ this.settingsStoreReady = false;
7313
7323
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
7314
7324
  return {};
7315
7325
  }
@@ -9213,6 +9223,12 @@ var ModelVariantGroupSchema = object({
9213
9223
  */
9214
9224
  resolution: number().int().positive().optional()
9215
9225
  });
9226
+ var ModelProviderIdSchema = _enum([
9227
+ "camstack",
9228
+ "frigate",
9229
+ "scrypted",
9230
+ "custom"
9231
+ ]);
9216
9232
  var ModelCatalogEntrySchema = object({
9217
9233
  id: string(),
9218
9234
  name: string(),
@@ -9310,6 +9326,12 @@ var ModelCatalogEntrySchema = object({
9310
9326
  */
9311
9327
  group: ModelVariantGroupSchema.optional(),
9312
9328
  /**
9329
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
9330
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
9331
+ * persisted before this field existed (`inferModelProvider` fills those).
9332
+ */
9333
+ provider: ModelProviderIdSchema.optional(),
9334
+ /**
9313
9335
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
9314
9336
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
9315
9337
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -13118,6 +13140,27 @@ var LinkedDeviceSchema = object({
13118
13140
  features: array(string()),
13119
13141
  producesTrackedEvents: boolean().optional()
13120
13142
  });
13143
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
13144
+ * The batch answer needs the tag; the single-device answer already has it
13145
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
13146
+ var LinkedDevicesForDeviceSchema = object({
13147
+ deviceId: number(),
13148
+ mode: LinkedDevicesModeSchema,
13149
+ devices: array(LinkedDeviceSchema)
13150
+ });
13151
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
13152
+ * `getAllBindings` all answer in. Declared once: three copies of the same
13153
+ * object literal is exactly how the three drift apart. */
13154
+ var DeviceBindingsForDeviceSchema = object({
13155
+ deviceId: number(),
13156
+ entries: array(object({
13157
+ capName: string(),
13158
+ kind: _enum(["native", "wrapped"]),
13159
+ providerAddonId: string(),
13160
+ providerNodeId: string(),
13161
+ nativeAddonId: string()
13162
+ }))
13163
+ });
13121
13164
  var SavedDeviceRowSchema = object({
13122
13165
  /** Numeric id reserved at allocateDeviceId time. */
13123
13166
  id: number(),
@@ -13343,11 +13386,25 @@ method(object({
13343
13386
  projection: _enum(["full", "slim"]).optional(),
13344
13387
  /** Return only camera devices. Filtering server-side instead of
13345
13388
  * shipping 293 rows to find 12. */
13346
- isCamera: boolean().optional()
13389
+ isCamera: boolean().optional(),
13390
+ /**
13391
+ * Return only these device ids. For the caller that already KNOWS the
13392
+ * handful it wants and needs a field the id-bearing answer does not
13393
+ * carry — the viewer's linked-devices panel joins `type` and `online`
13394
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
13395
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
13396
+ * refetches on the reconcile interval, on a phone.
13397
+ *
13398
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
13399
+ * keys rather than rejecting them (verified against the live hub
13400
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
13401
+ * it answers today and the caller filters as it already does.
13402
+ */
13403
+ deviceIds: array(number()).optional()
13347
13404
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
13348
13405
  mode: LinkedDevicesModeSchema,
13349
13406
  devices: array(LinkedDeviceSchema)
13350
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
13407
+ })), 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({
13351
13408
  deviceId: number(),
13352
13409
  values: record(string(), unknown())
13353
13410
  }), object({ success: literal(true) }), {
@@ -13374,25 +13431,7 @@ method(object({
13374
13431
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
13375
13432
  kind: "mutation",
13376
13433
  auth: "admin"
13377
- }), method(object({ deviceId: number() }), object({
13378
- deviceId: number(),
13379
- entries: array(object({
13380
- capName: string(),
13381
- kind: _enum(["native", "wrapped"]),
13382
- providerAddonId: string(),
13383
- providerNodeId: string(),
13384
- nativeAddonId: string()
13385
- }))
13386
- })), method(object({}), array(object({
13387
- deviceId: number(),
13388
- entries: array(object({
13389
- capName: string(),
13390
- kind: _enum(["native", "wrapped"]),
13391
- providerAddonId: string(),
13392
- providerNodeId: string(),
13393
- nativeAddonId: string()
13394
- }))
13395
- }))), method(object({
13434
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
13396
13435
  deviceId: number(),
13397
13436
  capName: string(),
13398
13437
  wrapperAddonId: string(),
@@ -15802,12 +15841,15 @@ var NcOccupancyConditionSchema = object({
15802
15841
  * there is no second switch that can disagree with the first and every rule
15803
15842
  * authored before the decision migrates for free (`audioModeOf`):
15804
15843
  *
15805
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
15806
- * classifier labels with one of them. No window, no percentage:
15807
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
15808
- * `throttle` cooldown is the only brake. The per-label confidence floor is
15809
- * the analyzer's (`classificationMinScore`, per device) a label only
15810
- * reaches this condition if the classifier was already confident enough.
15844
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
15845
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
15846
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
15847
+ * frames is the wrong question for a classifier that labels 1–3 frames
15848
+ * per episode. The count window is the brake that drops a single-frame
15849
+ * false positive; the rule's own `throttle` cooldown is the other. The
15850
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
15851
+ * per device) — a label only reaches this condition if the classifier was
15852
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
15811
15853
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
15812
15854
  * the condition: at least `hitPercent`% of the samples over
15813
15855
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -15834,14 +15876,22 @@ var NcOccupancyConditionSchema = object({
15834
15876
  * an operator who typed `dog` mean the same thing.
15835
15877
  */
15836
15878
  var NcAudioConditionSchema = object({
15837
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
15879
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
15838
15880
  labels: array(string().min(1)).min(1).optional(),
15839
15881
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
15840
15882
  dbThreshold: number().min(-96).max(0).optional(),
15841
15883
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
15842
15884
  hitPercent: number().int().min(1).max(100).default(60),
15843
15885
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
15844
- samplingSeconds: number().int().min(1).max(300).default(10)
15886
+ samplingSeconds: number().int().min(1).max(300).default(10),
15887
+ /**
15888
+ * LABEL MODE: how many labelled frames must land inside
15889
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
15890
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
15891
+ */
15892
+ confirmHits: number().int().min(1).max(20).optional(),
15893
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
15894
+ confirmWindowSec: number().int().min(1).max(60).optional()
15845
15895
  });
15846
15896
  /**
15847
15897
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -18215,6 +18265,46 @@ var RecentTracksPageSchema = object({
18215
18265
  /** Cursor for the next page, or null when this page is the last. */
18216
18266
  nextCursor: string().nullable()
18217
18267
  });
18268
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
18269
+ var LIST_GROUPS_MAX_LIMIT = 100;
18270
+ var AnalyticsGroupRecordSchema = object({
18271
+ id: string(),
18272
+ deviceId: number().int(),
18273
+ openedAt: number().int(),
18274
+ closedAt: number().int(),
18275
+ timestamp: number().int(),
18276
+ memberCount: number().int(),
18277
+ memberTrackIds: array(string()).readonly(),
18278
+ className: string(),
18279
+ classes: array(string()).readonly(),
18280
+ /** Relative event-media path, or null when the group has no picture yet. */
18281
+ mediaUrl: string().nullable(),
18282
+ singleton: boolean()
18283
+ });
18284
+ var AnalyticsGroupMemberSchema = object({
18285
+ trackId: string(),
18286
+ deviceId: number().int(),
18287
+ className: string(),
18288
+ firstSeen: number().int(),
18289
+ lastSeen: number().int(),
18290
+ mediaUrl: string().nullable()
18291
+ });
18292
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
18293
+ var ListGroupsQueryInput = object({
18294
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
18295
+ deviceIds: array(number()),
18296
+ /** Window lower bound on `closedAt` (inclusive). */
18297
+ since: number().optional(),
18298
+ /** Window upper bound on `openedAt` (inclusive). */
18299
+ until: number().optional(),
18300
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
18301
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
18302
+ cursor: string().optional()
18303
+ });
18304
+ var ListGroupsPageSchema = object({
18305
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
18306
+ nextCursor: string().nullable()
18307
+ });
18218
18308
  var KeyEventQueryInput = object({
18219
18309
  deviceId: number(),
18220
18310
  /** Window lower bound (track firstSeen ≥ since). */
@@ -18290,7 +18380,9 @@ var TrackCascadeCountsSchema = object({
18290
18380
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
18291
18381
  plates: number().int(),
18292
18382
  /** Per-track CLIP search vectors removed (best-effort). */
18293
- embeddings: number().int()
18383
+ embeddings: number().int(),
18384
+ /** Group membership + group rows removed with their last member (best-effort). */
18385
+ groups: number().int()
18294
18386
  });
18295
18387
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
18296
18388
  var DiskReconcileCountsSchema = object({
@@ -18436,7 +18528,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18436
18528
  * stationary registry). Default false: the timeline lists passages,
18437
18529
  * not parking records (operator decision, 2026-08-15). */
18438
18530
  includeStationary: boolean().optional()
18439
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
18531
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
18532
+ deviceId: number(),
18533
+ groupId: string().min(1)
18534
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
18440
18535
  kind: "mutation",
18441
18536
  auth: "admin"
18442
18537
  }), 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({
@@ -18746,7 +18841,8 @@ var PipelineModelOptionSchema = object({
18746
18841
  sizeMB: number()
18747
18842
  })),
18748
18843
  group: ModelVariantGroupSchema.optional(),
18749
- legacy: boolean().optional()
18844
+ legacy: boolean().optional(),
18845
+ provider: ModelProviderIdSchema.optional()
18750
18846
  });
18751
18847
  var ConfigFieldBridge = custom();
18752
18848
  var PipelineAddonSchemaSchema = object({
@@ -26851,7 +26947,12 @@ var PlateInfoSchema = object({
26851
26947
  plateBbox: BoundingBoxSchema.optional(),
26852
26948
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
26853
26949
  keyFrameMediaKey: string().optional(),
26854
- base64: string().optional()
26950
+ base64: string().optional(),
26951
+ /**
26952
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
26953
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
26954
+ */
26955
+ cropUrl: string().optional()
26855
26956
  });
26856
26957
  var MediaFileLiteSchema = object({
26857
26958
  key: string(),
@@ -32375,6 +32476,12 @@ Object.freeze({
32375
32476
  addonId: null,
32376
32477
  access: "view"
32377
32478
  },
32479
+ "deviceManager.getBindingsBatch": {
32480
+ capName: "device-manager",
32481
+ capScope: "system",
32482
+ addonId: null,
32483
+ access: "view"
32484
+ },
32378
32485
  "deviceManager.getChildren": {
32379
32486
  capName: "device-manager",
32380
32487
  capScope: "system",
@@ -32435,6 +32542,12 @@ Object.freeze({
32435
32542
  addonId: null,
32436
32543
  access: "view"
32437
32544
  },
32545
+ "deviceManager.getLinkedDevicesBatch": {
32546
+ capName: "device-manager",
32547
+ capScope: "system",
32548
+ addonId: null,
32549
+ access: "view"
32550
+ },
32438
32551
  "deviceManager.getRoleDisplayDefaults": {
32439
32552
  capName: "device-manager",
32440
32553
  capScope: "system",
@@ -34205,6 +34318,12 @@ Object.freeze({
34205
34318
  addonId: null,
34206
34319
  access: "view"
34207
34320
  },
34321
+ "pipelineAnalytics.getGroup": {
34322
+ capName: "pipeline-analytics",
34323
+ capScope: "device",
34324
+ addonId: null,
34325
+ access: "view"
34326
+ },
34208
34327
  "pipelineAnalytics.getKeyEvents": {
34209
34328
  capName: "pipeline-analytics",
34210
34329
  capScope: "device",
@@ -34289,6 +34408,12 @@ Object.freeze({
34289
34408
  addonId: null,
34290
34409
  access: "view"
34291
34410
  },
34411
+ "pipelineAnalytics.listGroups": {
34412
+ capName: "pipeline-analytics",
34413
+ capScope: "device",
34414
+ addonId: null,
34415
+ access: "view"
34416
+ },
34292
34417
  "pipelineAnalytics.listOpsLog": {
34293
34418
  capName: "pipeline-analytics",
34294
34419
  capScope: "device",
@@ -37070,6 +37195,11 @@ Object.freeze({
37070
37195
  form: "single",
37071
37196
  optional: false
37072
37197
  }],
37198
+ "deviceManager.getBindingsBatch": [{
37199
+ name: "deviceIds",
37200
+ form: "array",
37201
+ optional: false
37202
+ }],
37073
37203
  "deviceManager.getChildren": [{
37074
37204
  name: "parentDeviceId",
37075
37205
  form: "single",
@@ -37115,6 +37245,11 @@ Object.freeze({
37115
37245
  form: "single",
37116
37246
  optional: false
37117
37247
  }],
37248
+ "deviceManager.getLinkedDevicesBatch": [{
37249
+ name: "deviceIds",
37250
+ form: "array",
37251
+ optional: false
37252
+ }],
37118
37253
  "deviceManager.getSettingsSchema": [{
37119
37254
  name: "deviceId",
37120
37255
  form: "single",
@@ -37135,6 +37270,11 @@ Object.freeze({
37135
37270
  form: "single",
37136
37271
  optional: false
37137
37272
  }],
37273
+ "deviceManager.listAll": [{
37274
+ name: "deviceIds",
37275
+ form: "array",
37276
+ optional: true
37277
+ }],
37138
37278
  "deviceManager.loadConfig": [{
37139
37279
  name: "deviceId",
37140
37280
  form: "single",
@@ -37708,6 +37848,11 @@ Object.freeze({
37708
37848
  form: "single",
37709
37849
  optional: false
37710
37850
  }],
37851
+ "pipelineAnalytics.getGroup": [{
37852
+ name: "deviceId",
37853
+ form: "single",
37854
+ optional: false
37855
+ }],
37711
37856
  "pipelineAnalytics.getKeyEvents": [{
37712
37857
  name: "deviceId",
37713
37858
  form: "single",
@@ -37763,6 +37908,11 @@ Object.freeze({
37763
37908
  form: "array",
37764
37909
  optional: false
37765
37910
  }],
37911
+ "pipelineAnalytics.listGroups": [{
37912
+ name: "deviceIds",
37913
+ form: "array",
37914
+ optional: false
37915
+ }],
37766
37916
  "pipelineAnalytics.listOpsLog": [{
37767
37917
  name: "deviceId",
37768
37918
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-petkit",
3
- "version": "0.2.27",
3
+ "version": "0.2.28",
4
4
  "description": "PetKit smart-feeder device-provider addon for CamStack — wraps the @apocaliss92/nodepetkit PetKit cloud client",
5
5
  "keywords": [
6
6
  "camstack",