@camstack/addon-provider-tuya 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
@@ -6621,6 +6621,13 @@ var BaseAddon = class {
6621
6621
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
6622
6622
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
6623
6623
  _registeredCapNames = [];
6624
+ /**
6625
+ * True only after `readAddonStore` actually answered. Constructor
6626
+ * defaults look like stored config when the store is down — a forked
6627
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
6628
+ * mode, 2026-08-25) is not "the operator chose this".
6629
+ */
6630
+ settingsStoreReady = false;
6624
6631
  /** Default config values. Provided via constructor. */
6625
6632
  defaults;
6626
6633
  constructor(defaults) {
@@ -7021,7 +7028,9 @@ var BaseAddon = class {
7021
7028
  ];
7022
7029
  let lastErr;
7023
7030
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
7024
- return await settings.readAddonStore() ?? {};
7031
+ const stored = await settings.readAddonStore() ?? {};
7032
+ this.settingsStoreReady = true;
7033
+ return stored;
7025
7034
  } catch (err) {
7026
7035
  lastErr = err;
7027
7036
  const msg = err instanceof Error ? err.message : String(err);
@@ -7029,6 +7038,7 @@ var BaseAddon = class {
7029
7038
  if (attempt === delaysMs.length) break;
7030
7039
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
7031
7040
  }
7041
+ this.settingsStoreReady = false;
7032
7042
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
7033
7043
  return {};
7034
7044
  }
@@ -8932,6 +8942,12 @@ var ModelVariantGroupSchema = object({
8932
8942
  */
8933
8943
  resolution: number().int().positive().optional()
8934
8944
  });
8945
+ var ModelProviderIdSchema = _enum([
8946
+ "camstack",
8947
+ "frigate",
8948
+ "scrypted",
8949
+ "custom"
8950
+ ]);
8935
8951
  var ModelCatalogEntrySchema = object({
8936
8952
  id: string(),
8937
8953
  name: string(),
@@ -9029,6 +9045,12 @@ var ModelCatalogEntrySchema = object({
9029
9045
  */
9030
9046
  group: ModelVariantGroupSchema.optional(),
9031
9047
  /**
9048
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
9049
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
9050
+ * persisted before this field existed (`inferModelProvider` fills those).
9051
+ */
9052
+ provider: ModelProviderIdSchema.optional(),
9053
+ /**
9032
9054
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
9033
9055
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
9034
9056
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12837,6 +12859,27 @@ var LinkedDeviceSchema = object({
12837
12859
  features: array(string()),
12838
12860
  producesTrackedEvents: boolean().optional()
12839
12861
  });
12862
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12863
+ * The batch answer needs the tag; the single-device answer already has it
12864
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12865
+ var LinkedDevicesForDeviceSchema = object({
12866
+ deviceId: number(),
12867
+ mode: LinkedDevicesModeSchema,
12868
+ devices: array(LinkedDeviceSchema)
12869
+ });
12870
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12871
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12872
+ * object literal is exactly how the three drift apart. */
12873
+ var DeviceBindingsForDeviceSchema = object({
12874
+ deviceId: number(),
12875
+ entries: array(object({
12876
+ capName: string(),
12877
+ kind: _enum(["native", "wrapped"]),
12878
+ providerAddonId: string(),
12879
+ providerNodeId: string(),
12880
+ nativeAddonId: string()
12881
+ }))
12882
+ });
12840
12883
  var SavedDeviceRowSchema = object({
12841
12884
  /** Numeric id reserved at allocateDeviceId time. */
12842
12885
  id: number(),
@@ -13062,11 +13105,25 @@ method(object({
13062
13105
  projection: _enum(["full", "slim"]).optional(),
13063
13106
  /** Return only camera devices. Filtering server-side instead of
13064
13107
  * shipping 293 rows to find 12. */
13065
- isCamera: boolean().optional()
13108
+ isCamera: boolean().optional(),
13109
+ /**
13110
+ * Return only these device ids. For the caller that already KNOWS the
13111
+ * handful it wants and needs a field the id-bearing answer does not
13112
+ * carry — the viewer's linked-devices panel joins `type` and `online`
13113
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
13114
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
13115
+ * refetches on the reconcile interval, on a phone.
13116
+ *
13117
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
13118
+ * keys rather than rejecting them (verified against the live hub
13119
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
13120
+ * it answers today and the caller filters as it already does.
13121
+ */
13122
+ deviceIds: array(number()).optional()
13066
13123
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
13067
13124
  mode: LinkedDevicesModeSchema,
13068
13125
  devices: array(LinkedDeviceSchema)
13069
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
13126
+ })), 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({
13070
13127
  deviceId: number(),
13071
13128
  values: record(string(), unknown())
13072
13129
  }), object({ success: literal(true) }), {
@@ -13093,25 +13150,7 @@ method(object({
13093
13150
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
13094
13151
  kind: "mutation",
13095
13152
  auth: "admin"
13096
- }), method(object({ deviceId: number() }), object({
13097
- deviceId: number(),
13098
- entries: array(object({
13099
- capName: string(),
13100
- kind: _enum(["native", "wrapped"]),
13101
- providerAddonId: string(),
13102
- providerNodeId: string(),
13103
- nativeAddonId: string()
13104
- }))
13105
- })), method(object({}), array(object({
13106
- deviceId: number(),
13107
- entries: array(object({
13108
- capName: string(),
13109
- kind: _enum(["native", "wrapped"]),
13110
- providerAddonId: string(),
13111
- providerNodeId: string(),
13112
- nativeAddonId: string()
13113
- }))
13114
- }))), method(object({
13153
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
13115
13154
  deviceId: number(),
13116
13155
  capName: string(),
13117
13156
  wrapperAddonId: string(),
@@ -15521,12 +15560,15 @@ var NcOccupancyConditionSchema = object({
15521
15560
  * there is no second switch that can disagree with the first and every rule
15522
15561
  * authored before the decision migrates for free (`audioModeOf`):
15523
15562
  *
15524
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
15525
- * classifier labels with one of them. No window, no percentage:
15526
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
15527
- * `throttle` cooldown is the only brake. The per-label confidence floor is
15528
- * the analyzer's (`classificationMinScore`, per device) a label only
15529
- * reaches this condition if the classifier was already confident enough.
15563
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
15564
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
15565
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
15566
+ * frames is the wrong question for a classifier that labels 1–3 frames
15567
+ * per episode. The count window is the brake that drops a single-frame
15568
+ * false positive; the rule's own `throttle` cooldown is the other. The
15569
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
15570
+ * per device) — a label only reaches this condition if the classifier was
15571
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
15530
15572
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
15531
15573
  * the condition: at least `hitPercent`% of the samples over
15532
15574
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -15553,14 +15595,22 @@ var NcOccupancyConditionSchema = object({
15553
15595
  * an operator who typed `dog` mean the same thing.
15554
15596
  */
15555
15597
  var NcAudioConditionSchema = object({
15556
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
15598
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
15557
15599
  labels: array(string().min(1)).min(1).optional(),
15558
15600
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
15559
15601
  dbThreshold: number().min(-96).max(0).optional(),
15560
15602
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
15561
15603
  hitPercent: number().int().min(1).max(100).default(60),
15562
15604
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
15563
- samplingSeconds: number().int().min(1).max(300).default(10)
15605
+ samplingSeconds: number().int().min(1).max(300).default(10),
15606
+ /**
15607
+ * LABEL MODE: how many labelled frames must land inside
15608
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
15609
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
15610
+ */
15611
+ confirmHits: number().int().min(1).max(20).optional(),
15612
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
15613
+ confirmWindowSec: number().int().min(1).max(60).optional()
15564
15614
  });
15565
15615
  /**
15566
15616
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17934,6 +17984,46 @@ var RecentTracksPageSchema = object({
17934
17984
  /** Cursor for the next page, or null when this page is the last. */
17935
17985
  nextCursor: string().nullable()
17936
17986
  });
17987
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17988
+ var LIST_GROUPS_MAX_LIMIT = 100;
17989
+ var AnalyticsGroupRecordSchema = object({
17990
+ id: string(),
17991
+ deviceId: number().int(),
17992
+ openedAt: number().int(),
17993
+ closedAt: number().int(),
17994
+ timestamp: number().int(),
17995
+ memberCount: number().int(),
17996
+ memberTrackIds: array(string()).readonly(),
17997
+ className: string(),
17998
+ classes: array(string()).readonly(),
17999
+ /** Relative event-media path, or null when the group has no picture yet. */
18000
+ mediaUrl: string().nullable(),
18001
+ singleton: boolean()
18002
+ });
18003
+ var AnalyticsGroupMemberSchema = object({
18004
+ trackId: string(),
18005
+ deviceId: number().int(),
18006
+ className: string(),
18007
+ firstSeen: number().int(),
18008
+ lastSeen: number().int(),
18009
+ mediaUrl: string().nullable()
18010
+ });
18011
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
18012
+ var ListGroupsQueryInput = object({
18013
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
18014
+ deviceIds: array(number()),
18015
+ /** Window lower bound on `closedAt` (inclusive). */
18016
+ since: number().optional(),
18017
+ /** Window upper bound on `openedAt` (inclusive). */
18018
+ until: number().optional(),
18019
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
18020
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
18021
+ cursor: string().optional()
18022
+ });
18023
+ var ListGroupsPageSchema = object({
18024
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
18025
+ nextCursor: string().nullable()
18026
+ });
17937
18027
  var KeyEventQueryInput = object({
17938
18028
  deviceId: number(),
17939
18029
  /** Window lower bound (track firstSeen ≥ since). */
@@ -18009,7 +18099,9 @@ var TrackCascadeCountsSchema = object({
18009
18099
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
18010
18100
  plates: number().int(),
18011
18101
  /** Per-track CLIP search vectors removed (best-effort). */
18012
- embeddings: number().int()
18102
+ embeddings: number().int(),
18103
+ /** Group membership + group rows removed with their last member (best-effort). */
18104
+ groups: number().int()
18013
18105
  });
18014
18106
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
18015
18107
  var DiskReconcileCountsSchema = object({
@@ -18155,7 +18247,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18155
18247
  * stationary registry). Default false: the timeline lists passages,
18156
18248
  * not parking records (operator decision, 2026-08-15). */
18157
18249
  includeStationary: boolean().optional()
18158
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
18250
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
18251
+ deviceId: number(),
18252
+ groupId: string().min(1)
18253
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
18159
18254
  kind: "mutation",
18160
18255
  auth: "admin"
18161
18256
  }), 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({
@@ -18465,7 +18560,8 @@ var PipelineModelOptionSchema = object({
18465
18560
  sizeMB: number()
18466
18561
  })),
18467
18562
  group: ModelVariantGroupSchema.optional(),
18468
- legacy: boolean().optional()
18563
+ legacy: boolean().optional(),
18564
+ provider: ModelProviderIdSchema.optional()
18469
18565
  });
18470
18566
  var ConfigFieldBridge = custom();
18471
18567
  var PipelineAddonSchemaSchema = object({
@@ -26570,7 +26666,12 @@ var PlateInfoSchema = object({
26570
26666
  plateBbox: BoundingBoxSchema.optional(),
26571
26667
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
26572
26668
  keyFrameMediaKey: string().optional(),
26573
- base64: string().optional()
26669
+ base64: string().optional(),
26670
+ /**
26671
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
26672
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
26673
+ */
26674
+ cropUrl: string().optional()
26574
26675
  });
26575
26676
  var MediaFileLiteSchema = object({
26576
26677
  key: string(),
@@ -32094,6 +32195,12 @@ Object.freeze({
32094
32195
  addonId: null,
32095
32196
  access: "view"
32096
32197
  },
32198
+ "deviceManager.getBindingsBatch": {
32199
+ capName: "device-manager",
32200
+ capScope: "system",
32201
+ addonId: null,
32202
+ access: "view"
32203
+ },
32097
32204
  "deviceManager.getChildren": {
32098
32205
  capName: "device-manager",
32099
32206
  capScope: "system",
@@ -32154,6 +32261,12 @@ Object.freeze({
32154
32261
  addonId: null,
32155
32262
  access: "view"
32156
32263
  },
32264
+ "deviceManager.getLinkedDevicesBatch": {
32265
+ capName: "device-manager",
32266
+ capScope: "system",
32267
+ addonId: null,
32268
+ access: "view"
32269
+ },
32157
32270
  "deviceManager.getRoleDisplayDefaults": {
32158
32271
  capName: "device-manager",
32159
32272
  capScope: "system",
@@ -33924,6 +34037,12 @@ Object.freeze({
33924
34037
  addonId: null,
33925
34038
  access: "view"
33926
34039
  },
34040
+ "pipelineAnalytics.getGroup": {
34041
+ capName: "pipeline-analytics",
34042
+ capScope: "device",
34043
+ addonId: null,
34044
+ access: "view"
34045
+ },
33927
34046
  "pipelineAnalytics.getKeyEvents": {
33928
34047
  capName: "pipeline-analytics",
33929
34048
  capScope: "device",
@@ -34008,6 +34127,12 @@ Object.freeze({
34008
34127
  addonId: null,
34009
34128
  access: "view"
34010
34129
  },
34130
+ "pipelineAnalytics.listGroups": {
34131
+ capName: "pipeline-analytics",
34132
+ capScope: "device",
34133
+ addonId: null,
34134
+ access: "view"
34135
+ },
34011
34136
  "pipelineAnalytics.listOpsLog": {
34012
34137
  capName: "pipeline-analytics",
34013
34138
  capScope: "device",
@@ -36789,6 +36914,11 @@ Object.freeze({
36789
36914
  form: "single",
36790
36915
  optional: false
36791
36916
  }],
36917
+ "deviceManager.getBindingsBatch": [{
36918
+ name: "deviceIds",
36919
+ form: "array",
36920
+ optional: false
36921
+ }],
36792
36922
  "deviceManager.getChildren": [{
36793
36923
  name: "parentDeviceId",
36794
36924
  form: "single",
@@ -36834,6 +36964,11 @@ Object.freeze({
36834
36964
  form: "single",
36835
36965
  optional: false
36836
36966
  }],
36967
+ "deviceManager.getLinkedDevicesBatch": [{
36968
+ name: "deviceIds",
36969
+ form: "array",
36970
+ optional: false
36971
+ }],
36837
36972
  "deviceManager.getSettingsSchema": [{
36838
36973
  name: "deviceId",
36839
36974
  form: "single",
@@ -36854,6 +36989,11 @@ Object.freeze({
36854
36989
  form: "single",
36855
36990
  optional: false
36856
36991
  }],
36992
+ "deviceManager.listAll": [{
36993
+ name: "deviceIds",
36994
+ form: "array",
36995
+ optional: true
36996
+ }],
36857
36997
  "deviceManager.loadConfig": [{
36858
36998
  name: "deviceId",
36859
36999
  form: "single",
@@ -37427,6 +37567,11 @@ Object.freeze({
37427
37567
  form: "single",
37428
37568
  optional: false
37429
37569
  }],
37570
+ "pipelineAnalytics.getGroup": [{
37571
+ name: "deviceId",
37572
+ form: "single",
37573
+ optional: false
37574
+ }],
37430
37575
  "pipelineAnalytics.getKeyEvents": [{
37431
37576
  name: "deviceId",
37432
37577
  form: "single",
@@ -37482,6 +37627,11 @@ Object.freeze({
37482
37627
  form: "array",
37483
37628
  optional: false
37484
37629
  }],
37630
+ "pipelineAnalytics.listGroups": [{
37631
+ name: "deviceIds",
37632
+ form: "array",
37633
+ optional: false
37634
+ }],
37485
37635
  "pipelineAnalytics.listOpsLog": [{
37486
37636
  name: "deviceId",
37487
37637
  form: "single",
package/dist/addon.mjs CHANGED
@@ -6620,6 +6620,13 @@ var BaseAddon = class {
6620
6620
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
6621
6621
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
6622
6622
  _registeredCapNames = [];
6623
+ /**
6624
+ * True only after `readAddonStore` actually answered. Constructor
6625
+ * defaults look like stored config when the store is down — a forked
6626
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
6627
+ * mode, 2026-08-25) is not "the operator chose this".
6628
+ */
6629
+ settingsStoreReady = false;
6623
6630
  /** Default config values. Provided via constructor. */
6624
6631
  defaults;
6625
6632
  constructor(defaults) {
@@ -7020,7 +7027,9 @@ var BaseAddon = class {
7020
7027
  ];
7021
7028
  let lastErr;
7022
7029
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
7023
- return await settings.readAddonStore() ?? {};
7030
+ const stored = await settings.readAddonStore() ?? {};
7031
+ this.settingsStoreReady = true;
7032
+ return stored;
7024
7033
  } catch (err) {
7025
7034
  lastErr = err;
7026
7035
  const msg = err instanceof Error ? err.message : String(err);
@@ -7028,6 +7037,7 @@ var BaseAddon = class {
7028
7037
  if (attempt === delaysMs.length) break;
7029
7038
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
7030
7039
  }
7040
+ this.settingsStoreReady = false;
7031
7041
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
7032
7042
  return {};
7033
7043
  }
@@ -8931,6 +8941,12 @@ var ModelVariantGroupSchema = object({
8931
8941
  */
8932
8942
  resolution: number().int().positive().optional()
8933
8943
  });
8944
+ var ModelProviderIdSchema = _enum([
8945
+ "camstack",
8946
+ "frigate",
8947
+ "scrypted",
8948
+ "custom"
8949
+ ]);
8934
8950
  var ModelCatalogEntrySchema = object({
8935
8951
  id: string(),
8936
8952
  name: string(),
@@ -9028,6 +9044,12 @@ var ModelCatalogEntrySchema = object({
9028
9044
  */
9029
9045
  group: ModelVariantGroupSchema.optional(),
9030
9046
  /**
9047
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
9048
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
9049
+ * persisted before this field existed (`inferModelProvider` fills those).
9050
+ */
9051
+ provider: ModelProviderIdSchema.optional(),
9052
+ /**
9031
9053
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
9032
9054
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
9033
9055
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12836,6 +12858,27 @@ var LinkedDeviceSchema = object({
12836
12858
  features: array(string()),
12837
12859
  producesTrackedEvents: boolean().optional()
12838
12860
  });
12861
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12862
+ * The batch answer needs the tag; the single-device answer already has it
12863
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12864
+ var LinkedDevicesForDeviceSchema = object({
12865
+ deviceId: number(),
12866
+ mode: LinkedDevicesModeSchema,
12867
+ devices: array(LinkedDeviceSchema)
12868
+ });
12869
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12870
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12871
+ * object literal is exactly how the three drift apart. */
12872
+ var DeviceBindingsForDeviceSchema = object({
12873
+ deviceId: number(),
12874
+ entries: array(object({
12875
+ capName: string(),
12876
+ kind: _enum(["native", "wrapped"]),
12877
+ providerAddonId: string(),
12878
+ providerNodeId: string(),
12879
+ nativeAddonId: string()
12880
+ }))
12881
+ });
12839
12882
  var SavedDeviceRowSchema = object({
12840
12883
  /** Numeric id reserved at allocateDeviceId time. */
12841
12884
  id: number(),
@@ -13061,11 +13104,25 @@ method(object({
13061
13104
  projection: _enum(["full", "slim"]).optional(),
13062
13105
  /** Return only camera devices. Filtering server-side instead of
13063
13106
  * shipping 293 rows to find 12. */
13064
- isCamera: boolean().optional()
13107
+ isCamera: boolean().optional(),
13108
+ /**
13109
+ * Return only these device ids. For the caller that already KNOWS the
13110
+ * handful it wants and needs a field the id-bearing answer does not
13111
+ * carry — the viewer's linked-devices panel joins `type` and `online`
13112
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
13113
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
13114
+ * refetches on the reconcile interval, on a phone.
13115
+ *
13116
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
13117
+ * keys rather than rejecting them (verified against the live hub
13118
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
13119
+ * it answers today and the caller filters as it already does.
13120
+ */
13121
+ deviceIds: array(number()).optional()
13065
13122
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
13066
13123
  mode: LinkedDevicesModeSchema,
13067
13124
  devices: array(LinkedDeviceSchema)
13068
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
13125
+ })), 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({
13069
13126
  deviceId: number(),
13070
13127
  values: record(string(), unknown())
13071
13128
  }), object({ success: literal(true) }), {
@@ -13092,25 +13149,7 @@ method(object({
13092
13149
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
13093
13150
  kind: "mutation",
13094
13151
  auth: "admin"
13095
- }), method(object({ deviceId: number() }), object({
13096
- deviceId: number(),
13097
- entries: array(object({
13098
- capName: string(),
13099
- kind: _enum(["native", "wrapped"]),
13100
- providerAddonId: string(),
13101
- providerNodeId: string(),
13102
- nativeAddonId: string()
13103
- }))
13104
- })), method(object({}), array(object({
13105
- deviceId: number(),
13106
- entries: array(object({
13107
- capName: string(),
13108
- kind: _enum(["native", "wrapped"]),
13109
- providerAddonId: string(),
13110
- providerNodeId: string(),
13111
- nativeAddonId: string()
13112
- }))
13113
- }))), method(object({
13152
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
13114
13153
  deviceId: number(),
13115
13154
  capName: string(),
13116
13155
  wrapperAddonId: string(),
@@ -15520,12 +15559,15 @@ var NcOccupancyConditionSchema = object({
15520
15559
  * there is no second switch that can disagree with the first and every rule
15521
15560
  * authored before the decision migrates for free (`audioModeOf`):
15522
15561
  *
15523
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
15524
- * classifier labels with one of them. No window, no percentage:
15525
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
15526
- * `throttle` cooldown is the only brake. The per-label confidence floor is
15527
- * the analyzer's (`classificationMinScore`, per device) a label only
15528
- * reaches this condition if the classifier was already confident enough.
15562
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
15563
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
15564
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
15565
+ * frames is the wrong question for a classifier that labels 1–3 frames
15566
+ * per episode. The count window is the brake that drops a single-frame
15567
+ * false positive; the rule's own `throttle` cooldown is the other. The
15568
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
15569
+ * per device) — a label only reaches this condition if the classifier was
15570
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
15529
15571
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
15530
15572
  * the condition: at least `hitPercent`% of the samples over
15531
15573
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -15552,14 +15594,22 @@ var NcOccupancyConditionSchema = object({
15552
15594
  * an operator who typed `dog` mean the same thing.
15553
15595
  */
15554
15596
  var NcAudioConditionSchema = object({
15555
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
15597
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
15556
15598
  labels: array(string().min(1)).min(1).optional(),
15557
15599
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
15558
15600
  dbThreshold: number().min(-96).max(0).optional(),
15559
15601
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
15560
15602
  hitPercent: number().int().min(1).max(100).default(60),
15561
15603
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
15562
- samplingSeconds: number().int().min(1).max(300).default(10)
15604
+ samplingSeconds: number().int().min(1).max(300).default(10),
15605
+ /**
15606
+ * LABEL MODE: how many labelled frames must land inside
15607
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
15608
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
15609
+ */
15610
+ confirmHits: number().int().min(1).max(20).optional(),
15611
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
15612
+ confirmWindowSec: number().int().min(1).max(60).optional()
15563
15613
  });
15564
15614
  /**
15565
15615
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17933,6 +17983,46 @@ var RecentTracksPageSchema = object({
17933
17983
  /** Cursor for the next page, or null when this page is the last. */
17934
17984
  nextCursor: string().nullable()
17935
17985
  });
17986
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17987
+ var LIST_GROUPS_MAX_LIMIT = 100;
17988
+ var AnalyticsGroupRecordSchema = object({
17989
+ id: string(),
17990
+ deviceId: number().int(),
17991
+ openedAt: number().int(),
17992
+ closedAt: number().int(),
17993
+ timestamp: number().int(),
17994
+ memberCount: number().int(),
17995
+ memberTrackIds: array(string()).readonly(),
17996
+ className: string(),
17997
+ classes: array(string()).readonly(),
17998
+ /** Relative event-media path, or null when the group has no picture yet. */
17999
+ mediaUrl: string().nullable(),
18000
+ singleton: boolean()
18001
+ });
18002
+ var AnalyticsGroupMemberSchema = object({
18003
+ trackId: string(),
18004
+ deviceId: number().int(),
18005
+ className: string(),
18006
+ firstSeen: number().int(),
18007
+ lastSeen: number().int(),
18008
+ mediaUrl: string().nullable()
18009
+ });
18010
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
18011
+ var ListGroupsQueryInput = object({
18012
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
18013
+ deviceIds: array(number()),
18014
+ /** Window lower bound on `closedAt` (inclusive). */
18015
+ since: number().optional(),
18016
+ /** Window upper bound on `openedAt` (inclusive). */
18017
+ until: number().optional(),
18018
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
18019
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
18020
+ cursor: string().optional()
18021
+ });
18022
+ var ListGroupsPageSchema = object({
18023
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
18024
+ nextCursor: string().nullable()
18025
+ });
17936
18026
  var KeyEventQueryInput = object({
17937
18027
  deviceId: number(),
17938
18028
  /** Window lower bound (track firstSeen ≥ since). */
@@ -18008,7 +18098,9 @@ var TrackCascadeCountsSchema = object({
18008
18098
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
18009
18099
  plates: number().int(),
18010
18100
  /** Per-track CLIP search vectors removed (best-effort). */
18011
- embeddings: number().int()
18101
+ embeddings: number().int(),
18102
+ /** Group membership + group rows removed with their last member (best-effort). */
18103
+ groups: number().int()
18012
18104
  });
18013
18105
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
18014
18106
  var DiskReconcileCountsSchema = object({
@@ -18154,7 +18246,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18154
18246
  * stationary registry). Default false: the timeline lists passages,
18155
18247
  * not parking records (operator decision, 2026-08-15). */
18156
18248
  includeStationary: boolean().optional()
18157
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
18249
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
18250
+ deviceId: number(),
18251
+ groupId: string().min(1)
18252
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
18158
18253
  kind: "mutation",
18159
18254
  auth: "admin"
18160
18255
  }), 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({
@@ -18464,7 +18559,8 @@ var PipelineModelOptionSchema = object({
18464
18559
  sizeMB: number()
18465
18560
  })),
18466
18561
  group: ModelVariantGroupSchema.optional(),
18467
- legacy: boolean().optional()
18562
+ legacy: boolean().optional(),
18563
+ provider: ModelProviderIdSchema.optional()
18468
18564
  });
18469
18565
  var ConfigFieldBridge = custom();
18470
18566
  var PipelineAddonSchemaSchema = object({
@@ -26569,7 +26665,12 @@ var PlateInfoSchema = object({
26569
26665
  plateBbox: BoundingBoxSchema.optional(),
26570
26666
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
26571
26667
  keyFrameMediaKey: string().optional(),
26572
- base64: string().optional()
26668
+ base64: string().optional(),
26669
+ /**
26670
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
26671
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
26672
+ */
26673
+ cropUrl: string().optional()
26573
26674
  });
26574
26675
  var MediaFileLiteSchema = object({
26575
26676
  key: string(),
@@ -32093,6 +32194,12 @@ Object.freeze({
32093
32194
  addonId: null,
32094
32195
  access: "view"
32095
32196
  },
32197
+ "deviceManager.getBindingsBatch": {
32198
+ capName: "device-manager",
32199
+ capScope: "system",
32200
+ addonId: null,
32201
+ access: "view"
32202
+ },
32096
32203
  "deviceManager.getChildren": {
32097
32204
  capName: "device-manager",
32098
32205
  capScope: "system",
@@ -32153,6 +32260,12 @@ Object.freeze({
32153
32260
  addonId: null,
32154
32261
  access: "view"
32155
32262
  },
32263
+ "deviceManager.getLinkedDevicesBatch": {
32264
+ capName: "device-manager",
32265
+ capScope: "system",
32266
+ addonId: null,
32267
+ access: "view"
32268
+ },
32156
32269
  "deviceManager.getRoleDisplayDefaults": {
32157
32270
  capName: "device-manager",
32158
32271
  capScope: "system",
@@ -33923,6 +34036,12 @@ Object.freeze({
33923
34036
  addonId: null,
33924
34037
  access: "view"
33925
34038
  },
34039
+ "pipelineAnalytics.getGroup": {
34040
+ capName: "pipeline-analytics",
34041
+ capScope: "device",
34042
+ addonId: null,
34043
+ access: "view"
34044
+ },
33926
34045
  "pipelineAnalytics.getKeyEvents": {
33927
34046
  capName: "pipeline-analytics",
33928
34047
  capScope: "device",
@@ -34007,6 +34126,12 @@ Object.freeze({
34007
34126
  addonId: null,
34008
34127
  access: "view"
34009
34128
  },
34129
+ "pipelineAnalytics.listGroups": {
34130
+ capName: "pipeline-analytics",
34131
+ capScope: "device",
34132
+ addonId: null,
34133
+ access: "view"
34134
+ },
34010
34135
  "pipelineAnalytics.listOpsLog": {
34011
34136
  capName: "pipeline-analytics",
34012
34137
  capScope: "device",
@@ -36788,6 +36913,11 @@ Object.freeze({
36788
36913
  form: "single",
36789
36914
  optional: false
36790
36915
  }],
36916
+ "deviceManager.getBindingsBatch": [{
36917
+ name: "deviceIds",
36918
+ form: "array",
36919
+ optional: false
36920
+ }],
36791
36921
  "deviceManager.getChildren": [{
36792
36922
  name: "parentDeviceId",
36793
36923
  form: "single",
@@ -36833,6 +36963,11 @@ Object.freeze({
36833
36963
  form: "single",
36834
36964
  optional: false
36835
36965
  }],
36966
+ "deviceManager.getLinkedDevicesBatch": [{
36967
+ name: "deviceIds",
36968
+ form: "array",
36969
+ optional: false
36970
+ }],
36836
36971
  "deviceManager.getSettingsSchema": [{
36837
36972
  name: "deviceId",
36838
36973
  form: "single",
@@ -36853,6 +36988,11 @@ Object.freeze({
36853
36988
  form: "single",
36854
36989
  optional: false
36855
36990
  }],
36991
+ "deviceManager.listAll": [{
36992
+ name: "deviceIds",
36993
+ form: "array",
36994
+ optional: true
36995
+ }],
36856
36996
  "deviceManager.loadConfig": [{
36857
36997
  name: "deviceId",
36858
36998
  form: "single",
@@ -37426,6 +37566,11 @@ Object.freeze({
37426
37566
  form: "single",
37427
37567
  optional: false
37428
37568
  }],
37569
+ "pipelineAnalytics.getGroup": [{
37570
+ name: "deviceId",
37571
+ form: "single",
37572
+ optional: false
37573
+ }],
37429
37574
  "pipelineAnalytics.getKeyEvents": [{
37430
37575
  name: "deviceId",
37431
37576
  form: "single",
@@ -37481,6 +37626,11 @@ Object.freeze({
37481
37626
  form: "array",
37482
37627
  optional: false
37483
37628
  }],
37629
+ "pipelineAnalytics.listGroups": [{
37630
+ name: "deviceIds",
37631
+ form: "array",
37632
+ optional: false
37633
+ }],
37484
37634
  "pipelineAnalytics.listOpsLog": [{
37485
37635
  name: "deviceId",
37486
37636
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-tuya",
3
- "version": "0.2.27",
3
+ "version": "0.2.28",
4
4
  "description": "Tuya / Smart Life device-provider addon for CamStack — account-onboarded (Tuya IoT cloud fetch of device localKeys) + LOCAL DP control via the @apocaliss92/nodetuya encrypted-LAN client, exposing switch / water-heater-family kettle entities",
5
5
  "keywords": [
6
6
  "camstack",