@camstack/addon-provider-tuya 0.1.9 → 0.1.11

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 +220 -15
  2. package/dist/addon.mjs +220 -15
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -3,7 +3,7 @@ let crypto$1 = require("crypto");
3
3
  let net = require("net");
4
4
  let events = require("events");
5
5
  let dgram = require("dgram");
6
- //#region ../types/dist/event-category-CFZs3jI4.mjs
6
+ //#region ../types/dist/event-category-H4AVePnn.mjs
7
7
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
8
8
  EventCategory["SystemBoot"] = "system.boot";
9
9
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -153,6 +153,9 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
153
153
  EventCategory["RecordingSegmentWritten"] = "recording.segment.written";
154
154
  EventCategory["RecordingPolicyFallback"] = "recording.policy.fallback";
155
155
  EventCategory["RecordingRetentionCompleted"] = "recording.retention.completed";
156
+ /** Runner-sampled scrub thumbnail (~1/5 s/camera). Telemetry (D8): a lost
157
+ * thumb is a scrub gap the recorder's keyframe backfill covers. */
158
+ EventCategory["RecordingThumbSampled"] = "recording.thumb-sampled";
156
159
  EventCategory["DetectionEvent"] = "detection.event";
157
160
  EventCategory["SessionTrackNew"] = "session.track.new";
158
161
  EventCategory["SessionTrackExpired"] = "session.track.expired";
@@ -7252,6 +7255,24 @@ var RecordingRetentionSchema = object({
7252
7255
  maxSizeGb: number().min(0).optional()
7253
7256
  });
7254
7257
  /**
7258
+ * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
7259
+ * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
7260
+ * previews at. Five graduated steps; absent on a config = `standard` (the
7261
+ * shipped default, matching `sheet-geometry`/`sheet-composer`).
7262
+ *
7263
+ * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
7264
+ * Each window's index sidecar carries its own tile dims, so a camera whose
7265
+ * preset changed over time renders every historical window at the dims it was
7266
+ * written with.
7267
+ */
7268
+ var ScrubThumbnailPresetSchema = _enum([
7269
+ "minimal",
7270
+ "low",
7271
+ "standard",
7272
+ "high",
7273
+ "max"
7274
+ ]);
7275
+ /**
7255
7276
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
7256
7277
  *
7257
7278
  * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
@@ -7288,7 +7309,14 @@ var RecordingConfigSchema = object({
7288
7309
  * derived into bands once via `migrateConfigToBands`.
7289
7310
  */
7290
7311
  bands: array(RecordingBandSchema).optional(),
7291
- retention: RecordingRetentionSchema.optional()
7312
+ retention: RecordingRetentionSchema.optional(),
7313
+ /**
7314
+ * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
7315
+ * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
7316
+ * windows only — existing sheets are immutable, and each window's index
7317
+ * carries its own tile dims so mixed-preset history renders correctly.
7318
+ */
7319
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7292
7320
  });
