@camstack/addon-provider-homematic 1.2.28 → 1.2.29

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
@@ -5810,6 +5810,13 @@ var BaseAddon = class {
5810
5810
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5811
5811
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5812
5812
  _registeredCapNames = [];
5813
+ /**
5814
+ * True only after `readAddonStore` actually answered. Constructor
5815
+ * defaults look like stored config when the store is down — a forked
5816
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5817
+ * mode, 2026-08-25) is not "the operator chose this".
5818
+ */
5819
+ settingsStoreReady = false;
5813
5820
  /** Default config values. Provided via constructor. */
5814
5821
  defaults;
5815
5822
  constructor(defaults) {
@@ -6210,7 +6217,9 @@ var BaseAddon = class {
6210
6217
  ];
6211
6218
  let lastErr;
6212
6219
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6213
- return await settings.readAddonStore() ?? {};
6220
+ const stored = await settings.readAddonStore() ?? {};
6221
+ this.settingsStoreReady = true;
6222
+ return stored;
6214
6223
  } catch (err) {
6215
6224
  lastErr = err;
6216
6225
  const msg = err instanceof Error ? err.message : String(err);
@@ -6218,6 +6227,7 @@ var BaseAddon = class {
6218
6227
  if (attempt === delaysMs.length) break;
6219
6228
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6220
6229
  }
6230
+ this.settingsStoreReady = false;
6221
6231
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6222
6232
  return {};
6223
6233
  }
@@ -8121,6 +8131,12 @@ var ModelVariantGroupSchema = object({
8121
8131
  */
8122
8132
  resolution: number().int().positive().optional()
8123
8133
  });
8134
+ var ModelProviderIdSchema = _enum([
8135
+ "camstack",
8136
+ "frigate",
8137
+ "scrypted",
8138
+ "custom"
8139
+ ]);
8124
8140
  var ModelCatalogEntrySchema = object({
8125
8141
  id: string(),
8126
8142
  name: string(),
@@ -8218,6 +8234,12 @@ var ModelCatalogEntrySchema = object({
8218
8234
  */
8219
8235
  group: ModelVariantGroupSchema.optional(),
8220
8236
  /**
8237
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8238
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8239
+ * persisted before this field existed (`inferModelProvider` fills those).
8240
+ */
8241
+ provider: ModelProviderIdSchema.optional(),
8242
+ /**
8221
8243
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8222
8244
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8223
8245
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12075,6 +12097,27 @@ var LinkedDeviceSchema = object({
12075
12097
  features: array(string()),
12076
12098
  producesTrackedEvents: boolean().optional()
12077
12099
  });
12100
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12101
+ * The batch answer needs the tag; the single-device answer already has it
12102
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12103
+ var LinkedDevicesForDeviceSchema = object({
12104
+ deviceId: number(),
12105
+ mode: LinkedDevicesModeSchema,
12106
+ devices: array(LinkedDeviceSchema)
12107
+ });
12108
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12109
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12110
+ * object literal is exactly how the three drift apart. */
12111
+ var DeviceBindingsForDeviceSchema = object({
12112
+ deviceId: number(),
12113
+ entries: array(object({
12114
+ capName: string(),
12115
+ kind: _enum(["native", "wrapped"]),
12116
+ providerAddonId: string(),
12117
+ providerNodeId: string(),
12118
+ nativeAddonId: string()
12119
+ }))
12120
+ });
12078
12121
  var SavedDeviceRowSchema = object({
12079
12122
  /** Numeric id reserved at allocateDeviceId time. */
12080
12123
  id: number(),
@@ -12300,11 +12343,25 @@ method(object({
12300
12343
  projection: _enum(["full", "slim"]).optional(),
12301
12344
  /** Return only camera devices. Filtering server-side instead of
12302
12345
  * shipping 293 rows to find 12. */
12303
- isCamera: boolean().optional()
12346
+ isCamera: boolean().optional(),
12347
+ /**
12348
+ * Return only these device ids. For the caller that already KNOWS the
12349
+ * handful it wants and needs a field the id-bearing answer does not
12350
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12351
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12352
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12353
+ * refetches on the reconcile interval, on a phone.
12354
+ *
12355
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12356
+ * keys rather than rejecting them (verified against the live hub
12357
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12358
+ * it answers today and the caller filters as it already does.
12359
+ */
12360
+ deviceIds: array(number()).optional()
12304
12361
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12305
12362
  mode: LinkedDevicesModeSchema,
12306
12363
  devices: array(LinkedDeviceSchema)
12307
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12364
+ })), 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({
12308
12365
  deviceId: number(),
12309
12366
  values: record(string(), unknown())
12310
12367
  }), object({ success: literal(true) }), {
@@ -12331,25 +12388,7 @@ method(object({
12331
12388
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12332
12389
  kind: "mutation",
12333
12390
  auth: "admin"
12334
- }), method(object({ deviceId: number() }), object({
12335
- deviceId: number(),
12336
- entries: array(object({
12337
- capName: string(),
12338
- kind: _enum(["native", "wrapped"]),
12339
- providerAddonId: string(),
12340
- providerNodeId: string(),
12341
- nativeAddonId: string()
12342
- }))
12343
- })), method(object({}), array(object({
12344
- deviceId: number(),
12345
- entries: array(object({
12346
- capName: string(),
12347
- kind: _enum(["native", "wrapped"]),
12348
- providerAddonId: string(),
12349
- providerNodeId: string(),
12350
- nativeAddonId: string()
12351
- }))
12352
- }))), method(object({
12391
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12353
12392
  deviceId: number(),
12354
12393
  capName: string(),
12355
12394
  wrapperAddonId: string(),
@@ -14759,12 +14798,15 @@ var NcOccupancyConditionSchema = object({
14759
14798
  * there is no second switch that can disagree with the first and every rule
14760
14799
  * authored before the decision migrates for free (`audioModeOf`):
14761
14800
  *
14762
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14763
- * classifier labels with one of them. No window, no percentage:
14764
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14765
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14766
- * the analyzer's (`classificationMinScore`, per device) — a label only
14767
- * reaches this condition if the classifier was already confident enough.
14801
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14802
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14803
+ * `hitPercent` and `samplingSeconds` are still ignored — a percentage of
14804
+ * frames is the wrong question for a classifier that labels 1–3 frames
14805
+ * per episode. The count window is the brake that drops a single-frame
14806
+ * false positive; the rule's own `throttle` cooldown is the other. The
14807
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14808
+ * per device) — a label only reaches this condition if the classifier was
14809
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14768
14810
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14769
14811
  * the condition: at least `hitPercent`% of the samples over
14770
14812
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14791,14 +14833,22 @@ var NcOccupancyConditionSchema = object({
14791
14833
  * an operator who typed `dog` mean the same thing.
14792
14834
  */
14793
14835
  var NcAudioConditionSchema = object({
14794
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14836
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14795
14837
  labels: array(string().min(1)).min(1).optional(),
14796
14838
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14797
14839
  dbThreshold: number().min(-96).max(0).optional(),
14798
14840
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14799
14841
  hitPercent: number().int().min(1).max(100).default(60),
14800
14842
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14801
- samplingSeconds: number().int().min(1).max(300).default(10)
14843
+ samplingSeconds: number().int().min(1).max(300).default(10),
14844
+ /**
14845
+ * LABEL MODE: how many labelled frames must land inside
14846
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14847
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14848
+ */
14849
+ confirmHits: number().int().min(1).max(20).optional(),
14850
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14851
+ confirmWindowSec: number().int().min(1).max(60).optional()
14802
14852
  });
14803
14853
  /**
14804
14854
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17172,6 +17222,46 @@ var RecentTracksPageSchema = object({
17172
17222
  /** Cursor for the next page, or null when this page is the last. */
17173
17223
  nextCursor: string().nullable()
17174
17224
  });
17225
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17226
+ var LIST_GROUPS_MAX_LIMIT = 100;
17227
+ var AnalyticsGroupRecordSchema = object({
17228
+ id: string(),
17229
+ deviceId: number().int(),
17230
+ openedAt: number().int(),
17231
+ closedAt: number().int(),
17232
+ timestamp: number().int(),
17233
+ memberCount: number().int(),
17234
+ memberTrackIds: array(string()).readonly(),
17235
+ className: string(),
17236
+ classes: array(string()).readonly(),
17237
+ /** Relative event-media path, or null when the group has no picture yet. */
17238
+ mediaUrl: string().nullable(),
17239
+ singleton: boolean()
17240
+ });
17241
+ var AnalyticsGroupMemberSchema = object({
17242
+ trackId: string(),
17243
+ deviceId: number().int(),
17244
+ className: string(),
17245
+ firstSeen: number().int(),
17246
+ lastSeen: number().int(),
17247
+ mediaUrl: string().nullable()
17248
+ });
17249
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17250
+ var ListGroupsQueryInput = object({
17251
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17252
+ deviceIds: array(number()),
17253
+ /** Window lower bound on `closedAt` (inclusive). */
17254
+ since: number().optional(),
17255
+ /** Window upper bound on `openedAt` (inclusive). */
17256
+ until: number().optional(),
17257
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17258
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17259
+ cursor: string().optional()
17260
+ });
17261
+ var ListGroupsPageSchema = object({
17262
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17263
+ nextCursor: string().nullable()
17264
+ });
17175
17265
  var KeyEventQueryInput = object({
17176
17266
  deviceId: number(),
17177
17267
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17247,7 +17337,9 @@ var TrackCascadeCountsSchema = object({
17247
17337
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17248
17338
  plates: number().int(),
17249
17339
  /** Per-track CLIP search vectors removed (best-effort). */
17250
- embeddings: number().int()
17340
+ embeddings: number().int(),
17341
+ /** Group membership + group rows removed with their last member (best-effort). */
17342
+ groups: number().int()
17251
17343
  });
17252
17344
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17253
17345
  var DiskReconcileCountsSchema = object({
@@ -17393,7 +17485,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17393
17485
  * stationary registry). Default false: the timeline lists passages,
17394
17486
  * not parking records (operator decision, 2026-08-15). */
17395
17487
  includeStationary: boolean().optional()
17396
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17488
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17489
+ deviceId: number(),
17490
+ groupId: string().min(1)
17491
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17397
17492
  kind: "mutation",
17398
17493
  auth: "admin"
17399
17494
  }), 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({
@@ -17703,7 +17798,8 @@ var PipelineModelOptionSchema = object({
17703
17798
  sizeMB: number()
17704
17799
  })),
17705
17800
  group: ModelVariantGroupSchema.optional(),
17706
- legacy: boolean().optional()
17801
+ legacy: boolean().optional(),
17802
+ provider: ModelProviderIdSchema.optional()
17707
17803
  });
17708
17804
  var ConfigFieldBridge = custom();
17709
17805
  var PipelineAddonSchemaSchema = object({
@@ -25800,7 +25896,12 @@ var PlateInfoSchema = object({
25800
25896
  plateBbox: BoundingBoxSchema.optional(),
25801
25897
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25802
25898
  keyFrameMediaKey: string().optional(),
25803
- base64: string().optional()
25899
+ base64: string().optional(),
25900
+ /**
25901
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25902
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25903
+ */
25904
+ cropUrl: string().optional()
25804
25905
  });
25805
25906
  var MediaFileLiteSchema = object({
25806
25907
  key: string(),
@@ -31324,6 +31425,12 @@ Object.freeze({
31324
31425
  addonId: null,
31325
31426
  access: "view"
31326
31427
  },
31428
+ "deviceManager.getBindingsBatch": {
31429
+ capName: "device-manager",
31430
+ capScope: "system",
31431
+ addonId: null,
31432
+ access: "view"
31433
+ },
31327
31434
  "deviceManager.getChildren": {
31328
31435
  capName: "device-manager",
31329
31436
  capScope: "system",
@@ -31384,6 +31491,12 @@ Object.freeze({
31384
31491
  addonId: null,
31385
31492
  access: "view"
31386
31493
  },
31494
+ "deviceManager.getLinkedDevicesBatch": {
31495
+ capName: "device-manager",
31496
+ capScope: "system",
31497
+ addonId: null,
31498
+ access: "view"
31499
+ },
31387
31500
  "deviceManager.getRoleDisplayDefaults": {
31388
31501
  capName: "device-manager",
31389
31502
  capScope: "system",
@@ -33154,6 +33267,12 @@ Object.freeze({
33154
33267
  addonId: null,
33155
33268
  access: "view"
33156
33269
  },
33270
+ "pipelineAnalytics.getGroup": {
33271
+ capName: "pipeline-analytics",
33272
+ capScope: "device",
33273
+ addonId: null,
33274
+ access: "view"
33275
+ },
33157
33276
  "pipelineAnalytics.getKeyEvents": {
33158
33277
  capName: "pipeline-analytics",
33159
33278
  capScope: "device",
@@ -33238,6 +33357,12 @@ Object.freeze({
33238
33357
  addonId: null,
33239
33358
  access: "view"
33240
33359
  },
33360
+ "pipelineAnalytics.listGroups": {
33361
+ capName: "pipeline-analytics",
33362
+ capScope: "device",
33363
+ addonId: null,
33364
+ access: "view"
33365
+ },
33241
33366
  "pipelineAnalytics.listOpsLog": {
33242
33367
  capName: "pipeline-analytics",
33243
33368
  capScope: "device",
@@ -36019,6 +36144,11 @@ Object.freeze({
36019
36144
  form: "single",
36020
36145
  optional: false
36021
36146
  }],
36147
+ "deviceManager.getBindingsBatch": [{
36148
+ name: "deviceIds",
36149
+ form: "array",
36150
+ optional: false
36151
+ }],
36022
36152
  "deviceManager.getChildren": [{
36023
36153
  name: "parentDeviceId",
36024
36154
  form: "single",
@@ -36064,6 +36194,11 @@ Object.freeze({
36064
36194
  form: "single",
36065
36195
  optional: false
36066
36196
  }],
36197
+ "deviceManager.getLinkedDevicesBatch": [{
36198
+ name: "deviceIds",
36199
+ form: "array",
36200
+ optional: false
36201
+ }],
36067
36202
  "deviceManager.getSettingsSchema": [{
36068
36203
  name: "deviceId",
36069
36204
  form: "single",
@@ -36084,6 +36219,11 @@ Object.freeze({
36084
36219
  form: "single",
36085
36220
  optional: false
36086
36221
  }],
36222
+ "deviceManager.listAll": [{
36223
+ name: "deviceIds",
36224
+ form: "array",
36225
+ optional: true
36226
+ }],
36087
36227
  "deviceManager.loadConfig": [{
36088
36228
  name: "deviceId",
36089
36229
  form: "single",
@@ -36657,6 +36797,11 @@ Object.freeze({
36657
36797
  form: "single",
36658
36798
  optional: false
36659
36799
  }],
36800
+ "pipelineAnalytics.getGroup": [{
36801
+ name: "deviceId",
36802
+ form: "single",
36803
+ optional: false
36804
+ }],
36660
36805
  "pipelineAnalytics.getKeyEvents": [{
36661
36806
  name: "deviceId",
36662
36807
  form: "single",
@@ -36712,6 +36857,11 @@ Object.freeze({
36712
36857
  form: "array",
36713
36858
  optional: false
36714
36859
  }],
36860
+ "pipelineAnalytics.listGroups": [{
36861
+ name: "deviceIds",
36862
+ form: "array",
36863
+ optional: false
36864
+ }],
36715
36865
  "pipelineAnalytics.listOpsLog": [{
36716
36866
  name: "deviceId",
36717
36867
  form: "single",
package/dist/addon.mjs CHANGED
@@ -5811,6 +5811,13 @@ var BaseAddon = class {
5811
5811
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5812
5812
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5813
5813
  _registeredCapNames = [];
5814
+ /**
5815
+ * True only after `readAddonStore` actually answered. Constructor
5816
+ * defaults look like stored config when the store is down — a forked
5817
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5818
+ * mode, 2026-08-25) is not "the operator chose this".
5819
+ */
5820
+ settingsStoreReady = false;
5814
5821
  /** Default config values. Provided via constructor. */
5815
5822
  defaults;
5816
5823
  constructor(defaults) {
@@ -6211,7 +6218,9 @@ var BaseAddon = class {
6211
6218
  ];
6212
6219
  let lastErr;
6213
6220
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6214
- return await settings.readAddonStore() ?? {};
6221
+ const stored = await settings.readAddonStore() ?? {};
6222
+ this.settingsStoreReady = true;
6223
+ return stored;
6215
6224
  } catch (err) {
6216
6225
  lastErr = err;
6217
6226
  const msg = err instanceof Error ? err.message : String(err);
@@ -6219,6 +6228,7 @@ var BaseAddon = class {
6219
6228
  if (attempt === delaysMs.length) break;
6220
6229
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6221
6230
  }
6231
+ this.settingsStoreReady = false;
6222
6232
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6223
6233
  return {};
6224
6234
  }
@@ -8122,6 +8132,12 @@ var ModelVariantGroupSchema = object({
8122
8132
  */
8123
8133
  resolution: number().int().positive().optional()
8124
8134
  });
8135
+ var ModelProviderIdSchema = _enum([
8136
+ "camstack",
8137
+ "frigate",
8138
+ "scrypted",
8139
+ "custom"
8140
+ ]);
8125
8141
  var ModelCatalogEntrySchema = object({
8126
8142
  id: string(),
8127
8143
  name: string(),
@@ -8219,6 +8235,12 @@ var ModelCatalogEntrySchema = object({
8219
8235
  */
8220
8236
  group: ModelVariantGroupSchema.optional(),
8221
8237
  /**
8238
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8239
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8240
+ * persisted before this field existed (`inferModelProvider` fills those).
8241
+ */
8242
+ provider: ModelProviderIdSchema.optional(),
8243
+ /**
8222
8244
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8223
8245
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8224
8246
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12076,6 +12098,27 @@ var LinkedDeviceSchema = object({
12076
12098
  features: array(string()),
12077
12099
  producesTrackedEvents: boolean().optional()
12078
12100
  });
12101
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12102
+ * The batch answer needs the tag; the single-device answer already has it
12103
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12104
+ var LinkedDevicesForDeviceSchema = object({
12105
+ deviceId: number(),
12106
+ mode: LinkedDevicesModeSchema,
12107
+ devices: array(LinkedDeviceSchema)
12108
+ });
12109
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12110
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12111
+ * object literal is exactly how the three drift apart. */
12112
+ var DeviceBindingsForDeviceSchema = object({
12113
+ deviceId: number(),
12114
+ entries: array(object({
12115
+ capName: string(),
12116
+ kind: _enum(["native", "wrapped"]),
12117
+ providerAddonId: string(),
12118
+ providerNodeId: string(),
12119
+ nativeAddonId: string()
12120
+ }))
12121
+ });
12079
12122
  var SavedDeviceRowSchema = object({
12080
12123
  /** Numeric id reserved at allocateDeviceId time. */
12081
12124
  id: number(),
@@ -12301,11 +12344,25 @@ method(object({
12301
12344
  projection: _enum(["full", "slim"]).optional(),
12302
12345
  /** Return only camera devices. Filtering server-side instead of
12303
12346
  * shipping 293 rows to find 12. */
12304
- isCamera: boolean().optional()
12347
+ isCamera: boolean().optional(),
12348
+ /**
12349
+ * Return only these device ids. For the caller that already KNOWS the
12350
+ * handful it wants and needs a field the id-bearing answer does not
12351
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12352
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12353
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12354
+ * refetches on the reconcile interval, on a phone.
12355
+ *
12356
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12357
+ * keys rather than rejecting them (verified against the live hub
12358
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12359
+ * it answers today and the caller filters as it already does.
12360
+ */
12361
+ deviceIds: array(number()).optional()
12305
12362
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12306
12363
  mode: LinkedDevicesModeSchema,
12307
12364
  devices: array(LinkedDeviceSchema)
12308
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12365
+ })), 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({
12309
12366
  deviceId: number(),
12310
12367
  values: record(string(), unknown())
12311
12368
  }), object({ success: literal(true) }), {
@@ -12332,25 +12389,7 @@ method(object({
12332
12389
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12333
12390
  kind: "mutation",
12334
12391
  auth: "admin"
12335
- }), method(object({ deviceId: number() }), object({
12336
- deviceId: number(),
12337
- entries: array(object({
12338
- capName: string(),
12339
- kind: _enum(["native", "wrapped"]),
12340
- providerAddonId: string(),
12341
- providerNodeId: string(),
12342
- nativeAddonId: string()
12343
- }))
12344
- })), method(object({}), array(object({
12345
- deviceId: number(),
12346
- entries: array(object({
12347
- capName: string(),
12348
- kind: _enum(["native", "wrapped"]),
12349
- providerAddonId: string(),
12350
- providerNodeId: string(),
12351
- nativeAddonId: string()
12352
- }))
12353
- }))), method(object({
12392
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12354
12393
  deviceId: number(),
12355
12394
  capName: string(),
12356
12395
  wrapperAddonId: string(),
@@ -14760,12 +14799,15 @@ var NcOccupancyConditionSchema = object({
14760
14799
  * there is no second switch that can disagree with the first and every rule
14761
14800
  * authored before the decision migrates for free (`audioModeOf`):
14762
14801
  *
14763
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14764
- * classifier labels with one of them. No window, no percentage:
14765
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14766
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14767
- * the analyzer's (`classificationMinScore`, per device) — a label only
14768
- * reaches this condition if the classifier was already confident enough.
14802
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14803
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14804
+ * `hitPercent` and `samplingSeconds` are still ignored — a percentage of
14805
+ * frames is the wrong question for a classifier that labels 1–3 frames
14806
+ * per episode. The count window is the brake that drops a single-frame
14807
+ * false positive; the rule's own `throttle` cooldown is the other. The
14808
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14809
+ * per device) — a label only reaches this condition if the classifier was
14810
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14769
14811
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14770
14812
  * the condition: at least `hitPercent`% of the samples over
14771
14813
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14792,14 +14834,22 @@ var NcOccupancyConditionSchema = object({
14792
14834
  * an operator who typed `dog` mean the same thing.
14793
14835
  */
14794
14836
  var NcAudioConditionSchema = object({
14795
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14837
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14796
14838
  labels: array(string().min(1)).min(1).optional(),
14797
14839
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14798
14840
  dbThreshold: number().min(-96).max(0).optional(),
14799
14841
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14800
14842
  hitPercent: number().int().min(1).max(100).default(60),
14801
14843
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14802
- samplingSeconds: number().int().min(1).max(300).default(10)
14844
+ samplingSeconds: number().int().min(1).max(300).default(10),
14845
+ /**
14846
+ * LABEL MODE: how many labelled frames must land inside
14847
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14848
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14849
+ */
14850
+ confirmHits: number().int().min(1).max(20).optional(),
14851
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14852
+ confirmWindowSec: number().int().min(1).max(60).optional()
14803
14853
  });
14804
14854
  /**
14805
14855
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17173,6 +17223,46 @@ var RecentTracksPageSchema = object({
17173
17223
  /** Cursor for the next page, or null when this page is the last. */
17174
17224
  nextCursor: string().nullable()
17175
17225
  });
17226
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17227
+ var LIST_GROUPS_MAX_LIMIT = 100;
17228
+ var AnalyticsGroupRecordSchema = object({
17229
+ id: string(),
17230
+ deviceId: number().int(),
17231
+ openedAt: number().int(),
17232
+ closedAt: number().int(),
17233
+ timestamp: number().int(),
17234
+ memberCount: number().int(),
17235
+ memberTrackIds: array(string()).readonly(),
17236
+ className: string(),
17237
+ classes: array(string()).readonly(),
17238
+ /** Relative event-media path, or null when the group has no picture yet. */
17239
+ mediaUrl: string().nullable(),
17240
+ singleton: boolean()
17241
+ });
17242
+ var AnalyticsGroupMemberSchema = object({
17243
+ trackId: string(),
17244
+ deviceId: number().int(),
17245
+ className: string(),
17246
+ firstSeen: number().int(),
17247
+ lastSeen: number().int(),
17248
+ mediaUrl: string().nullable()
17249
+ });
17250
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17251
+ var ListGroupsQueryInput = object({
17252
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17253
+ deviceIds: array(number()),
17254
+ /** Window lower bound on `closedAt` (inclusive). */
17255
+ since: number().optional(),
17256
+ /** Window upper bound on `openedAt` (inclusive). */
17257
+ until: number().optional(),
17258
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17259
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17260
+ cursor: string().optional()
17261
+ });
17262
+ var ListGroupsPageSchema = object({
17263
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17264
+ nextCursor: string().nullable()
17265
+ });
17176
17266
  var KeyEventQueryInput = object({
17177
17267
  deviceId: number(),
17178
17268
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17248,7 +17338,9 @@ var TrackCascadeCountsSchema = object({
17248
17338
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17249
17339
  plates: number().int(),
17250
17340
  /** Per-track CLIP search vectors removed (best-effort). */
17251
- embeddings: number().int()
17341
+ embeddings: number().int(),
17342
+ /** Group membership + group rows removed with their last member (best-effort). */
17343
+ groups: number().int()
17252
17344
  });
17253
17345
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17254
17346
  var DiskReconcileCountsSchema = object({
@@ -17394,7 +17486,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17394
17486
  * stationary registry). Default false: the timeline lists passages,
17395
17487
  * not parking records (operator decision, 2026-08-15). */
17396
17488
  includeStationary: boolean().optional()
17397
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17489
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17490
+ deviceId: number(),
17491
+ groupId: string().min(1)
17492
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17398
17493
  kind: "mutation",
17399
17494
  auth: "admin"
17400
17495
  }), 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({
@@ -17704,7 +17799,8 @@ var PipelineModelOptionSchema = object({
17704
17799
  sizeMB: number()
17705
17800
  })),
17706
17801
  group: ModelVariantGroupSchema.optional(),
17707
- legacy: boolean().optional()
17802
+ legacy: boolean().optional(),
17803
+ provider: ModelProviderIdSchema.optional()
17708
17804
  });
17709
17805
  var ConfigFieldBridge = custom();
17710
17806
  var PipelineAddonSchemaSchema = object({
@@ -25801,7 +25897,12 @@ var PlateInfoSchema = object({
25801
25897
  plateBbox: BoundingBoxSchema.optional(),
25802
25898
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25803
25899
  keyFrameMediaKey: string().optional(),
25804
- base64: string().optional()
25900
+ base64: string().optional(),
25901
+ /**
25902
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25903
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25904
+ */
25905
+ cropUrl: string().optional()
25805
25906
  });
25806
25907
  var MediaFileLiteSchema = object({
25807
25908
  key: string(),
@@ -31325,6 +31426,12 @@ Object.freeze({
31325
31426
  addonId: null,
31326
31427
  access: "view"
31327
31428
  },
31429
+ "deviceManager.getBindingsBatch": {
31430
+ capName: "device-manager",
31431
+ capScope: "system",
31432
+ addonId: null,
31433
+ access: "view"
31434
+ },
31328
31435
  "deviceManager.getChildren": {
31329
31436
  capName: "device-manager",
31330
31437
  capScope: "system",
@@ -31385,6 +31492,12 @@ Object.freeze({
31385
31492
  addonId: null,
31386
31493
  access: "view"
31387
31494
  },
31495
+ "deviceManager.getLinkedDevicesBatch": {
31496
+ capName: "device-manager",
31497
+ capScope: "system",
31498
+ addonId: null,
31499
+ access: "view"
31500
+ },
31388
31501
  "deviceManager.getRoleDisplayDefaults": {
31389
31502
  capName: "device-manager",
31390
31503
  capScope: "system",
@@ -33155,6 +33268,12 @@ Object.freeze({
33155
33268
  addonId: null,
33156
33269
  access: "view"
33157
33270
  },
33271
+ "pipelineAnalytics.getGroup": {
33272
+ capName: "pipeline-analytics",
33273
+ capScope: "device",
33274
+ addonId: null,
33275
+ access: "view"
33276
+ },
33158
33277
  "pipelineAnalytics.getKeyEvents": {
33159
33278
  capName: "pipeline-analytics",
33160
33279
  capScope: "device",
@@ -33239,6 +33358,12 @@ Object.freeze({
33239
33358
  addonId: null,
33240
33359
  access: "view"
33241
33360
  },
33361
+ "pipelineAnalytics.listGroups": {
33362
+ capName: "pipeline-analytics",
33363
+ capScope: "device",
33364
+ addonId: null,
33365
+ access: "view"
33366
+ },
33242
33367
  "pipelineAnalytics.listOpsLog": {
33243
33368
  capName: "pipeline-analytics",
33244
33369
  capScope: "device",
@@ -36020,6 +36145,11 @@ Object.freeze({
36020
36145
  form: "single",
36021
36146
  optional: false
36022
36147
  }],
36148
+ "deviceManager.getBindingsBatch": [{
36149
+ name: "deviceIds",
36150
+ form: "array",
36151
+ optional: false
36152
+ }],
36023
36153
  "deviceManager.getChildren": [{
36024
36154
  name: "parentDeviceId",
36025
36155
  form: "single",
@@ -36065,6 +36195,11 @@ Object.freeze({
36065
36195
  form: "single",
36066
36196
  optional: false
36067
36197
  }],
36198
+ "deviceManager.getLinkedDevicesBatch": [{
36199
+ name: "deviceIds",
36200
+ form: "array",
36201
+ optional: false
36202
+ }],
36068
36203
  "deviceManager.getSettingsSchema": [{
36069
36204
  name: "deviceId",
36070
36205
  form: "single",
@@ -36085,6 +36220,11 @@ Object.freeze({
36085
36220
  form: "single",
36086
36221
  optional: false
36087
36222
  }],
36223
+ "deviceManager.listAll": [{
36224
+ name: "deviceIds",
36225
+ form: "array",
36226
+ optional: true
36227
+ }],
36088
36228
  "deviceManager.loadConfig": [{
36089
36229
  name: "deviceId",
36090
36230
  form: "single",
@@ -36658,6 +36798,11 @@ Object.freeze({
36658
36798
  form: "single",
36659
36799
  optional: false
36660
36800
  }],
36801
+ "pipelineAnalytics.getGroup": [{
36802
+ name: "deviceId",
36803
+ form: "single",
36804
+ optional: false
36805
+ }],
36661
36806
  "pipelineAnalytics.getKeyEvents": [{
36662
36807
  name: "deviceId",
36663
36808
  form: "single",
@@ -36713,6 +36858,11 @@ Object.freeze({
36713
36858
  form: "array",
36714
36859
  optional: false
36715
36860
  }],
36861
+ "pipelineAnalytics.listGroups": [{
36862
+ name: "deviceIds",
36863
+ form: "array",
36864
+ optional: false
36865
+ }],
36716
36866
  "pipelineAnalytics.listOpsLog": [{
36717
36867
  name: "deviceId",
36718
36868
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homematic",
3
- "version": "1.2.28",
3
+ "version": "1.2.29",
4
4
  "description": "Homematic / HomematicIP (CCU3 / RaspberryMatic) device-provider addon for CamStack — wraps the nodehomematic library",
5
5
  "keywords": [
6
6
  "camstack",