@camstack/addon-provider-hikvision 1.1.24 → 1.1.26

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 +4 -7
package/dist/addon.js CHANGED
@@ -6,7 +6,7 @@ let node_http = require("node:http");
6
6
  let node_https = require("node:https");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_os = require("node:os");
9
- //#region ../types/dist/event-category-CFZs3jI4.mjs
9
+ //#region ../types/dist/event-category-H4AVePnn.mjs
10
10
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
11
11
  EventCategory["SystemBoot"] = "system.boot";
12
12
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -156,6 +156,9 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
156
156
  EventCategory["RecordingSegmentWritten"] = "recording.segment.written";
157
157
  EventCategory["RecordingPolicyFallback"] = "recording.policy.fallback";
158
158
  EventCategory["RecordingRetentionCompleted"] = "recording.retention.completed";
159
+ /** Runner-sampled scrub thumbnail (~1/5 s/camera). Telemetry (D8): a lost
160
+ * thumb is a scrub gap the recorder's keyframe backfill covers. */
161
+ EventCategory["RecordingThumbSampled"] = "recording.thumb-sampled";
159
162
  EventCategory["DetectionEvent"] = "detection.event";
160
163
  EventCategory["SessionTrackNew"] = "session.track.new";
161
164
  EventCategory["SessionTrackExpired"] = "session.track.expired";
@@ -7404,6 +7407,24 @@ var RecordingRetentionSchema = object({
7404
7407
  maxSizeGb: number().min(0).optional()
7405
7408
  });
7406
7409
  /**
7410
+ * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
7411
+ * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
7412
+ * previews at. Five graduated steps; absent on a config = `standard` (the
7413
+ * shipped default, matching `sheet-geometry`/`sheet-composer`).
7414
+ *
7415
+ * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
7416
+ * Each window's index sidecar carries its own tile dims, so a camera whose
7417
+ * preset changed over time renders every historical window at the dims it was
7418
+ * written with.
7419
+ */
7420
+ var ScrubThumbnailPresetSchema = _enum([
7421
+ "minimal",
7422
+ "low",
7423
+ "standard",
7424
+ "high",
7425
+ "max"
7426
+ ]);
7427
+ /**
7407
7428
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
7408
7429
  *
7409
7430
  * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
@@ -7440,7 +7461,14 @@ var RecordingConfigSchema = object({
7440
7461
  * derived into bands once via `migrateConfigToBands`.
7441
7462
  */
7442
7463
  bands: array(RecordingBandSchema).optional(),
7443
- retention: RecordingRetentionSchema.optional()
7464
+ retention: RecordingRetentionSchema.optional(),
7465
+ /**
7466
+ * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
7467
+ * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
7468
+ * windows only — existing sheets are immutable, and each window's index
7469
+ * carries its own tile dims so mixed-preset history renders correctly.
7470
+ */
7471
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7444
7472
  });