7293
7321
  /**
7294
7322
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -10800,10 +10828,23 @@ var deviceDiscoveryCapability = {
10800
10828
  }
10801
10829
  };
10802
10830
  /**
10803
- * Doorbell button cap. Installed on a `DeviceType.Button` accessory
10804
- * with `role: DeviceRole.Doorbell`. Emits an `onPressed` event every
10805
- * time the firmware pushes a ring; status tracks the last press and
10806
- * a pressCount since start (diagnostic).
10831
+ * Doorbell button cap. Two kinds of providers coexist behind this cap
10832
+ * name (same pattern as `snapshot`):
10833
+ *
10834
+ * - **Native** providers: registered per-device by device-driver
10835
+ * addons via `ctx.registerNativeCap` — either on a
10836
+ * `DeviceType.Button` accessory with `role: DeviceRole.Doorbell`,
10837
+ * or directly on the camera (Reolink registers at camera level).
10838
+ * Emits an `onPressed` event every time the firmware pushes a
10839
+ * ring; status tracks the last press and a pressCount since start
10840
+ * (diagnostic).
10841
+ *
10842
+ * - **Wrapper** provider: the `virtual-doorbell` system builtin
10843
+ * (`@camstack/system/builtins/doorbell`). Turns ANY binary-ish
10844
+ * device (contact / switch / event-emitter …) into a doorbell for
10845
+ * a camera. `defaultActive: false` — the operator explicitly binds
10846
+ * it per camera in the device-bindings UI, then picks the source
10847
+ * device + trigger in the per-device settings.
10807
10848
  *
10808
10849
  * The DeviceEventPropagator re-emits `onPressed` on the camera parent
10809
10850
  * — subscribers listening at the camera level receive ring events
@@ -10824,7 +10865,10 @@ var doorbellCapability = {
10824
10865
  scope: "device",
10825
10866
  deviceNative: true,
10826
10867
  mode: "singleton",
10827
- deviceTypes: [DeviceType.Button],
10868
+ kind: "wrapper",
10869
+ defaultActive: false,
10870
+ deviceTypes: [DeviceType.Button, DeviceType.Camera],
10871
+ exposesDeviceSettings: true,
10828
10872
  methods: {},
10829
10873
  events: {
10830
10874
  /**
@@ -17193,6 +17237,14 @@ var ConfigEntrySchema = object({
17193
17237
  value: unknown(),
17194
17238
  description: string().optional()
17195
17239
  });
17240
+ var DeviceLinkModeSchema = _enum(["auto", "manual"]);
17241
+ /** One resolved linked device — the compact projection consumers need. */
17242
+ var LinkedDeviceSchema = object({
17243
+ deviceId: number(),
17244
+ name: string(),
17245
+ location: string().nullable(),
17246
+ features: array(string())
17247
+ });
17196
17248
  var SavedDeviceRowSchema = object({
17197
17249
  /** Numeric id reserved at allocateDeviceId time. */
17198
17250
  id: number(),
@@ -17414,7 +17466,10 @@ method(object({
17414
17466
  }), _void(), {
17415
17467
  kind: "mutation",
17416
17468
  auth: "admin"
17417
- }), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({ addonId: string().optional() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
17469
+ }), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({ addonId: string().optional() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
17470
+ mode: DeviceLinkModeSchema,
17471
+ devices: array(LinkedDeviceSchema)
17472
+ })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
17418
17473
  deviceId: number(),
17419
17474
  values: record(string(), unknown())
17420
17475
  }), object({ success: literal(true) }), {
@@ -18683,6 +18738,61 @@ var EventKindSchema = _enum([
18683
18738
  "object",
18684
18739
  "audio"
18685
18740
  ]);
18741
+ /**
18742
+ * Spatial filter for `listTracks` — the rect + polygon variants of the shared
18743
+ * MaskShape vocabulary (see `mask-shape.ts`). Coordinates are NORMALIZED 0..1
18744
+ * of the camera frame (top-left origin), matching the drawing-plane editor.
18745
+ */
18746
+ var TrackZoneFilterSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
18747
+ /** Closed icon vocabulary so clients render a known glyph per kind. */
18748
+ var EventKindIconSchema = _enum([
18749
+ "motion",
18750
+ "audio",
18751
+ "person",
18752
+ "vehicle",
18753
+ "animal",
18754
+ "door",
18755
+ "pir",
18756
+ "smoke",
18757
+ "water",
18758
+ "button",
18759
+ "generic"
18760
+ ]);
18761
+ var EventKindCategorySchema = _enum([
18762
+ "motion",
18763
+ "audio",
18764
+ "detection",
18765
+ "sensor",
18766
+ "custom"
18767
+ ]);
18768
+ var EventKindDescriptorSchema = object({
18769
+ /** Stable kind id (e.g. 'motion', 'person', 'contact'). */
18770
+ kind: string(),
18771
+ label: string(),
18772
+ /** Hex color for timeline/legend rendering. */
18773
+ color: string(),
18774
+ icon: EventKindIconSchema,
18775
+ category: EventKindCategorySchema,
18776
+ /** Which cap + device contributes this kind. For built-ins the camera
18777
+ * itself; for sensor kinds the LINKED source device. */
18778
+ source: object({
18779
+ capName: string(),
18780
+ deviceId: number()
18781
+ })
18782
+ });
18783
+ var SensorEventSchema = object({
18784
+ id: string(),
18785
+ /** The CAMERA the event is attributed to (a sensor linked to N cameras
18786
+ * yields N rows, one per camera). */
18787
+ deviceId: number(),
18788
+ /** The linked sensor device whose state changed. */
18789
+ sourceDeviceId: number(),
18790
+ /** Event kind id — matches an `EventKindDescriptor.kind`. */
18791
+ kind: string(),
18792
+ /** Snapshot of the sensor cap's runtime-state slice at the change. */
18793
+ value: record(string(), unknown()).nullable(),
18794
+ timestamp: number()
18795
+ });
18686
18796
  var TrackPositionSchema = object({
18687
18797
  x: number(),
18688
18798
  y: number(),
@@ -18696,6 +18806,30 @@ var TrackSnapshotSchema = object({
18696
18806
  mediaKey: string()
18697
18807
  });
18698
18808
  /**
18809
+ * Normalized 0..1 trajectory envelope (min/max over every position bbox,
18810
+ * divided by the track's detection-frame dims), computed at persist time.
18811
+ * Absent when the frame dims were unknown when the track was persisted
18812
+ * (legacy rows / dims-less sources) and on active (in-RAM) tracks.
18813
+ */
18814
+ var TrackEnvelopeSchema = object({
18815
+ minX: number(),
18816
+ minY: number(),
18817
+ maxX: number(),
18818
+ maxY: number()
18819
+ });
18820
+ /**
18821
+ * Row projection for track list queries. `full` (default) returns the
18822
+ * complete Track including the frame-rate `positions[]` history and the
18823
+ * `snapshots[]` references — megabytes across a page of tracks. `slim`
18824
+ * keeps every scalar the list surfaces actually render (ids, class(es),
18825
+ * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
18826
+ * zonesVisited, bestEventId, envelope) and returns `positions` /
18827
+ * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
18828
+ * `getTrack`. Mirrors the event-store `projection` convention
18829
+ * (`getObjectEvents` et al.).
18830
+ */
18831
+ var TrackProjectionSchema = _enum(["full", "slim"]);
18832
+ /**
18699
18833
  * One audio-classification label heard on the track's camera while the
18700
18834
  * track was alive, aggregated per label. An "episode" is one persisted
18701
18835
  * audio event (the confident-classification path: score ≥ the device's
@@ -18746,7 +18880,11 @@ var TrackSchema = object({
18746
18880
  /** Audio-classification labels heard on the camera during the track's
18747
18881
  * life (score ≥ device `classificationMinScore`), aggregated per label.
18748
18882
  * Absent on legacy rows / tracks with no confident audio. */
18749
- audioLabels: array(TrackAudioLabelSchema).readonly().optional()
18883
+ audioLabels: array(TrackAudioLabelSchema).readonly().optional(),
18884
+ /** Normalized 0..1 trajectory envelope (see {@link TrackEnvelopeSchema}).
18885
+ * Populated from the persisted envelope columns on historical reads;
18886
+ * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
18887
+ envelope: TrackEnvelopeSchema.optional()
18750
18888
  });
18751
18889
  var BaseEventFields = {
18752
18890
  id: string(),
@@ -18856,11 +18994,13 @@ var MediaFileSchema = object({
18856
18994
  sizeBytes: number(),
18857
18995
  timestamp: number()
18858
18996
  });
18997
+ var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
18998
+ var MAX_EVENT_QUERY_LIMIT = 5e3;
18859
18999
  var DeviceEventQueryInput = object({
18860
19000
  deviceId: number(),
18861
19001
  since: number().optional(),
18862
19002
  until: number().optional(),
18863
- limit: number().int().min(1).max(5e3).default(1e3),
19003
+ limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT),
18864
19004
  /** `slim` drops heavy JSON fields (regions/bbox/zones) and carries an
18865
19005
  * optional `mediaUrl` (populated by B5). `full` (default) keeps today's
18866
19006
  * exact behaviour. Callers may omit this field — the store defaults to
@@ -18868,6 +19008,27 @@ var DeviceEventQueryInput = object({
18868
19008
  projection: _enum(["full", "slim"]).optional()
18869
19009
  });
18870
19010
  var ObjectEventQueryInput = DeviceEventQueryInput.extend({ classFilter: string().optional() });
19011
+ var RecentTracksQueryInput = object({
19012
+ /** Devices to merge. An empty array yields `{ tracks: [], nextCursor: null }`. */
19013
+ deviceIds: array(number()),
19014
+ /** Window lower bound on `lastSeen` (inclusive). */
19015
+ since: number().optional(),
19016
+ /** Window upper bound on `lastSeen` (inclusive). */
19017
+ until: number().optional(),
19018
+ /** Page size. Default 200, max 1000. */
19019
+ limit: number().int().min(1).max(1e3).default(200),
19020
+ /** Opaque continuation cursor from a previous page's `nextCursor`.
19021
+ * Encodes the (lastSeen, trackId) sort position — treat as opaque. */
19022
+ cursor: string().optional(),
19023
+ /** See {@link TrackProjectionSchema}. Default `full`. */
19024
+ projection: TrackProjectionSchema.optional()
19025
+ });
19026
+ var RecentTracksPageSchema = object({
19027
+ /** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
19028
+ tracks: array(TrackSchema).readonly(),
19029
+ /** Cursor for the next page, or null when this page is the last. */
19030
+ nextCursor: string().nullable()
19031
+ });
18871
19032
  var KeyEventQueryInput = object({
18872
19033
  deviceId: number(),
18873
19034
  /** Window lower bound (track firstSeen ≥ since). */
@@ -18950,11 +19111,25 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18950
19111
  deviceId: number(),
18951
19112
  since: number().optional(),
18952
19113
  until: number().optional(),
18953
- limit: number().optional()
18954
- }), array(TrackSchema).readonly()), method(object({ deviceId: number() }), _void(), {
19114
+ limit: number().optional(),
19115
+ /** Spatial filter only tracks whose trajectory intersects the zone
19116
+ * (normalized 0..1 rect or polygon). SQL-prefiltered on the persisted
19117
+ * envelope columns, then precisely tested per position. Tracks with
19118
+ * an unknown envelope (no frame dims at persist time) always match. */
19119
+ zone: TrackZoneFilterSchema.optional(),
19120
+ /** See {@link TrackProjectionSchema}. Default `full` (backward
19121
+ * compatible — omitting the field keeps today's exact behaviour). */
19122
+ projection: TrackProjectionSchema.optional()
19123
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
18955
19124
  kind: "mutation",
18956
19125
  auth: "admin"
18957
- }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19126
+ }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({
19127
+ deviceId: number(),
19128
+ since: number().optional(),
19129
+ until: number().optional(),
19130
+ kinds: array(string()).optional(),
19131
+ limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
19132
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
18958
19133
  deviceId: number(),
18959
19134
  since: number(),
18960
19135
  until: number(),
@@ -22155,7 +22330,12 @@ var RecordingStorageUsageSchema = object({
22155
22330
  /**
22156
22331
  * Result of locating footage at a wall-clock instant for one device/profile.
22157
22332
  * `segment` carries the covering segment's window; `gap` reports the forward
22158
- * nearest covered edge (`null` past the end of footage / when none exists).
22333
+ * nearest covered edge (`null` past the end of footage / when none exists)
22334
+ * and the backward covered edge `prevEndMs` (exclusive end of the nearest
22335
+ * footage behind the epoch; `null` when none — optional so older providers
22336
+ * that omit it stay valid). `prevEndMs` lets a backward frame-step hop the
22337
+ * small inter-segment cracks (durMs under-covers the span to the next
22338
+ * startMs by ~11-17 ms) instead of no-opping at a segment head.
22159
22339
  */
22160
22340
  var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
22161
22341
  kind: literal("segment"),
@@ -22164,7 +22344,8 @@ var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
22164
22344
  bytes: number()
22165
22345
  }), object({
22166
22346
  kind: literal("gap"),
22167
- nearestEdgeMs: number().nullable()
22347
+ nearestEdgeMs: number().nullable(),
22348
+ prevEndMs: number().nullable().optional()
22168
22349
  })]);
