@camstack/addon-provider-ecowitt 0.2.26 → 0.2.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +184 -34
  2. package/dist/addon.mjs +184 -34
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -5804,6 +5804,13 @@ var BaseAddon = class {
5804
5804
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5805
5805
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5806
5806
  _registeredCapNames = [];
5807
+ /**
5808
+ * True only after `readAddonStore` actually answered. Constructor
5809
+ * defaults look like stored config when the store is down — a forked
5810
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5811
+ * mode, 2026-08-25) is not "the operator chose this".
5812
+ */
5813
+ settingsStoreReady = false;
5807
5814
  /** Default config values. Provided via constructor. */
5808
5815
  defaults;
5809
5816
  constructor(defaults) {
@@ -6204,7 +6211,9 @@ var BaseAddon = class {
6204
6211
  ];
6205
6212
  let lastErr;
6206
6213
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6207
- return await settings.readAddonStore() ?? {};
6214
+ const stored = await settings.readAddonStore() ?? {};
6215
+ this.settingsStoreReady = true;
6216
+ return stored;
6208
6217
  } catch (err) {
6209
6218
  lastErr = err;
6210
6219
  const msg = err instanceof Error ? err.message : String(err);
@@ -6212,6 +6221,7 @@ var BaseAddon = class {
6212
6221
  if (attempt === delaysMs.length) break;
6213
6222
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6214
6223
  }
6224
+ this.settingsStoreReady = false;
6215
6225
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6216
6226
  return {};
6217
6227
  }
@@ -8115,6 +8125,12 @@ var ModelVariantGroupSchema = object({
8115
8125
  */
8116
8126
  resolution: number().int().positive().optional()
8117
8127
  });
8128
+ var ModelProviderIdSchema = _enum([
8129
+ "camstack",
8130
+ "frigate",
8131
+ "scrypted",
8132
+ "custom"
8133
+ ]);
8118
8134
  var ModelCatalogEntrySchema = object({
8119
8135
  id: string(),
8120
8136
  name: string(),
@@ -8212,6 +8228,12 @@ var ModelCatalogEntrySchema = object({
8212
8228
  */
8213
8229
  group: ModelVariantGroupSchema.optional(),
8214
8230
  /**
8231
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8232
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8233
+ * persisted before this field existed (`inferModelProvider` fills those).
8234
+ */
8235
+ provider: ModelProviderIdSchema.optional(),
8236
+ /**
8215
8237
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8216
8238
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8217
8239
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12003,6 +12025,27 @@ var LinkedDeviceSchema = object({
12003
12025
  features: array(string()),
12004
12026
  producesTrackedEvents: boolean().optional()
12005
12027
  });
12028
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12029
+ * The batch answer needs the tag; the single-device answer already has it
12030
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12031
+ var LinkedDevicesForDeviceSchema = object({
12032
+ deviceId: number(),
12033
+ mode: LinkedDevicesModeSchema,
12034
+ devices: array(LinkedDeviceSchema)
12035
+ });
12036
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12037
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12038
+ * object literal is exactly how the three drift apart. */
12039
+ var DeviceBindingsForDeviceSchema = object({
12040
+ deviceId: number(),
12041
+ entries: array(object({
12042
+ capName: string(),
12043
+ kind: _enum(["native", "wrapped"]),
12044
+ providerAddonId: string(),
12045
+ providerNodeId: string(),
12046
+ nativeAddonId: string()
12047
+ }))
12048
+ });
12006
12049
  var SavedDeviceRowSchema = object({
12007
12050
  /** Numeric id reserved at allocateDeviceId time. */
12008
12051
  id: number(),
@@ -12228,11 +12271,25 @@ method(object({
12228
12271
  projection: _enum(["full", "slim"]).optional(),
12229
12272
  /** Return only camera devices. Filtering server-side instead of
12230
12273
  * shipping 293 rows to find 12. */
12231
- isCamera: boolean().optional()
12274
+ isCamera: boolean().optional(),
12275
+ /**
12276
+ * Return only these device ids. For the caller that already KNOWS the
12277
+ * handful it wants and needs a field the id-bearing answer does not
12278
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12279
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12280
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12281
+ * refetches on the reconcile interval, on a phone.
12282
+ *
12283
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12284
+ * keys rather than rejecting them (verified against the live hub
12285
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12286
+ * it answers today and the caller filters as it already does.
12287
+ */
12288
+ deviceIds: array(number()).optional()
12232
12289
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12233
12290
  mode: LinkedDevicesModeSchema,
12234
12291
  devices: array(LinkedDeviceSchema)
12235
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12292
+ })), 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({
12236
12293
  deviceId: number(),
12237
12294
  values: record(string(), unknown())
12238
12295
  }), object({ success: literal(true) }), {
@@ -12259,25 +12316,7 @@ method(object({
12259
12316
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12260
12317
  kind: "mutation",
12261
12318
  auth: "admin"
12262
- }), method(object({ deviceId: number() }), object({
12263
- deviceId: number(),
12264
- entries: array(object({
12265
- capName: string(),
12266
- kind: _enum(["native", "wrapped"]),
12267
- providerAddonId: string(),
12268
- providerNodeId: string(),
12269
- nativeAddonId: string()
12270
- }))
12271
- })), method(object({}), array(object({
12272
- deviceId: number(),
12273
- entries: array(object({
12274
- capName: string(),
12275
- kind: _enum(["native", "wrapped"]),
12276
- providerAddonId: string(),
12277
- providerNodeId: string(),
12278
- nativeAddonId: string()
12279
- }))
12280
- }))), method(object({
12319
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12281
12320
  deviceId: number(),
12282
12321
  capName: string(),
12283
12322
  wrapperAddonId: string(),
@@ -14687,12 +14726,15 @@ var NcOccupancyConditionSchema = object({
14687
14726
  * there is no second switch that can disagree with the first and every rule
14688
14727
  * authored before the decision migrates for free (`audioModeOf`):
14689
14728
  *
14690
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14691
- * classifier labels with one of them. No window, no percentage:
14692
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14693
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14694
- * the analyzer's (`classificationMinScore`, per device) a label only
14695
- * reaches this condition if the classifier was already confident enough.
14729
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14730
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14731
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14732
+ * frames is the wrong question for a classifier that labels 1–3 frames
14733
+ * per episode. The count window is the brake that drops a single-frame
14734
+ * false positive; the rule's own `throttle` cooldown is the other. The
14735
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14736
+ * per device) — a label only reaches this condition if the classifier was
14737
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14696
14738
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14697
14739
  * the condition: at least `hitPercent`% of the samples over
14698
14740
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14719,14 +14761,22 @@ var NcOccupancyConditionSchema = object({
14719
14761
  * an operator who typed `dog` mean the same thing.
14720
14762
  */
14721
14763
  var NcAudioConditionSchema = object({
14722
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14764
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14723
14765
  labels: array(string().min(1)).min(1).optional(),
14724
14766
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14725
14767
  dbThreshold: number().min(-96).max(0).optional(),
14726
14768
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14727
14769
  hitPercent: number().int().min(1).max(100).default(60),
14728
14770
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14729
- samplingSeconds: number().int().min(1).max(300).default(10)
14771
+ samplingSeconds: number().int().min(1).max(300).default(10),
14772
+ /**
14773
+ * LABEL MODE: how many labelled frames must land inside
14774
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14775
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14776
+ */
14777
+ confirmHits: number().int().min(1).max(20).optional(),
14778
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14779
+ confirmWindowSec: number().int().min(1).max(60).optional()
14730
14780
  });
14731
14781
  /**
14732
14782
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17100,6 +17150,46 @@ var RecentTracksPageSchema = object({
17100
17150
  /** Cursor for the next page, or null when this page is the last. */
17101
17151
  nextCursor: string().nullable()
17102
17152
  });
17153
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17154
+ var LIST_GROUPS_MAX_LIMIT = 100;
17155
+ var AnalyticsGroupRecordSchema = object({
17156
+ id: string(),
17157
+ deviceId: number().int(),
17158
+ openedAt: number().int(),
17159
+ closedAt: number().int(),
17160
+ timestamp: number().int(),
17161
+ memberCount: number().int(),
17162
+ memberTrackIds: array(string()).readonly(),
17163
+ className: string(),
17164
+ classes: array(string()).readonly(),
17165
+ /** Relative event-media path, or null when the group has no picture yet. */
17166
+ mediaUrl: string().nullable(),
17167
+ singleton: boolean()
17168
+ });
17169
+ var AnalyticsGroupMemberSchema = object({
17170
+ trackId: string(),
17171
+ deviceId: number().int(),
17172
+ className: string(),
17173
+ firstSeen: number().int(),
17174
+ lastSeen: number().int(),
17175
+ mediaUrl: string().nullable()
17176
+ });
17177
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17178
+ var ListGroupsQueryInput = object({
17179
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17180
+ deviceIds: array(number()),
17181
+ /** Window lower bound on `closedAt` (inclusive). */
17182
+ since: number().optional(),
17183
+ /** Window upper bound on `openedAt` (inclusive). */
17184
+ until: number().optional(),
17185
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17186
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17187
+ cursor: string().optional()
17188
+ });
17189
+ var ListGroupsPageSchema = object({
17190
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17191
+ nextCursor: string().nullable()
17192
+ });
17103
17193
  var KeyEventQueryInput = object({
17104
17194
  deviceId: number(),
17105
17195
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17175,7 +17265,9 @@ var TrackCascadeCountsSchema = object({
17175
17265
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17176
17266
  plates: number().int(),
17177
17267
  /** Per-track CLIP search vectors removed (best-effort). */
17178
- embeddings: number().int()
17268
+ embeddings: number().int(),
17269
+ /** Group membership + group rows removed with their last member (best-effort). */
17270
+ groups: number().int()
17179
17271
  });
17180
17272
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17181
17273
  var DiskReconcileCountsSchema = object({
@@ -17321,7 +17413,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17321
17413
  * stationary registry). Default false: the timeline lists passages,
17322
17414
  * not parking records (operator decision, 2026-08-15). */
17323
17415
  includeStationary: boolean().optional()
17324
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17416
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17417
+ deviceId: number(),
17418
+ groupId: string().min(1)
17419
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17325
17420
  kind: "mutation",
17326
17421
  auth: "admin"
17327
17422
  }), 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({
@@ -17631,7 +17726,8 @@ var PipelineModelOptionSchema = object({
17631
17726
  sizeMB: number()
17632
17727
  })),
17633
17728
  group: ModelVariantGroupSchema.optional(),
17634
- legacy: boolean().optional()
17729
+ legacy: boolean().optional(),
17730
+ provider: ModelProviderIdSchema.optional()
17635
17731
  });
17636
17732
  var ConfigFieldBridge = custom();
17637
17733
  var PipelineAddonSchemaSchema = object({
@@ -25728,7 +25824,12 @@ var PlateInfoSchema = object({
25728
25824
  plateBbox: BoundingBoxSchema.optional(),
25729
25825
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25730
25826
  keyFrameMediaKey: string().optional(),
25731
- base64: string().optional()
25827
+ base64: string().optional(),
25828
+ /**
25829
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25830
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25831
+ */
25832
+ cropUrl: string().optional()
25732
25833
  });
25733
25834
  var MediaFileLiteSchema = object({
25734
25835
  key: string(),
@@ -31252,6 +31353,12 @@ Object.freeze({
31252
31353
  addonId: null,
31253
31354
  access: "view"
31254
31355
  },
31356
+ "deviceManager.getBindingsBatch": {
31357
+ capName: "device-manager",
31358
+ capScope: "system",
31359
+ addonId: null,
31360
+ access: "view"
31361
+ },
31255
31362
  "deviceManager.getChildren": {
31256
31363
  capName: "device-manager",
31257
31364
  capScope: "system",
@@ -31312,6 +31419,12 @@ Object.freeze({
31312
31419
  addonId: null,
31313
31420
  access: "view"
31314
31421
  },
31422
+ "deviceManager.getLinkedDevicesBatch": {
31423
+ capName: "device-manager",
31424
+ capScope: "system",
31425
+ addonId: null,
31426
+ access: "view"
31427
+ },
31315
31428
  "deviceManager.getRoleDisplayDefaults": {
31316
31429
  capName: "device-manager",
31317
31430
  capScope: "system",
@@ -33082,6 +33195,12 @@ Object.freeze({
33082
33195
  addonId: null,
33083
33196
  access: "view"
33084
33197
  },
33198
+ "pipelineAnalytics.getGroup": {
33199
+ capName: "pipeline-analytics",
33200
+ capScope: "device",
33201
+ addonId: null,
33202
+ access: "view"
33203
+ },
33085
33204
  "pipelineAnalytics.getKeyEvents": {
33086
33205
  capName: "pipeline-analytics",
33087
33206
  capScope: "device",
@@ -33166,6 +33285,12 @@ Object.freeze({
33166
33285
  addonId: null,
33167
33286
  access: "view"
33168
33287
  },
33288
+ "pipelineAnalytics.listGroups": {
33289
+ capName: "pipeline-analytics",
33290
+ capScope: "device",
33291
+ addonId: null,
33292
+ access: "view"
33293
+ },
33169
33294
  "pipelineAnalytics.listOpsLog": {
33170
33295
  capName: "pipeline-analytics",
33171
33296
  capScope: "device",
@@ -35947,6 +36072,11 @@ Object.freeze({
35947
36072
  form: "single",
35948
36073
  optional: false
35949
36074
  }],
36075
+ "deviceManager.getBindingsBatch": [{
36076
+ name: "deviceIds",
36077
+ form: "array",
36078
+ optional: false
36079
+ }],
35950
36080
  "deviceManager.getChildren": [{
35951
36081
  name: "parentDeviceId",
35952
36082
  form: "single",
@@ -35992,6 +36122,11 @@ Object.freeze({
35992
36122
  form: "single",
35993
36123
  optional: false
35994
36124
  }],
36125
+ "deviceManager.getLinkedDevicesBatch": [{
36126
+ name: "deviceIds",
36127
+ form: "array",
36128
+ optional: false
36129
+ }],
35995
36130
  "deviceManager.getSettingsSchema": [{
35996
36131
  name: "deviceId",
35997
36132
  form: "single",
@@ -36012,6 +36147,11 @@ Object.freeze({
36012
36147
  form: "single",
36013
36148
  optional: false
36014
36149
  }],
36150
+ "deviceManager.listAll": [{
36151
+ name: "deviceIds",
36152
+ form: "array",
36153
+ optional: true
36154
+ }],
36015
36155
  "deviceManager.loadConfig": [{
36016
36156
  name: "deviceId",
36017
36157
  form: "single",
@@ -36585,6 +36725,11 @@ Object.freeze({
36585
36725
  form: "single",
36586
36726
  optional: false
36587
36727
  }],
36728
+ "pipelineAnalytics.getGroup": [{
36729
+ name: "deviceId",
36730
+ form: "single",
36731
+ optional: false
36732
+ }],
36588
36733
  "pipelineAnalytics.getKeyEvents": [{
36589
36734
  name: "deviceId",
36590
36735
  form: "single",
@@ -36640,6 +36785,11 @@ Object.freeze({
36640
36785
  form: "array",
36641
36786
  optional: false
36642
36787
  }],
36788
+ "pipelineAnalytics.listGroups": [{
36789
+ name: "deviceIds",
36790
+ form: "array",
36791
+ optional: false
36792
+ }],
36643
36793
  "pipelineAnalytics.listOpsLog": [{
36644
36794
  name: "deviceId",
36645
36795
  form: "single",
package/dist/addon.mjs CHANGED
@@ -5803,6 +5803,13 @@ var BaseAddon = class {
5803
5803
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5804
5804
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5805
5805
  _registeredCapNames = [];
5806
+ /**
5807
+ * True only after `readAddonStore` actually answered. Constructor
5808
+ * defaults look like stored config when the store is down — a forked
5809
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5810
+ * mode, 2026-08-25) is not "the operator chose this".
5811
+ */
5812
+ settingsStoreReady = false;
5806
5813
  /** Default config values. Provided via constructor. */
5807
5814
  defaults;
5808
5815
  constructor(defaults) {
@@ -6203,7 +6210,9 @@ var BaseAddon = class {
6203
6210
  ];
6204
6211
  let lastErr;
6205
6212
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6206
- return await settings.readAddonStore() ?? {};
6213
+ const stored = await settings.readAddonStore() ?? {};
6214
+ this.settingsStoreReady = true;
6215
+ return stored;
6207
6216
  } catch (err) {
6208
6217
  lastErr = err;
6209
6218
  const msg = err instanceof Error ? err.message : String(err);
@@ -6211,6 +6220,7 @@ var BaseAddon = class {
6211
6220
  if (attempt === delaysMs.length) break;
6212
6221
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6213
6222
  }
6223
+ this.settingsStoreReady = false;
6214
6224
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6215
6225
  return {};
6216
6226
  }
@@ -8114,6 +8124,12 @@ var ModelVariantGroupSchema = object({
8114
8124
  */
8115
8125
  resolution: number().int().positive().optional()
8116
8126
  });
8127
+ var ModelProviderIdSchema = _enum([
8128
+ "camstack",
8129
+ "frigate",
8130
+ "scrypted",
8131
+ "custom"
8132
+ ]);
8117
8133
  var ModelCatalogEntrySchema = object({
8118
8134
  id: string(),
8119
8135
  name: string(),
@@ -8211,6 +8227,12 @@ var ModelCatalogEntrySchema = object({
8211
8227
  */
8212
8228
  group: ModelVariantGroupSchema.optional(),
8213
8229
  /**
8230
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8231
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8232
+ * persisted before this field existed (`inferModelProvider` fills those).
8233
+ */
8234
+ provider: ModelProviderIdSchema.optional(),
8235
+ /**
8214
8236
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8215
8237
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8216
8238
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12002,6 +12024,27 @@ var LinkedDeviceSchema = object({
12002
12024
  features: array(string()),
12003
12025
  producesTrackedEvents: boolean().optional()
12004
12026
  });
12027
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12028
+ * The batch answer needs the tag; the single-device answer already has it
12029
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12030
+ var LinkedDevicesForDeviceSchema = object({
12031
+ deviceId: number(),
12032
+ mode: LinkedDevicesModeSchema,
12033
+ devices: array(LinkedDeviceSchema)
12034
+ });
12035
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12036
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12037
+ * object literal is exactly how the three drift apart. */
12038
+ var DeviceBindingsForDeviceSchema = object({
12039
+ deviceId: number(),
12040
+ entries: array(object({
12041
+ capName: string(),
12042
+ kind: _enum(["native", "wrapped"]),
12043
+ providerAddonId: string(),
12044
+ providerNodeId: string(),
12045
+ nativeAddonId: string()
12046
+ }))
12047
+ });
12005
12048
  var SavedDeviceRowSchema = object({
12006
12049
  /** Numeric id reserved at allocateDeviceId time. */
12007
12050
  id: number(),
@@ -12227,11 +12270,25 @@ method(object({
12227
12270
  projection: _enum(["full", "slim"]).optional(),
12228
12271
  /** Return only camera devices. Filtering server-side instead of
12229
12272
  * shipping 293 rows to find 12. */
12230
- isCamera: boolean().optional()
12273
+ isCamera: boolean().optional(),
12274
+ /**
12275
+ * Return only these device ids. For the caller that already KNOWS the
12276
+ * handful it wants and needs a field the id-bearing answer does not
12277
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12278
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12279
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12280
+ * refetches on the reconcile interval, on a phone.
12281
+ *
12282
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12283
+ * keys rather than rejecting them (verified against the live hub
12284
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12285
+ * it answers today and the caller filters as it already does.
12286
+ */
12287
+ deviceIds: array(number()).optional()
12231
12288
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12232
12289
  mode: LinkedDevicesModeSchema,
12233
12290
  devices: array(LinkedDeviceSchema)
12234
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12291
+ })), 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({
12235
12292
  deviceId: number(),
12236
12293
  values: record(string(), unknown())
12237
12294
  }), object({ success: literal(true) }), {
@@ -12258,25 +12315,7 @@ method(object({
12258
12315
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12259
12316
  kind: "mutation",
12260
12317
  auth: "admin"
12261
- }), method(object({ deviceId: number() }), object({
12262
- deviceId: number(),
12263
- entries: array(object({
12264
- capName: string(),
12265
- kind: _enum(["native", "wrapped"]),
12266
- providerAddonId: string(),
12267
- providerNodeId: string(),
12268
- nativeAddonId: string()
12269
- }))
12270
- })), method(object({}), array(object({
12271
- deviceId: number(),
12272
- entries: array(object({
12273
- capName: string(),
12274
- kind: _enum(["native", "wrapped"]),
12275
- providerAddonId: string(),
12276
- providerNodeId: string(),
12277
- nativeAddonId: string()
12278
- }))
12279
- }))), method(object({
12318
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12280
12319
  deviceId: number(),
12281
12320
  capName: string(),
12282
12321
  wrapperAddonId: string(),
@@ -14686,12 +14725,15 @@ var NcOccupancyConditionSchema = object({
14686
14725
  * there is no second switch that can disagree with the first and every rule
14687
14726
  * authored before the decision migrates for free (`audioModeOf`):
14688
14727
  *
14689
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14690
- * classifier labels with one of them. No window, no percentage:
14691
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14692
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14693
- * the analyzer's (`classificationMinScore`, per device) a label only
14694
- * reaches this condition if the classifier was already confident enough.
14728
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14729
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14730
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14731
+ * frames is the wrong question for a classifier that labels 1–3 frames
14732
+ * per episode. The count window is the brake that drops a single-frame
14733
+ * false positive; the rule's own `throttle` cooldown is the other. The
14734
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14735
+ * per device) — a label only reaches this condition if the classifier was
14736
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14695
14737
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14696
14738
  * the condition: at least `hitPercent`% of the samples over
14697
14739
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14718,14 +14760,22 @@ var NcOccupancyConditionSchema = object({
14718
14760
  * an operator who typed `dog` mean the same thing.
14719
14761
  */
14720
14762
  var NcAudioConditionSchema = object({
14721
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14763
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14722
14764
  labels: array(string().min(1)).min(1).optional(),
14723
14765
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14724
14766
  dbThreshold: number().min(-96).max(0).optional(),
14725
14767
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14726
14768
  hitPercent: number().int().min(1).max(100).default(60),
14727
14769
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14728
- samplingSeconds: number().int().min(1).max(300).default(10)
14770
+ samplingSeconds: number().int().min(1).max(300).default(10),
14771
+ /**
14772
+ * LABEL MODE: how many labelled frames must land inside
14773
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14774
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14775
+ */
14776
+ confirmHits: number().int().min(1).max(20).optional(),
14777
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14778
+ confirmWindowSec: number().int().min(1).max(60).optional()
14729
14779
  });
14730
14780
  /**
14731
14781
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17099,6 +17149,46 @@ var RecentTracksPageSchema = object({
17099
17149
  /** Cursor for the next page, or null when this page is the last. */
17100
17150
  nextCursor: string().nullable()
17101
17151
  });
17152
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17153
+ var LIST_GROUPS_MAX_LIMIT = 100;
17154
+ var AnalyticsGroupRecordSchema = object({
17155
+ id: string(),
17156
+ deviceId: number().int(),
17157
+ openedAt: number().int(),
17158
+ closedAt: number().int(),
17159
+ timestamp: number().int(),
17160
+ memberCount: number().int(),
17161
+ memberTrackIds: array(string()).readonly(),
17162
+ className: string(),
17163
+ classes: array(string()).readonly(),
17164
+ /** Relative event-media path, or null when the group has no picture yet. */
17165
+ mediaUrl: string().nullable(),
17166
+ singleton: boolean()
17167
+ });
17168
+ var AnalyticsGroupMemberSchema = object({
17169
+ trackId: string(),
17170
+ deviceId: number().int(),
17171
+ className: string(),
17172
+ firstSeen: number().int(),
17173
+ lastSeen: number().int(),
17174
+ mediaUrl: string().nullable()
17175
+ });
17176
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17177
+ var ListGroupsQueryInput = object({
17178
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17179
+ deviceIds: array(number()),
17180
+ /** Window lower bound on `closedAt` (inclusive). */
17181
+ since: number().optional(),
17182
+ /** Window upper bound on `openedAt` (inclusive). */
17183
+ until: number().optional(),
17184
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17185
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17186
+ cursor: string().optional()
17187
+ });
17188
+ var ListGroupsPageSchema = object({
17189
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17190
+ nextCursor: string().nullable()
17191
+ });
17102
17192
  var KeyEventQueryInput = object({
17103
17193
  deviceId: number(),
17104
17194
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17174,7 +17264,9 @@ var TrackCascadeCountsSchema = object({
17174
17264
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17175
17265
  plates: number().int(),
17176
17266
  /** Per-track CLIP search vectors removed (best-effort). */
17177
- embeddings: number().int()
17267
+ embeddings: number().int(),
17268
+ /** Group membership + group rows removed with their last member (best-effort). */
17269
+ groups: number().int()
17178
17270
  });
17179
17271
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17180
17272
  var DiskReconcileCountsSchema = object({
@@ -17320,7 +17412,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17320
17412
  * stationary registry). Default false: the timeline lists passages,
17321
17413
  * not parking records (operator decision, 2026-08-15). */
17322
17414
  includeStationary: boolean().optional()
17323
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17415
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17416
+ deviceId: number(),
17417
+ groupId: string().min(1)
17418
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17324
17419
  kind: "mutation",
17325
17420
  auth: "admin"
17326
17421
  }), 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({
@@ -17630,7 +17725,8 @@ var PipelineModelOptionSchema = object({
17630
17725
  sizeMB: number()
17631
17726
  })),
17632
17727
  group: ModelVariantGroupSchema.optional(),
17633
- legacy: boolean().optional()
17728
+ legacy: boolean().optional(),
17729
+ provider: ModelProviderIdSchema.optional()
17634
17730
  });
17635
17731
  var ConfigFieldBridge = custom();
17636
17732
  var PipelineAddonSchemaSchema = object({
@@ -25727,7 +25823,12 @@ var PlateInfoSchema = object({
25727
25823
  plateBbox: BoundingBoxSchema.optional(),
25728
25824
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25729
25825
  keyFrameMediaKey: string().optional(),
25730
- base64: string().optional()
25826
+ base64: string().optional(),
25827
+ /**
25828
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25829
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25830
+ */
25831
+ cropUrl: string().optional()
25731
25832
  });
25732
25833
  var MediaFileLiteSchema = object({
25733
25834
  key: string(),
@@ -31251,6 +31352,12 @@ Object.freeze({
31251
31352
  addonId: null,
31252
31353
  access: "view"
31253
31354
  },
31355
+ "deviceManager.getBindingsBatch": {
31356
+ capName: "device-manager",
31357
+ capScope: "system",
31358
+ addonId: null,
31359
+ access: "view"
31360
+ },
31254
31361
  "deviceManager.getChildren": {
31255
31362
  capName: "device-manager",
31256
31363
  capScope: "system",
@@ -31311,6 +31418,12 @@ Object.freeze({
31311
31418
  addonId: null,
31312
31419
  access: "view"
31313
31420
  },
31421
+ "deviceManager.getLinkedDevicesBatch": {
31422
+ capName: "device-manager",
31423
+ capScope: "system",
31424
+ addonId: null,
31425
+ access: "view"
31426
+ },
31314
31427
  "deviceManager.getRoleDisplayDefaults": {
31315
31428
  capName: "device-manager",
31316
31429
  capScope: "system",
@@ -33081,6 +33194,12 @@ Object.freeze({
33081
33194
  addonId: null,
33082
33195
  access: "view"
33083
33196
  },
33197
+ "pipelineAnalytics.getGroup": {
33198
+ capName: "pipeline-analytics",
33199
+ capScope: "device",
33200
+ addonId: null,
33201
+ access: "view"
33202
+ },
33084
33203
  "pipelineAnalytics.getKeyEvents": {
33085
33204
  capName: "pipeline-analytics",
33086
33205
  capScope: "device",
@@ -33165,6 +33284,12 @@ Object.freeze({
33165
33284
  addonId: null,
33166
33285
  access: "view"
33167
33286
  },
33287
+ "pipelineAnalytics.listGroups": {
33288
+ capName: "pipeline-analytics",
33289
+ capScope: "device",
33290
+ addonId: null,
33291
+ access: "view"
33292
+ },
33168
33293
  "pipelineAnalytics.listOpsLog": {
33169
33294
  capName: "pipeline-analytics",
33170
33295
  capScope: "device",
@@ -35946,6 +36071,11 @@ Object.freeze({
35946
36071
  form: "single",
35947
36072
  optional: false
35948
36073
  }],
36074
+ "deviceManager.getBindingsBatch": [{
36075
+ name: "deviceIds",
36076
+ form: "array",
36077
+ optional: false
36078
+ }],
35949
36079
  "deviceManager.getChildren": [{
35950
36080
  name: "parentDeviceId",
35951
36081
  form: "single",
@@ -35991,6 +36121,11 @@ Object.freeze({
35991
36121
  form: "single",
35992
36122
  optional: false
35993
36123
  }],
36124
+ "deviceManager.getLinkedDevicesBatch": [{
36125
+ name: "deviceIds",
36126
+ form: "array",
36127
+ optional: false
36128
+ }],
35994
36129
  "deviceManager.getSettingsSchema": [{
35995
36130
  name: "deviceId",
35996
36131
  form: "single",
@@ -36011,6 +36146,11 @@ Object.freeze({
36011
36146
  form: "single",
36012
36147
  optional: false
36013
36148
  }],
36149
+ "deviceManager.listAll": [{
36150
+ name: "deviceIds",
36151
+ form: "array",
36152
+ optional: true
36153
+ }],
36014
36154
  "deviceManager.loadConfig": [{
36015
36155
  name: "deviceId",
36016
36156
  form: "single",
@@ -36584,6 +36724,11 @@ Object.freeze({
36584
36724
  form: "single",
36585
36725
  optional: false
36586
36726
  }],
36727
+ "pipelineAnalytics.getGroup": [{
36728
+ name: "deviceId",
36729
+ form: "single",
36730
+ optional: false
36731
+ }],
36587
36732
  "pipelineAnalytics.getKeyEvents": [{
36588
36733
  name: "deviceId",
36589
36734
  form: "single",
@@ -36639,6 +36784,11 @@ Object.freeze({
36639
36784
  form: "array",
36640
36785
  optional: false
36641
36786
  }],
36787
+ "pipelineAnalytics.listGroups": [{
36788
+ name: "deviceIds",
36789
+ form: "array",
36790
+ optional: false
36791
+ }],
36642
36792
  "pipelineAnalytics.listOpsLog": [{
36643
36793
  name: "deviceId",
36644
36794
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-ecowitt",
3
- "version": "0.2.26",
3
+ "version": "0.2.27",
4
4
  "description": "Ecowitt weather-station device-provider addon for CamStack — wraps the @apocaliss92/nodewitt local-poll / push client",
5
5
  "keywords": [
6
6
  "camstack",