7445
7473
  /**
7446
7474
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -11057,10 +11085,23 @@ var deviceDiscoveryCapability = {
11057
11085
  }
11058
11086
  };
11059
11087
  /**
11060
- * Doorbell button cap. Installed on a `DeviceType.Button` accessory
11061
- * with `role: DeviceRole.Doorbell`. Emits an `onPressed` event every
11062
- * time the firmware pushes a ring; status tracks the last press and
11063
- * a pressCount since start (diagnostic).
11088
+ * Doorbell button cap. Two kinds of providers coexist behind this cap
11089
+ * name (same pattern as `snapshot`):
11090
+ *
11091
+ * - **Native** providers: registered per-device by device-driver
11092
+ * addons via `ctx.registerNativeCap` — either on a
11093
+ * `DeviceType.Button` accessory with `role: DeviceRole.Doorbell`,
11094
+ * or directly on the camera (Reolink registers at camera level).
11095
+ * Emits an `onPressed` event every time the firmware pushes a
11096
+ * ring; status tracks the last press and a pressCount since start
11097
+ * (diagnostic).
11098
+ *
11099
+ * - **Wrapper** provider: the `virtual-doorbell` system builtin
11100
+ * (`@camstack/system/builtins/doorbell`). Turns ANY binary-ish
11101
+ * device (contact / switch / event-emitter …) into a doorbell for
11102
+ * a camera. `defaultActive: false` — the operator explicitly binds
11103
+ * it per camera in the device-bindings UI, then picks the source
11104
+ * device + trigger in the per-device settings.
11064
11105
  *
11065
11106
  * The DeviceEventPropagator re-emits `onPressed` on the camera parent
11066
11107
  * — subscribers listening at the camera level receive ring events
@@ -11081,7 +11122,10 @@ var doorbellCapability = {
11081
11122
  scope: "device",
11082
11123
  deviceNative: true,
11083
11124
  mode: "singleton",
11084
- deviceTypes: [DeviceType.Button],
11125
+ kind: "wrapper",
11126
+ defaultActive: false,
11127
+ deviceTypes: [DeviceType.Button, DeviceType.Camera],
11128
+ exposesDeviceSettings: true,
11085
11129
  methods: {},
11086
11130
  events: {
11087
11131
  /**
@@ -17445,6 +17489,14 @@ var ConfigEntrySchema = object({
17445
17489
  value: unknown(),
17446
17490
  description: string().optional()
17447
17491
  });
17492
+ var DeviceLinkModeSchema = _enum(["auto", "manual"]);
17493
+ /** One resolved linked device — the compact projection consumers need. */
17494
+ var LinkedDeviceSchema = object({
17495
+ deviceId: number(),
17496
+ name: string(),
17497
+ location: string().nullable(),
17498
+ features: array(string())
17499
+ });
17448
17500
  var SavedDeviceRowSchema = object({
17449
17501
  /** Numeric id reserved at allocateDeviceId time. */
17450
17502
  id: number(),
@@ -17666,7 +17718,10 @@ method(object({
17666
17718
  }), _void(), {
17667
17719
  kind: "mutation",
17668
17720
  auth: "admin"
17669
- }), 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({
17721
+ }), 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({
17722
+ mode: DeviceLinkModeSchema,
17723
+ devices: array(LinkedDeviceSchema)
17724
+ })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
17670
17725
  deviceId: number(),
17671
17726
  values: record(string(), unknown())
17672
17727
  }), object({ success: literal(true) }), {
@@ -18935,6 +18990,61 @@ var EventKindSchema = _enum([
18935
18990
  "object",
18936
18991
  "audio"
18937
18992
  ]);
18993
+ /**
18994
+ * Spatial filter for `listTracks` — the rect + polygon variants of the shared
18995
+ * MaskShape vocabulary (see `mask-shape.ts`). Coordinates are NORMALIZED 0..1
18996
+ * of the camera frame (top-left origin), matching the drawing-plane editor.
18997
+ */
18998
+ var TrackZoneFilterSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
18999
+ /** Closed icon vocabulary so clients render a known glyph per kind. */
19000
+ var EventKindIconSchema = _enum([
19001
+ "motion",
19002
+ "audio",
19003
+ "person",
19004
+ "vehicle",
19005
+ "animal",
19006
+ "door",
19007
+ "pir",
19008
+ "smoke",
19009
+ "water",
19010
+ "button",
19011
+ "generic"
19012
+ ]);
19013
+ var EventKindCategorySchema = _enum([
19014
+ "motion",
19015
+ "audio",
19016
+ "detection",
19017
+ "sensor",
19018
+ "custom"
19019
+ ]);
19020
+ var EventKindDescriptorSchema = object({
19021
+ /** Stable kind id (e.g. 'motion', 'person', 'contact'). */
19022
+ kind: string(),
19023
+ label: string(),
19024
+ /** Hex color for timeline/legend rendering. */
19025
+ color: string(),
19026
+ icon: EventKindIconSchema,
19027
+ category: EventKindCategorySchema,
19028
+ /** Which cap + device contributes this kind. For built-ins the camera
19029
+ * itself; for sensor kinds the LINKED source device. */
19030
+ source: object({
19031
+ capName: string(),
19032
+ deviceId: number()
19033
+ })
19034
+ });
19035
+ var SensorEventSchema = object({
19036
+ id: string(),
19037
+ /** The CAMERA the event is attributed to (a sensor linked to N cameras
19038
+ * yields N rows, one per camera). */
19039
+ deviceId: number(),
19040
+ /** The linked sensor device whose state changed. */
19041
+ sourceDeviceId: number(),
19042
+ /** Event kind id — matches an `EventKindDescriptor.kind`. */
19043
+ kind: string(),
19044
+ /** Snapshot of the sensor cap's runtime-state slice at the change. */
19045
+ value: record(string(), unknown()).nullable(),
19046
+ timestamp: number()
19047
+ });
18938
19048
  var TrackPositionSchema = object({
18939
19049
  x: number(),
18940
19050
  y: number(),
@@ -18948,6 +19058,30 @@ var TrackSnapshotSchema = object({
18948
19058
  mediaKey: string()
18949
19059
  });
18950
19060
  /**
19061
+ * Normalized 0..1 trajectory envelope (min/max over every position bbox,
19062
+ * divided by the track's detection-frame dims), computed at persist time.
19063
+ * Absent when the frame dims were unknown when the track was persisted
19064
+ * (legacy rows / dims-less sources) and on active (in-RAM) tracks.
19065
+ */
19066
+ var TrackEnvelopeSchema = object({
19067
+ minX: number(),
19068
+ minY: number(),
19069
+ maxX: number(),
19070
+ maxY: number()
19071
+ });
19072
+ /**
19073
+ * Row projection for track list queries. `full` (default) returns the
19074
+ * complete Track including the frame-rate `positions[]` history and the
19075
+ * `snapshots[]` references — megabytes across a page of tracks. `slim`
19076
+ * keeps every scalar the list surfaces actually render (ids, class(es),
19077
+ * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
19078
+ * zonesVisited, bestEventId, envelope) and returns `positions` /
19079
+ * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
19080
+ * `getTrack`. Mirrors the event-store `projection` convention
19081
+ * (`getObjectEvents` et al.).
19082
+ */
19083
+ var TrackProjectionSchema = _enum(["full", "slim"]);
19084
+ /**
18951
19085
  * One audio-classification label heard on the track's camera while the
18952
19086
  * track was alive, aggregated per label. An "episode" is one persisted
18953
19087
  * audio event (the confident-classification path: score ≥ the device's
@@ -18998,7 +19132,11 @@ var TrackSchema = object({
18998
19132
  /** Audio-classification labels heard on the camera during the track's
18999
19133
  * life (score ≥ device `classificationMinScore`), aggregated per label.
19000
19134
  * Absent on legacy rows / tracks with no confident audio. */
19001
- audioLabels: array(TrackAudioLabelSchema).readonly().optional()
19135
+ audioLabels: array(TrackAudioLabelSchema).readonly().optional(),
19136
+ /** Normalized 0..1 trajectory envelope (see {@link TrackEnvelopeSchema}).
19137
+ * Populated from the persisted envelope columns on historical reads;
19138
+ * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
19139
+ envelope: TrackEnvelopeSchema.optional()
19002
19140
  });
19003
19141
  var BaseEventFields = {
19004
19142
  id: string(),
@@ -19108,11 +19246,13 @@ var MediaFileSchema = object({
19108
19246
  sizeBytes: number(),
19109
19247
  timestamp: number()
19110
19248
  });
19249
+ var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
19250
+ var MAX_EVENT_QUERY_LIMIT = 5e3;
19111
19251
  var DeviceEventQueryInput = object({
19112
19252
  deviceId: number(),
19113
19253
  since: number().optional(),
19114
19254
  until: number().optional(),
19115
- limit: number().int().min(1).max(5e3).default(1e3),
19255
+ limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT),
19116
19256
  /** `slim` drops heavy JSON fields (regions/bbox/zones) and carries an
19117
19257
  * optional `mediaUrl` (populated by B5). `full` (default) keeps today's
19118
19258
  * exact behaviour. Callers may omit this field — the store defaults to
@@ -19120,6 +19260,27 @@ var DeviceEventQueryInput = object({
19120
19260
  projection: _enum(["full", "slim"]).optional()
19121
19261
  });
19122
19262
  var ObjectEventQueryInput = DeviceEventQueryInput.extend({ classFilter: string().optional() });
19263
+ var RecentTracksQueryInput = object({
19264
+ /** Devices to merge. An empty array yields `{ tracks: [], nextCursor: null }`. */
19265
+ deviceIds: array(number()),
19266
+ /** Window lower bound on `lastSeen` (inclusive). */
19267
+ since: number().optional(),
19268
+ /** Window upper bound on `lastSeen` (inclusive). */
19269
+ until: number().optional(),
19270
+ /** Page size. Default 200, max 1000. */
19271
+ limit: number().int().min(1).max(1e3).default(200),
19272
+ /** Opaque continuation cursor from a previous page's `nextCursor`.
19273
+ * Encodes the (lastSeen, trackId) sort position — treat as opaque. */
19274
+ cursor: string().optional(),
19275
+ /** See {@link TrackProjectionSchema}. Default `full`. */
19276
+ projection: TrackProjectionSchema.optional()
19277
+ });
19278
+ var RecentTracksPageSchema = object({
19279
+ /** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
19280
+ tracks: array(TrackSchema).readonly(),
19281
+ /** Cursor for the next page, or null when this page is the last. */
19282
+ nextCursor: string().nullable()
19283
+ });
19123
19284
  var KeyEventQueryInput = object({
19124
19285
  deviceId: number(),
19125
19286
  /** Window lower bound (track firstSeen ≥ since). */
@@ -19202,11 +19363,25 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19202
19363
  deviceId: number(),
19203
19364
  since: number().optional(),
19204
19365
  until: number().optional(),
19205
- limit: number().optional()
19206
- }), array(TrackSchema).readonly()), method(object({ deviceId: number() }), _void(), {
19366
+ limit: number().optional(),
19367
+ /** Spatial filter only tracks whose trajectory intersects the zone
19368
+ * (normalized 0..1 rect or polygon). SQL-prefiltered on the persisted
19369
+ * envelope columns, then precisely tested per position. Tracks with
19370
+ * an unknown envelope (no frame dims at persist time) always match. */
19371
+ zone: TrackZoneFilterSchema.optional(),
19372
+ /** See {@link TrackProjectionSchema}. Default `full` (backward
19373
+ * compatible — omitting the field keeps today's exact behaviour). */
19374
+ projection: TrackProjectionSchema.optional()
19375
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
19207
19376
  kind: "mutation",
19208
19377
  auth: "admin"
19209
- }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19378
+ }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({
19379
+ deviceId: number(),
19380
+ since: number().optional(),
19381
+ until: number().optional(),
19382
+ kinds: array(string()).optional(),
19383
+ limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
19384
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19210
19385
  deviceId: number(),
19211
19386
  since: number(),
19212
19387
  until: number(),
@@ -22622,7 +22797,12 @@ var RecordingStorageUsageSchema = object({
22622
22797
  /**
22623
22798
  * Result of locating footage at a wall-clock instant for one device/profile.
22624
22799
  * `segment` carries the covering segment's window; `gap` reports the forward
22625
- * nearest covered edge (`null` past the end of footage / when none exists).
22800
+ * nearest covered edge (`null` past the end of footage / when none exists)
22801
+ * and the backward covered edge `prevEndMs` (exclusive end of the nearest
22802
+ * footage behind the epoch; `null` when none — optional so older providers
22803
+ * that omit it stay valid). `prevEndMs` lets a backward frame-step hop the
22804
+ * small inter-segment cracks (durMs under-covers the span to the next
22805
+ * startMs by ~11-17 ms) instead of no-opping at a segment head.
22626
22806
  */
22627
22807
  var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
22628
22808
  kind: literal("segment"),
@@ -22631,7 +22811,8 @@ var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
22631
22811
  bytes: number()
22632
22812
  }), object({
22633
22813
  kind: literal("gap"),
22634
- nearestEdgeMs: number().nullable()
22814
+ nearestEdgeMs: number().nullable(),
22815
+ prevEndMs: number().nullable().optional()
22635
22816
  })]);