22169
22350
  /** Raw bytes of one finalized footage segment (read off disk on the recording node). */
22170
22351
  var ReadSegmentBytesResultSchema = object({ data: _instanceof(Uint8Array) });
@@ -23733,6 +23914,12 @@ Object.freeze({
23733
23914
  addonId: null,
23734
23915
  access: "view"
23735
23916
  },
23917
+ "deviceManager.getLinkedDevices": {
23918
+ capName: "device-manager",
23919
+ capScope: "system",
23920
+ addonId: null,
23921
+ access: "view"
23922
+ },
23736
23923
  "deviceManager.getRoleDisplayDefaults": {
23737
23924
  capName: "device-manager",
23738
23925
  capScope: "system",
@@ -25287,6 +25474,12 @@ Object.freeze({
25287
25474
  addonId: null,
25288
25475
  access: "view"
25289
25476
  },
25477
+ "pipelineAnalytics.getSensorEvents": {
25478
+ capName: "pipeline-analytics",
25479
+ capScope: "device",
25480
+ addonId: null,
25481
+ access: "view"
25482
+ },
25290
25483
  "pipelineAnalytics.getTrack": {
25291
25484
  capName: "pipeline-analytics",
25292
25485
  capScope: "device",
@@ -25299,6 +25492,18 @@ Object.freeze({
25299
25492
  addonId: null,
25300
25493
  access: "view"
25301
25494
  },
25495
+ "pipelineAnalytics.listEventKinds": {
25496
+ capName: "pipeline-analytics",
25497
+ capScope: "device",
25498
+ addonId: null,
25499
+ access: "view"
25500
+ },
25501
+ "pipelineAnalytics.listRecentTracks": {
25502
+ capName: "pipeline-analytics",
25503
+ capScope: "device",
25504
+ addonId: null,
25505
+ access: "view"
25506
+ },
25302
25507
  "pipelineAnalytics.listTracks": {
25303
25508
  capName: "pipeline-analytics",
25304
25509
  capScope: "device",
package/dist/addon.mjs CHANGED
@@ -2,7 +2,7 @@ import { createCipheriv, createDecipheriv, createHash, createHmac } from "crypto
2
2
  import { Socket } from "net";
3
3
  import { EventEmitter } from "events";
4
4
  import { createSocket } from "dgram";
5
- //#region ../types/dist/event-category-CFZs3jI4.mjs
5
+ //#region ../types/dist/event-category-H4AVePnn.mjs
6
6
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
7
7
  EventCategory["SystemBoot"] = "system.boot";
8
8
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -152,6 +152,9 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
152
152
  EventCategory["RecordingSegmentWritten"] = "recording.segment.written";
153
153
  EventCategory["RecordingPolicyFallback"] = "recording.policy.fallback";
154
154
  EventCategory["RecordingRetentionCompleted"] = "recording.retention.completed";
155
+ /** Runner-sampled scrub thumbnail (~1/5 s/camera). Telemetry (D8): a lost
156
+ * thumb is a scrub gap the recorder's keyframe backfill covers. */
157
+ EventCategory["RecordingThumbSampled"] = "recording.thumb-sampled";
155
158
  EventCategory["DetectionEvent"] = "detection.event";
156
159
  EventCategory["SessionTrackNew"] = "session.track.new";
157
160
  EventCategory["SessionTrackExpired"] = "session.track.expired";
@@ -7251,6 +7254,24 @@ var RecordingRetentionSchema = object({
7251
7254
  maxSizeGb: number().min(0).optional()
7252
7255
  });
7253
7256
  /**
7257
+ * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
7258
+ * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
7259
+ * previews at. Five graduated steps; absent on a config = `standard` (the
7260
+ * shipped default, matching `sheet-geometry`/`sheet-composer`).
7261
+ *
7262
+ * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
7263
+ * Each window's index sidecar carries its own tile dims, so a camera whose
7264
+ * preset changed over time renders every historical window at the dims it was
7265
+ * written with.
7266
+ */
7267
+ var ScrubThumbnailPresetSchema = _enum([
7268
+ "minimal",
7269
+ "low",
7270
+ "standard",
7271
+ "high",
7272
+ "max"
7273
+ ]);
7274
+ /**
7254
7275
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
7255
7276
  *
7256
7277
  * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
@@ -7287,7 +7308,14 @@ var RecordingConfigSchema = object({
7287
7308
  * derived into bands once via `migrateConfigToBands`.
7288
7309
  */
7289
7310
  bands: array(RecordingBandSchema).optional(),
7290
- retention: RecordingRetentionSchema.optional()
7311
+ retention: RecordingRetentionSchema.optional(),
7312
+ /**
7313
+ * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
7314
+ * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
7315
+ * windows only — existing sheets are immutable, and each window's index
7316
+ * carries its own tile dims so mixed-preset history renders correctly.
7317
+ */
7318
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7291
7319
  });
7292
7320
  /**
7293
7321
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -10799,10 +10827,23 @@ var deviceDiscoveryCapability = {
10799
10827
  }
10800
10828
  };
10801
10829
  /**
10802
- * Doorbell button cap. Installed on a `DeviceType.Button` accessory
10803
- * with `role: DeviceRole.Doorbell`. Emits an `onPressed` event every
10804
- * time the firmware pushes a ring; status tracks the last press and
10805
- * a pressCount since start (diagnostic).
10830
+ * Doorbell button cap. Two kinds of providers coexist behind this cap
10831
+ * name (same pattern as `snapshot`):
10832
+ *
10833
+ * - **Native** providers: registered per-device by device-driver
10834
+ * addons via `ctx.registerNativeCap` — either on a
10835
+ * `DeviceType.Button` accessory with `role: DeviceRole.Doorbell`,
10836
+ * or directly on the camera (Reolink registers at camera level).
10837
+ * Emits an `onPressed` event every time the firmware pushes a
10838
+ * ring; status tracks the last press and a pressCount since start
10839
+ * (diagnostic).
10840
+ *
10841
+ * - **Wrapper** provider: the `virtual-doorbell` system builtin
10842
+ * (`@camstack/system/builtins/doorbell`). Turns ANY binary-ish
10843
+ * device (contact / switch / event-emitter …) into a doorbell for
10844
+ * a camera. `defaultActive: false` — the operator explicitly binds
10845
+ * it per camera in the device-bindings UI, then picks the source
10846
+ * device + trigger in the per-device settings.
10806
10847
  *
10807
10848
  * The DeviceEventPropagator re-emits `onPressed` on the camera parent
10808
10849
  * — subscribers listening at the camera level receive ring events
@@ -10823,7 +10864,10 @@ var doorbellCapability = {
10823
10864
  scope: "device",
10824
10865
  deviceNative: true,
10825
10866
  mode: "singleton",
10826
- deviceTypes: [DeviceType.Button],
10867
+ kind: "wrapper",
10868
+ defaultActive: false,
10869
+ deviceTypes: [DeviceType.Button, DeviceType.Camera],
10870
+ exposesDeviceSettings: true,
10827
10871
  methods: {},
10828
10872
  events: {
10829
10873
  /**
@@ -17192,6 +17236,14 @@ var ConfigEntrySchema = object({
17192
17236
  value: unknown(),
17193
17237
  description: string().optional()
17194
17238
  });
17239
+ var DeviceLinkModeSchema = _enum(["auto", "manual"]);
17240
+ /** One resolved linked device — the compact projection consumers need. */
17241
+ var LinkedDeviceSchema = object({
17242
+ deviceId: number(),
17243
+ name: string(),
17244
+ location: string().nullable(),
17245
+ features: array(string())
17246
+ });
17195
17247
  var SavedDeviceRowSchema = object({
17196
17248
  /** Numeric id reserved at allocateDeviceId time. */
17197
17249
  id: number(),
@@ -17413,7 +17465,10 @@ method(object({
17413
17465
  }), _void(), {
17414
17466
  kind: "mutation",
17415
17467
  auth: "admin"
17416
- }), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({ addonId: string().optional() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
17468
+ }), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({ addonId: string().optional() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
17469
+ mode: DeviceLinkModeSchema,
17470
+ devices: array(LinkedDeviceSchema)
17471
+ })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
17417
17472
  deviceId: number(),
17418
17473
  values: record(string(), unknown())
17419
17474
  }), object({ success: literal(true) }), {
@@ -18682,6 +18737,61 @@ var EventKindSchema = _enum([
18682
18737
  "object",
18683
18738
  "audio"
18684
18739
  ]);
18740
+ /**
18741
+ * Spatial filter for `listTracks` — the rect + polygon variants of the shared
18742
+ * MaskShape vocabulary (see `mask-shape.ts`). Coordinates are NORMALIZED 0..1
18743
+ * of the camera frame (top-left origin), matching the drawing-plane editor.
18744
+ */
18745
+ var TrackZoneFilterSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
18746
+ /** Closed icon vocabulary so clients render a known glyph per kind. */
18747
+ var EventKindIconSchema = _enum([
18748
+ "motion",
18749
+ "audio",
18750
+ "person",
18751
+ "vehicle",
18752
+ "animal",
18753
+ "door",
18754
+ "pir",
18755
+ "smoke",
18756
+ "water",
18757
+ "button",
18758
+ "generic"
18759
+ ]);
18760
+ var EventKindCategorySchema = _enum([
18761
+ "motion",
18762
+ "audio",
18763
+ "detection",
18764
+ "sensor",
18765
+ "custom"
18766
+ ]);
18767
+ var EventKindDescriptorSchema = object({
18768
+ /** Stable kind id (e.g. 'motion', 'person', 'contact'). */
18769
+ kind: string(),
18770
+ label: string(),
18771
+ /** Hex color for timeline/legend rendering. */
18772
+ color: string(),
18773
+ icon: EventKindIconSchema,
18774
+ category: EventKindCategorySchema,
18775
+ /** Which cap + device contributes this kind. For built-ins the camera
18776
+ * itself; for sensor kinds the LINKED source device. */
18777
+ source: object({
18778
+ capName: string(),
18779
+ deviceId: number()
18780
+ })
18781
+ });
18782
+ var SensorEventSchema = object({
18783
+ id: string(),
18784
+ /** The CAMERA the event is attributed to (a sensor linked to N cameras
18785
+ * yields N rows, one per camera). */
18786
+ deviceId: number(),
18787
+ /** The linked sensor device whose state changed. */
18788
+ sourceDeviceId: number(),
18789
+ /** Event kind id — matches an `EventKindDescriptor.kind`. */
18790
+ kind: string(),
18791
+ /** Snapshot of the sensor cap's runtime-state slice at the change. */
18792
+ value: record(string(), unknown()).nullable(),
18793
+ timestamp: number()
18794
+ });
18685
18795
  var TrackPositionSchema = object({
18686
18796
  x: number(),
18687
18797
  y: number(),
@@ -18695,6 +18805,30 @@ var TrackSnapshotSchema = object({
18695
18805
  mediaKey: string()
18696
18806
  });
18697
18807
  /**
18808
+ * Normalized 0..1 trajectory envelope (min/max over every position bbox,
18809
+ * divided by the track's detection-frame dims), computed at persist time.
18810
+ * Absent when the frame dims were unknown when the track was persisted
18811
+ * (legacy rows / dims-less sources) and on active (in-RAM) tracks.
18812
+ */
18813
+ var TrackEnvelopeSchema = object({
18814
+ minX: number(),
18815
+ minY: number(),
18816
+ maxX: number(),
18817
+ maxY: number()
18818
+ });
18819
+ /**
18820
+ * Row projection for track list queries. `full` (default) returns the
18821
+ * complete Track including the frame-rate `positions[]` history and the
18822
+ * `snapshots[]` references — megabytes across a page of tracks. `slim`
18823
+ * keeps every scalar the list surfaces actually render (ids, class(es),
18824
+ * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
18825
+ * zonesVisited, bestEventId, envelope) and returns `positions` /
18826
+ * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
18827
+ * `getTrack`. Mirrors the event-store `projection` convention
18828
+ * (`getObjectEvents` et al.).
18829
+ */
18830
+ var TrackProjectionSchema = _enum(["full", "slim"]);
18831
+ /**
18698
18832
  * One audio-classification label heard on the track's camera while the
18699
18833
  * track was alive, aggregated per label. An "episode" is one persisted
18700
18834
  * audio event (the confident-classification path: score ≥ the device's
@@ -18745,7 +18879,11 @@ var TrackSchema = object({
18745
18879
  /** Audio-classification labels heard on the camera during the track's
18746
18880
  * life (score ≥ device `classificationMinScore`), aggregated per label.
18747
18881
  * Absent on legacy rows / tracks with no confident audio. */
18748
- audioLabels: array(TrackAudioLabelSchema).readonly().optional()
18882
+ audioLabels: array(TrackAudioLabelSchema).readonly().optional(),
18883
+ /** Normalized 0..1 trajectory envelope (see {@link TrackEnvelopeSchema}).
18884
+ * Populated from the persisted envelope columns on historical reads;
18885
+ * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
18886
+ envelope: TrackEnvelopeSchema.optional()
18749
18887
  });
18750
18888
  var BaseEventFields = {
18751
18889
  id: string(),
@@ -18855,11 +18993,13 @@ var MediaFileSchema = object({
18855
18993
  sizeBytes: number(),
18856
18994
  timestamp: number()
18857
18995
  });
18996
+ var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
18997
+ var MAX_EVENT_QUERY_LIMIT = 5e3;
18858
18998
  var DeviceEventQueryInput = object({
18859
18999
  deviceId: number(),
18860
19000
  since: number().optional(),
18861
19001
  until: number().optional(),
18862
- limit: number().int().min(1).max(5e3).default(1e3),
19002
+ limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT),
18863
19003
  /** `slim` drops heavy JSON fields (regions/bbox/zones) and carries an
18864
19004
  * optional `mediaUrl` (populated by B5). `full` (default) keeps today's
18865
19005
  * exact behaviour. Callers may omit this field — the store defaults to
@@ -18867,6 +19007,27 @@ var DeviceEventQueryInput = object({
18867
19007
  projection: _enum(["full", "slim"]).optional()
18868
19008
  });
18869
19009
  var ObjectEventQueryInput = DeviceEventQueryInput.extend({ classFilter: string().optional() });
19010
+ var RecentTracksQueryInput = object({
19011
+ /** Devices to merge. An empty array yields `{ tracks: [], nextCursor: null }`. */
19012
+ deviceIds: array(number()),
19013
+ /** Window lower bound on `lastSeen` (inclusive). */
19014
+ since: number().optional(),
19015
+ /** Window upper bound on `lastSeen` (inclusive). */
19016
+ until: number().optional(),
19017
+ /** Page size. Default 200, max 1000. */
19018
+ limit: number().int().min(1).max(1e3).default(200),
19019
+ /** Opaque continuation cursor from a previous page's `nextCursor`.
19020
+ * Encodes the (lastSeen, trackId) sort position — treat as opaque. */
19021
+ cursor: string().optional(),
19022
+ /** See {@link TrackProjectionSchema}. Default `full`. */
19023
+ projection: TrackProjectionSchema.optional()
19024
+ });
19025
+ var RecentTracksPageSchema = object({
19026
+ /** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
19027
+ tracks: array(TrackSchema).readonly(),
19028
+ /** Cursor for the next page, or null when this page is the last. */
19029
+ nextCursor: string().nullable()
19030
+ });
18870
19031
  var KeyEventQueryInput = object({
18871
19032
  deviceId: number(),
18872
19033
  /** Window lower bound (track firstSeen ≥ since). */
@@ -18949,11 +19110,25 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18949
19110
  deviceId: number(),
18950
19111
  since: number().optional(),
18951
19112
  until: number().optional(),
18952
- limit: number().optional()
18953
- }), array(TrackSchema).readonly()), method(object({ deviceId: number() }), _void(), {
19113
+ limit: number().optional(),
19114
+ /** Spatial filter only tracks whose trajectory intersects the zone
19115
+ * (normalized 0..1 rect or polygon). SQL-prefiltered on the persisted
19116
+ * envelope columns, then precisely tested per position. Tracks with
19117
+ * an unknown envelope (no frame dims at persist time) always match. */
19118
+ zone: TrackZoneFilterSchema.optional(),
19119
+ /** See {@link TrackProjectionSchema}. Default `full` (backward
19120
+ * compatible — omitting the field keeps today's exact behaviour). */
19121
+ projection: TrackProjectionSchema.optional()
19122
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
18954
19123
  kind: "mutation",
18955
19124
  auth: "admin"
18956
- }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19125
+ }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({
19126
+ deviceId: number(),
19127
+ since: number().optional(),
19128
+ until: number().optional(),
19129
+ kinds: array(string()).optional(),
19130
+ limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
19131
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
18957
19132
  deviceId: number(),
18958
19133
  since: number(),
18959
19134
  until: number(),
@@ -22154,7 +22329,12 @@ var RecordingStorageUsageSchema = object({
22154
22329
  /**
22155
22330
  * Result of locating footage at a wall-clock instant for one device/profile.
22156
22331
  * `segment` carries the covering segment's window; `gap` reports the forward
22157
- * nearest covered edge (`null` past the end of footage / when none exists).
22332
+ * nearest covered edge (`null` past the end of footage / when none exists)
22333
+ * and the backward covered edge `prevEndMs` (exclusive end of the nearest
22334
+ * footage behind the epoch; `null` when none — optional so older providers
22335
+ * that omit it stay valid). `prevEndMs` lets a backward frame-step hop the
22336
+ * small inter-segment cracks (durMs under-covers the span to the next
22337
+ * startMs by ~11-17 ms) instead of no-opping at a segment head.
22158
22338
  */
22159
22339
  var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
22160
22340
  kind: literal("segment"),
@@ -22163,7 +22343,8 @@ var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
22163
22343
  bytes: number()
22164
22344
  }), object({
22165
22345
  kind: literal("gap"),
22166
- nearestEdgeMs: number().nullable()
22346
+ nearestEdgeMs: number().nullable(),
22347
+ prevEndMs: number().nullable().optional()
22167
22348
  })]);
22168
22349
  /** Raw bytes of one finalized footage segment (read off disk on the recording node). */
22169
22350
  var ReadSegmentBytesResultSchema = object({ data: _instanceof(Uint8Array) });
@@ -23732,6 +23913,12 @@ Object.freeze({
23732
23913
  addonId: null,
23733
23914
  access: "view"
23734
23915
  },
23916
+ "deviceManager.getLinkedDevices": {
23917
+ capName: "device-manager",
23918
+ capScope: "system",
23919
+ addonId: null,
23920
+ access: "view"
23921
+ },
23735
23922
  "deviceManager.getRoleDisplayDefaults": {
23736
23923
  capName: "device-manager",
23737
23924
  capScope: "system",
@@ -25286,6 +25473,12 @@ Object.freeze({
25286
25473
  addonId: null,
25287
25474
  access: "view"
25288
25475
  },
25476
+ "pipelineAnalytics.getSensorEvents": {
25477
+ capName: "pipeline-analytics",
25478
+ capScope: "device",
25479
+ addonId: null,
25480
+ access: "view"
25481
+ },
25289
25482
  "pipelineAnalytics.getTrack": {
25290
25483
  capName: "pipeline-analytics",
25291
25484
  capScope: "device",
@@ -25298,6 +25491,18 @@ Object.freeze({
25298
25491
  addonId: null,
25299
25492
  access: "view"
25300
25493
  },
25494
+ "pipelineAnalytics.listEventKinds": {
25495
+ capName: "pipeline-analytics",
25496
+ capScope: "device",
25497
+ addonId: null,
25498
+ access: "view"
25499
+ },
25500
+ "pipelineAnalytics.listRecentTracks": {
25501
+ capName: "pipeline-analytics",
25502
+ capScope: "device",
25503
+ addonId: null,
25504
+ access: "view"
25505
+ },
25301
25506
  "pipelineAnalytics.listTracks": {
25302
25507
  capName: "pipeline-analytics",
25303
25508
  capScope: "device",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-tuya",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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",