22636
22817
  /** Raw bytes of one finalized footage segment (read off disk on the recording node). */
22637
22818
  var ReadSegmentBytesResultSchema = object({ data: _instanceof(Uint8Array) });
@@ -24411,6 +24592,12 @@ Object.freeze({
24411
24592
  addonId: null,
24412
24593
  access: "view"
24413
24594
  },
24595
+ "deviceManager.getLinkedDevices": {
24596
+ capName: "device-manager",
24597
+ capScope: "system",
24598
+ addonId: null,
24599
+ access: "view"
24600
+ },
24414
24601
  "deviceManager.getRoleDisplayDefaults": {
24415
24602
  capName: "device-manager",
24416
24603
  capScope: "system",
@@ -25965,6 +26152,12 @@ Object.freeze({
25965
26152
  addonId: null,
25966
26153
  access: "view"
25967
26154
  },
26155
+ "pipelineAnalytics.getSensorEvents": {
26156
+ capName: "pipeline-analytics",
26157
+ capScope: "device",
26158
+ addonId: null,
26159
+ access: "view"
26160
+ },
25968
26161
  "pipelineAnalytics.getTrack": {
25969
26162
  capName: "pipeline-analytics",
25970
26163
  capScope: "device",
@@ -25977,6 +26170,18 @@ Object.freeze({
25977
26170
  addonId: null,
25978
26171
  access: "view"
25979
26172
  },
26173
+ "pipelineAnalytics.listEventKinds": {
26174
+ capName: "pipeline-analytics",
26175
+ capScope: "device",
26176
+ addonId: null,
26177
+ access: "view"
26178
+ },
26179
+ "pipelineAnalytics.listRecentTracks": {
26180
+ capName: "pipeline-analytics",
26181
+ capScope: "device",
26182
+ addonId: null,
26183
+ access: "view"
26184
+ },
25980
26185
  "pipelineAnalytics.listTracks": {
25981
26186
  capName: "pipeline-analytics",
25982
26187
  capScope: "device",
package/dist/addon.mjs CHANGED
@@ -7,7 +7,7 @@ import { networkInterfaces } from "node:os";
7
7
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
8
8
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
9
9
  //#endregion
10
- //#region ../types/dist/event-category-CFZs3jI4.mjs
10
+ //#region ../types/dist/event-category-H4AVePnn.mjs
11
11
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
12
12
  EventCategory["SystemBoot"] = "system.boot";
13
13
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -157,6 +157,9 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
157
157
  EventCategory["RecordingSegmentWritten"] = "recording.segment.written";
158
158
  EventCategory["RecordingPolicyFallback"] = "recording.policy.fallback";
159
159
  EventCategory["RecordingRetentionCompleted"] = "recording.retention.completed";
160
+ /** Runner-sampled scrub thumbnail (~1/5 s/camera). Telemetry (D8): a lost
161
+ * thumb is a scrub gap the recorder's keyframe backfill covers. */
162
+ EventCategory["RecordingThumbSampled"] = "recording.thumb-sampled";
160
163
  EventCategory["DetectionEvent"] = "detection.event";
161
164
  EventCategory["SessionTrackNew"] = "session.track.new";
162
165
  EventCategory["SessionTrackExpired"] = "session.track.expired";
@@ -7405,6 +7408,24 @@ var RecordingRetentionSchema = object({
7405
7408
  maxSizeGb: number().min(0).optional()
7406
7409
  });
7407
7410
  /**
7411
+ * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
7412
+ * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
7413
+ * previews at. Five graduated steps; absent on a config = `standard` (the
7414
+ * shipped default, matching `sheet-geometry`/`sheet-composer`).
7415
+ *
7416
+ * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
7417
+ * Each window's index sidecar carries its own tile dims, so a camera whose
7418
+ * preset changed over time renders every historical window at the dims it was
7419
+ * written with.
7420
+ */
7421
+ var ScrubThumbnailPresetSchema = _enum([
7422
+ "minimal",
7423
+ "low",
7424
+ "standard",
7425
+ "high",
7426
+ "max"
7427
+ ]);
7428
+ /**
7408
7429
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
7409
7430
  *
7410
7431
  * `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
@@ -7441,7 +7462,14 @@ var RecordingConfigSchema = object({
7441
7462
  * derived into bands once via `migrateConfigToBands`.
7442
7463
  */
7443
7464
  bands: array(RecordingBandSchema).optional(),
7444
- retention: RecordingRetentionSchema.optional()
7465
+ retention: RecordingRetentionSchema.optional(),
7466
+ /**
7467
+ * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
7468
+ * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
7469
+ * windows only — existing sheets are immutable, and each window's index
7470
+ * carries its own tile dims so mixed-preset history renders correctly.
7471
+ */
7472
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7445
7473
  });
7446
7474
  /**
7447
7475
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -11058,10 +11086,23 @@ var deviceDiscoveryCapability = {
11058
11086
  }
11059
11087
  };
11060
11088
  /**
11061
- * Doorbell button cap. Installed on a `DeviceType.Button` accessory
11062
- * with `role: DeviceRole.Doorbell`. Emits an `onPressed` event every
11063
- * time the firmware pushes a ring; status tracks the last press and
11064
- * a pressCount since start (diagnostic).
11089
+ * Doorbell button cap. Two kinds of providers coexist behind this cap
11090
+ * name (same pattern as `snapshot`):
11091
+ *
11092
+ * - **Native** providers: registered per-device by device-driver
11093
+ * addons via `ctx.registerNativeCap` — either on a
11094
+ * `DeviceType.Button` accessory with `role: DeviceRole.Doorbell`,
11095
+ * or directly on the camera (Reolink registers at camera level).
11096
+ * Emits an `onPressed` event every time the firmware pushes a
11097
+ * ring; status tracks the last press and a pressCount since start
11098
+ * (diagnostic).
11099
+ *
11100
+ * - **Wrapper** provider: the `virtual-doorbell` system builtin
11101
+ * (`@camstack/system/builtins/doorbell`). Turns ANY binary-ish
11102
+ * device (contact / switch / event-emitter …) into a doorbell for
11103
+ * a camera. `defaultActive: false` — the operator explicitly binds
11104
+ * it per camera in the device-bindings UI, then picks the source
11105
+ * device + trigger in the per-device settings.
11065
11106
  *
11066
11107
  * The DeviceEventPropagator re-emits `onPressed` on the camera parent
11067
11108
  * — subscribers listening at the camera level receive ring events
@@ -11082,7 +11123,10 @@ var doorbellCapability = {
11082
11123
  scope: "device",
11083
11124
  deviceNative: true,
11084
11125
  mode: "singleton",
11085
- deviceTypes: [DeviceType.Button],
11126
+ kind: "wrapper",
11127
+ defaultActive: false,
11128
+ deviceTypes: [DeviceType.Button, DeviceType.Camera],
11129
+ exposesDeviceSettings: true,
11086
11130
  methods: {},
11087
11131
  events: {
11088
11132
  /**
@@ -17446,6 +17490,14 @@ var ConfigEntrySchema = object({
17446
17490
  value: unknown(),
17447
17491
  description: string().optional()
17448
17492
  });
17493
+ var DeviceLinkModeSchema = _enum(["auto", "manual"]);
17494
+ /** One resolved linked device — the compact projection consumers need. */
17495
+ var LinkedDeviceSchema = object({
17496
+ deviceId: number(),
17497
+ name: string(),
17498
+ location: string().nullable(),
17499
+ features: array(string())
17500
+ });
17449
17501
  var SavedDeviceRowSchema = object({
17450
17502
  /** Numeric id reserved at allocateDeviceId time. */
17451
17503
  id: number(),
@@ -17667,7 +17719,10 @@ method(object({
17667
17719
  }), _void(), {
17668
17720
  kind: "mutation",
17669
17721
  auth: "admin"
17670
- }), 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({
17722
+ }), 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({
17723
+ mode: DeviceLinkModeSchema,
17724
+ devices: array(LinkedDeviceSchema)
17725
+ })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
17671
17726
  deviceId: number(),
17672
17727
  values: record(string(), unknown())
17673
17728
  }), object({ success: literal(true) }), {
@@ -18936,6 +18991,61 @@ var EventKindSchema = _enum([
18936
18991
  "object",
18937
18992
  "audio"
18938
18993
  ]);
18994
+ /**
18995
+ * Spatial filter for `listTracks` — the rect + polygon variants of the shared
18996
+ * MaskShape vocabulary (see `mask-shape.ts`). Coordinates are NORMALIZED 0..1
18997
+ * of the camera frame (top-left origin), matching the drawing-plane editor.
18998
+ */
18999
+ var TrackZoneFilterSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
19000
+ /** Closed icon vocabulary so clients render a known glyph per kind. */
19001
+ var EventKindIconSchema = _enum([
19002
+ "motion",
19003
+ "audio",
19004
+ "person",
19005
+ "vehicle",
19006
+ "animal",
19007
+ "door",
19008
+ "pir",
19009
+ "smoke",
19010
+ "water",
19011
+ "button",
19012
+ "generic"
19013
+ ]);
19014
+ var EventKindCategorySchema = _enum([
19015
+ "motion",
19016
+ "audio",
19017
+ "detection",
19018
+ "sensor",
19019
+ "custom"
19020
+ ]);
19021
+ var EventKindDescriptorSchema = object({
19022
+ /** Stable kind id (e.g. 'motion', 'person', 'contact'). */
19023
+ kind: string(),
19024
+ label: string(),
19025
+ /** Hex color for timeline/legend rendering. */
19026
+ color: string(),
19027
+ icon: EventKindIconSchema,
19028
+ category: EventKindCategorySchema,
19029
+ /** Which cap + device contributes this kind. For built-ins the camera
19030
+ * itself; for sensor kinds the LINKED source device. */
19031
+ source: object({
19032
+ capName: string(),
19033
+ deviceId: number()
19034
+ })
19035
+ });
19036
+ var SensorEventSchema = object({
19037
+ id: string(),
19038
+ /** The CAMERA the event is attributed to (a sensor linked to N cameras
19039
+ * yields N rows, one per camera). */
19040
+ deviceId: number(),
19041
+ /** The linked sensor device whose state changed. */
19042
+ sourceDeviceId: number(),
19043
+ /** Event kind id — matches an `EventKindDescriptor.kind`. */
19044
+ kind: string(),
19045
+ /** Snapshot of the sensor cap's runtime-state slice at the change. */
19046
+ value: record(string(), unknown()).nullable(),
19047
+ timestamp: number()
19048
+ });
18939
19049
  var TrackPositionSchema = object({
18940
19050
  x: number(),
18941
19051
  y: number(),
@@ -18949,6 +19059,30 @@ var TrackSnapshotSchema = object({
18949
19059
  mediaKey: string()
18950
19060
  });
18951
19061
  /**
19062
+ * Normalized 0..1 trajectory envelope (min/max over every position bbox,
19063
+ * divided by the track's detection-frame dims), computed at persist time.
19064
+ * Absent when the frame dims were unknown when the track was persisted
19065
+ * (legacy rows / dims-less sources) and on active (in-RAM) tracks.
19066
+ */
19067
+ var TrackEnvelopeSchema = object({
19068
+ minX: number(),
19069
+ minY: number(),
19070
+ maxX: number(),
19071
+ maxY: number()
19072
+ });
19073
+ /**
19074
+ * Row projection for track list queries. `full` (default) returns the
19075
+ * complete Track including the frame-rate `positions[]` history and the
19076
+ * `snapshots[]` references — megabytes across a page of tracks. `slim`
19077
+ * keeps every scalar the list surfaces actually render (ids, class(es),
19078
+ * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
19079
+ * zonesVisited, bestEventId, envelope) and returns `positions` /
19080
+ * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
19081
+ * `getTrack`. Mirrors the event-store `projection` convention
19082
+ * (`getObjectEvents` et al.).
19083
+ */
19084
+ var TrackProjectionSchema = _enum(["full", "slim"]);
19085
+ /**
18952
19086
  * One audio-classification label heard on the track's camera while the
18953
19087
  * track was alive, aggregated per label. An "episode" is one persisted
18954
19088
  * audio event (the confident-classification path: score ≥ the device's
@@ -18999,7 +19133,11 @@ var TrackSchema = object({
18999
19133
  /** Audio-classification labels heard on the camera during the track's
19000
19134
  * life (score ≥ device `classificationMinScore`), aggregated per label.
19001
19135
  * Absent on legacy rows / tracks with no confident audio. */
19002
- audioLabels: array(TrackAudioLabelSchema).readonly().optional()
19136
+ audioLabels: array(TrackAudioLabelSchema).readonly().optional(),
19137
+ /** Normalized 0..1 trajectory envelope (see {@link TrackEnvelopeSchema}).
19138
+ * Populated from the persisted envelope columns on historical reads;
19139
+ * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
19140
+ envelope: TrackEnvelopeSchema.optional()
19003
19141
  });
19004
19142
  var BaseEventFields = {
19005
19143
  id: string(),
@@ -19109,11 +19247,13 @@ var MediaFileSchema = object({
19109
19247
  sizeBytes: number(),
19110
19248
  timestamp: number()
19111
19249
  });
19250
+ var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
19251
+ var MAX_EVENT_QUERY_LIMIT = 5e3;
19112
19252
  var DeviceEventQueryInput = object({
19113
19253
  deviceId: number(),
19114
19254
  since: number().optional(),
19115
19255
  until: number().optional(),
19116
- limit: number().int().min(1).max(5e3).default(1e3),
19256
+ limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT),
19117
19257
  /** `slim` drops heavy JSON fields (regions/bbox/zones) and carries an
19118
19258
  * optional `mediaUrl` (populated by B5). `full` (default) keeps today's
19119
19259
  * exact behaviour. Callers may omit this field — the store defaults to
@@ -19121,6 +19261,27 @@ var DeviceEventQueryInput = object({
19121
19261
  projection: _enum(["full", "slim"]).optional()
19122
19262
  });
19123
19263
  var ObjectEventQueryInput = DeviceEventQueryInput.extend({ classFilter: string().optional() });
19264
+ var RecentTracksQueryInput = object({
19265
+ /** Devices to merge. An empty array yields `{ tracks: [], nextCursor: null }`. */
19266
+ deviceIds: array(number()),
19267
+ /** Window lower bound on `lastSeen` (inclusive). */
19268
+ since: number().optional(),
19269
+ /** Window upper bound on `lastSeen` (inclusive). */
19270
+ until: number().optional(),
19271
+ /** Page size. Default 200, max 1000. */
19272
+ limit: number().int().min(1).max(1e3).default(200),
19273
+ /** Opaque continuation cursor from a previous page's `nextCursor`.
19274
+ * Encodes the (lastSeen, trackId) sort position — treat as opaque. */
19275
+ cursor: string().optional(),
19276
+ /** See {@link TrackProjectionSchema}. Default `full`. */
19277
+ projection: TrackProjectionSchema.optional()
19278
+ });
19279
+ var RecentTracksPageSchema = object({
19280
+ /** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
19281
+ tracks: array(TrackSchema).readonly(),
19282
+ /** Cursor for the next page, or null when this page is the last. */
19283
+ nextCursor: string().nullable()
19284
+ });
19124
19285
  var KeyEventQueryInput = object({
19125
19286
  deviceId: number(),
19126
19287
  /** Window lower bound (track firstSeen ≥ since). */
@@ -19203,11 +19364,25 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19203
19364
  deviceId: number(),
19204
19365
  since: number().optional(),
19205
19366
  until: number().optional(),
19206
- limit: number().optional()
19207
- }), array(TrackSchema).readonly()), method(object({ deviceId: number() }), _void(), {
19367
+ limit: number().optional(),
19368
+ /** Spatial filter only tracks whose trajectory intersects the zone
19369
+ * (normalized 0..1 rect or polygon). SQL-prefiltered on the persisted
19370
+ * envelope columns, then precisely tested per position. Tracks with
19371
+ * an unknown envelope (no frame dims at persist time) always match. */
19372
+ zone: TrackZoneFilterSchema.optional(),
19373
+ /** See {@link TrackProjectionSchema}. Default `full` (backward
19374
+ * compatible — omitting the field keeps today's exact behaviour). */
19375
+ projection: TrackProjectionSchema.optional()
19376
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
19208
19377
  kind: "mutation",
19209
19378
  auth: "admin"
19210
- }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19379
+ }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({
19380
+ deviceId: number(),
19381
+ since: number().optional(),
19382
+ until: number().optional(),
19383
+ kinds: array(string()).optional(),
19384
+ limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
19385
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19211
19386
  deviceId: number(),
19212
19387
  since: number(),
19213
19388
  until: number(),
@@ -22623,7 +22798,12 @@ var RecordingStorageUsageSchema = object({
22623
22798
  /**
22624
22799
  * Result of locating footage at a wall-clock instant for one device/profile.
22625
22800
  * `segment` carries the covering segment's window; `gap` reports the forward
22626
- * nearest covered edge (`null` past the end of footage / when none exists).
22801
+ * nearest covered edge (`null` past the end of footage / when none exists)
22802
+ * and the backward covered edge `prevEndMs` (exclusive end of the nearest
22803
+ * footage behind the epoch; `null` when none — optional so older providers
22804
+ * that omit it stay valid). `prevEndMs` lets a backward frame-step hop the
22805
+ * small inter-segment cracks (durMs under-covers the span to the next
22806
+ * startMs by ~11-17 ms) instead of no-opping at a segment head.
22627
22807
  */
22628
22808
  var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
22629
22809
  kind: literal("segment"),
@@ -22632,7 +22812,8 @@ var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
22632
22812
  bytes: number()
22633
22813
  }), object({
22634
22814
  kind: literal("gap"),
22635
- nearestEdgeMs: number().nullable()
22815
+ nearestEdgeMs: number().nullable(),
22816
+ prevEndMs: number().nullable().optional()
22636
22817
  })]);
22637
22818
  /** Raw bytes of one finalized footage segment (read off disk on the recording node). */
22638
22819
  var ReadSegmentBytesResultSchema = object({ data: _instanceof(Uint8Array) });
@@ -24412,6 +24593,12 @@ Object.freeze({
24412
24593
  addonId: null,
24413
24594
  access: "view"
24414
24595
  },
24596
+ "deviceManager.getLinkedDevices": {
24597
+ capName: "device-manager",
24598
+ capScope: "system",
24599
+ addonId: null,
24600
+ access: "view"
24601
+ },
24415
24602
  "deviceManager.getRoleDisplayDefaults": {
24416
24603
  capName: "device-manager",
24417
24604
  capScope: "system",
@@ -25966,6 +26153,12 @@ Object.freeze({
25966
26153
  addonId: null,
25967
26154
  access: "view"
25968
26155
  },
26156
+ "pipelineAnalytics.getSensorEvents": {
26157
+ capName: "pipeline-analytics",
26158
+ capScope: "device",
26159
+ addonId: null,
26160
+ access: "view"
26161
+ },
25969
26162
  "pipelineAnalytics.getTrack": {
25970
26163
  capName: "pipeline-analytics",
25971
26164
  capScope: "device",
@@ -25978,6 +26171,18 @@ Object.freeze({
25978
26171
  addonId: null,
25979
26172
  access: "view"
25980
26173
  },
26174
+ "pipelineAnalytics.listEventKinds": {
26175
+ capName: "pipeline-analytics",
26176
+ capScope: "device",
26177
+ addonId: null,
26178
+ access: "view"
26179
+ },
26180
+ "pipelineAnalytics.listRecentTracks": {
26181
+ capName: "pipeline-analytics",
26182
+ capScope: "device",
26183
+ addonId: null,
26184
+ access: "view"
26185
+ },
25981
26186
  "pipelineAnalytics.listTracks": {
25982
26187
  capName: "pipeline-analytics",
25983
26188
  capScope: "device",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-hikvision",
3
- "version": "1.1.24",
3
+ "version": "1.1.26",
4
4
  "description": "Hikvision camera device provider addon for CamStack — ISAPI over HTTP(S) with digest auth (snapshot, alarm stream, RTSP discovery)",
5
5
  "keywords": [
6
6
  "camstack",
@@ -81,16 +81,13 @@
81
81
  "typecheck": "tsc --noEmit",
82
82
  "publish": "npm publish --access public"
83
83
  },
84
+ "dependencies": {
85
+ "werift": "^0.22.9"
86
+ },
84
87
  "peerDependencies": {
85
88
  "@camstack/types": "*",
86
- "werift": "^0.22.9",
87
89
  "zod": "^4.3.6"
88
90
  },
89
- "peerDependenciesMeta": {
90
- "werift": {
91
- "optional": true
92
- }
93
- },
94
91
  "devDependencies": {
95
92
  "@camstack/types": "*",
96
93
  "typescript": "~6.0.3",