@camstack/addon-provider-reolink 1.2.130 → 1.2.132

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 +332 -82
  2. package/dist/addon.mjs +332 -82
  3. package/package.json +2 -2
package/dist/addon.js CHANGED
@@ -5386,7 +5386,7 @@ var ZodIssueCode = {
5386
5386
  var ZodFirstPartyTypeKind;
5387
5387
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5388
5388
  //#endregion
5389
- //#region ../types/dist/sleep-BEyvfshj.mjs
5389
+ //#region ../types/dist/sleep-BDR76Ykr.mjs
5390
5390
  /**
5391
5391
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5392
5392
  * window to float samples (D455).
@@ -7271,6 +7271,40 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
7271
7271
  DeviceRole["GenericNotifier"] = "generic-notifier";
7272
7272
  return DeviceRole;
7273
7273
  }({});
7274
+ /**
7275
+ * Identity — preserves literal types for downstream inference.
7276
+ *
7277
+ * The constraint is `Record<string, unknown>` (not `CustomActionsSpec`) so
7278
+ * TypeScript does not widen each entry's literal `kind`/`auth` fields to
7279
+ * the broader unions declared on `CustomActionSpec`'s default generics.
7280
+ * Shape validity is enforced separately by the `customAction(...)` helper
7281
+ * whose return type is already a `CustomActionSpec<...>`.
7282
+ */
7283
+ function defineCustomActions(spec) {
7284
+ return spec;
7285
+ }
7286
+ function customAction(input, output, options) {
7287
+ return {
7288
+ input,
7289
+ output,
7290
+ kind: options?.kind ?? "query",
7291
+ auth: options?.auth ?? "protected",
7292
+ scope: options?.scope ?? { kind: "system" },
7293
+ ...options?.caller ? { caller: "required" } : {}
7294
+ };
7295
+ }
7296
+ function deviceCustomAction(input, output, options) {
7297
+ return {
7298
+ input,
7299
+ output,
7300
+ kind: options?.kind ?? "query",
7301
+ auth: options?.auth ?? "protected",
7302
+ scope: {
7303
+ kind: "device",
7304
+ ...options?.deviceTypes ? { deviceTypes: options.deviceTypes } : {}
7305
+ }
7306
+ };
7307
+ }
7274
7308
  var StaticDirOutputSchema = object({ staticDir: string() });
7275
7309
  var VersionOutputSchema = object({ version: string() });
7276
7310
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -12132,6 +12166,21 @@ var streamSignalsCapability = {
12132
12166
  mode: "singleton",
12133
12167
  deviceTypes: Object.values(DeviceType),
12134
12168
  runtimeState: StreamSignalsStatusSchema,
12169
+ /**
12170
+ * Runtime-state durability: **session** — mirrored in RAM, never written.
12171
+ *
12172
+ * The slice holds what the DEVICE says it can emit. That is a probed fact,
12173
+ * not an operator choice: the provider re-declares it on every registration,
12174
+ * so losing it loses nothing and persisting it would freeze an answer the
12175
+ * camera is entitled to change. Measured the same day on the sibling case —
12176
+ * `native-object-detection.supportedClasses` was persisted, and a firmware
12177
+ * class the camera really detected stayed missing for the life of the row
12178
+ * because the fix could not reach it.
12179
+ *
12180
+ * See `RuntimeStateDurability`. Enforced by
12181
+ * `scripts/check-runtime-state-durability.ts`.
12182
+ */
12183
+ durability: "session",
12135
12184
  methods: {
12136
12185
  /**
12137
12186
  * What this device can emit. Empty is a valid and common answer — most
@@ -13106,40 +13155,6 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
13106
13155
  content: string()
13107
13156
  })) }), { auth: "admin" });
13108
13157
  /**
13109
- * Identity — preserves literal types for downstream inference.
13110
- *
13111
- * The constraint is `Record<string, unknown>` (not `CustomActionsSpec`) so
13112
- * TypeScript does not widen each entry's literal `kind`/`auth` fields to
13113
- * the broader unions declared on `CustomActionSpec`'s default generics.
13114
- * Shape validity is enforced separately by the `customAction(...)` helper
13115
- * whose return type is already a `CustomActionSpec<...>`.
13116
- */
13117
- function defineCustomActions(spec) {
13118
- return spec;
13119
- }
13120
- function customAction(input, output, options) {
13121
- return {
13122
- input,
13123
- output,
13124
- kind: options?.kind ?? "query",
13125
- auth: options?.auth ?? "protected",
13126
- scope: options?.scope ?? { kind: "system" },
13127
- ...options?.caller ? { caller: "required" } : {}
13128
- };
13129
- }
13130
- function deviceCustomAction(input, output, options) {
13131
- return {
13132
- input,
13133
- output,
13134
- kind: options?.kind ?? "query",
13135
- auth: options?.auth ?? "protected",
13136
- scope: {
13137
- kind: "device",
13138
- ...options?.deviceTypes ? { deviceTypes: options.deviceTypes } : {}
13139
- }
13140
- };
13141
- }
13142
- /**
13143
13158
  * `custom-model-registry` — collection cap exposing operator-registered
13144
13159
  * custom detection models. Each provider (today: `addon-model-studio`)
13145
13160
  * contributes a list of `CustomModelDescriptor`s; the hub auto-concatenates
@@ -18820,6 +18835,48 @@ var NcAlarmConfigSchema = object({
18820
18835
  settings: NcAlarmSettingsSchema,
18821
18836
  coverage: array(NcAlarmModeCoverageSchema)
18822
18837
  });
18838
+ /**
18839
+ * ONE rule's demand on ONE camera's clip ring.
18840
+ *
18841
+ * A rule, not a camera: the broker takes the MAX over the asks that reach a
18842
+ * camera, so the answer stays a faithful description of the rules and the
18843
+ * bounding (the byte budget, the broker's own ceiling) stays where the cost
18844
+ * lives. A camera that appears in no ask is being told **nothing** — which is
18845
+ * a different thing from never having been told, and only the envelope
18846
+ * (`rulesLoaded`) can tell those apart.
18847
+ */
18848
+ var NcClipRetentionAskSchema = object({
18849
+ /**
18850
+ * The camera this ask is about. **Absent = every camera**, which is what a
18851
+ * rule with no `conditions.devices` means: it can fire anywhere, so it is
18852
+ * asking everywhere.
18853
+ */
18854
+ deviceId: number().int().nonnegative().optional(),
18855
+ /** Seconds of history the rule needs held for it. Never zero — a rule that
18856
+ * needs nothing produces no ask at all. */
18857
+ seconds: number().min(0).max(60),
18858
+ /** The profile the rule cuts from, when it named one. Absent = the cheapest
18859
+ * assigned, which is what the cut defaults to. */
18860
+ profile: CamProfileSchema.optional(),
18861
+ /** Who is asking — so a retention the operator did not expect names a rule. */
18862
+ ruleId: string(),
18863
+ ruleName: string(),
18864
+ /** Why this many seconds: the rule's own window, or the occupancy ceiling. */
18865
+ reason: _enum(["window", "occupancy"])
18866
+ });
18867
+ /**
18868
+ * The whole answer, with the one bit that keeps absence from reading as zero.
18869
+ *
18870
+ * `rulesLoaded` is false until the rule ledger has completed a load at least
18871
+ * once. A centre that is up but has not read its rules yet answers with an
18872
+ * empty ask list that means nothing at all — applying it would drop every
18873
+ * camera's ring to zero for the first events after a restart, which is the
18874
+ * footage this mechanism exists to keep.
18875
+ */
18876
+ var NcClipRetentionPlanSchema = object({
18877
+ rulesLoaded: boolean(),
18878
+ asks: array(NcClipRetentionAskSchema).readonly()
18879
+ });
18823
18880
  method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), method(object({ ruleId: string() }), object({ rule: NcRuleSchema.nullable() }), { auth: "admin" }), method(object({ rule: NcRuleInputSchema }), object({ rule: NcRuleSchema }), {
18824
18881
  kind: "mutation",
18825
18882
  auth: "admin",
@@ -18840,7 +18897,7 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
18840
18897
  }), object({ success: literal(true) }), {
18841
18898
  kind: "mutation",
18842
18899
  auth: "admin"
18843
- }), method(object({}), object({ mutedDeviceIds: array(number().int()).readonly() }), { auth: "admin" }), method(object({
18900
+ }), method(object({}), NcClipRetentionPlanSchema, { auth: "admin" }), method(object({}), object({ mutedDeviceIds: array(number().int()).readonly() }), { auth: "admin" }), method(object({
18844
18901
  deviceId: number().int(),
18845
18902
  muted: boolean()
18846
18903
  }), object({ success: literal(true) }), {
@@ -20025,13 +20082,26 @@ var TrackAudioLabelSchema = object({
20025
20082
  * - `audio` — an audio event on the camera itself that was anomalous for
20026
20083
  * THAT camera, loud, and heard while nothing visual was happening (D62).
20027
20084
  *
20085
+ * - `onboard` — the CAMERA's own firmware, paired with a decoded frame in
20086
+ * that frame's pixel space. A real root detector and a SPATIAL one: its
20087
+ * boxes are the same kind of fact `pipeline`'s are, produced by a different
20088
+ * detector.
20089
+ *
20090
+ * `onboard` was missing here while `DetectionSourceSchema` already had it, so
20091
+ * a persisted onboard track failed `safeParse` on read and came back with NO
20092
+ * source at all — which `isSpatialTrack` then admitted by accident, through the
20093
+ * `undefined` arm rather than by anybody's decision, and which `excludeSources`
20094
+ * could not name.
20095
+ *
20028
20096
  * The spatial subsystems (tracker association, occupancy count, re-id /
20029
20097
  * embedding, resurrection) MUST skip every synthetic source. Test for that
20030
- * with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
20031
- * check silently readmits every source added after it was written.
20098
+ * with `isSpatialTrack`, which allow-lists the spatial sources explicitly — a
20099
+ * `!== 'sensor'` check silently readmits every source added after it was
20100
+ * written.
20032
20101
  */
20033
20102
  var TrackSourceSchema = _enum([
20034
20103
  "pipeline",
20104
+ "onboard",
20035
20105
  "sensor",
20036
20106
  "audio"
20037
20107
  ]);
@@ -21414,6 +21484,33 @@ var RunReplayFrameProcessorResultSchema = object({ tracks: array(object({
21414
21484
  * solid is this track", cheaper than re-deriving it from a trajectory. */
21415
21485
  framesMatched: number().int()
21416
21486
  })).readonly() });
21487
+ /**
21488
+ * The long-term series a camera produces. A closed union so a typo cannot
21489
+ * invent one, and the ONE declaration of it — the addon's `AnalyticsLtsSeries`
21490
+ * is an alias of this, not a second list.
21491
+ */
21492
+ var AnalyticsLtsSeriesSchema = _enum([
21493
+ "motion",
21494
+ "audio-dbfs",
21495
+ "battery",
21496
+ "occupancy"
21497
+ ]);
21498
+ /**
21499
+ * One closed 5-minute bucket of a long-term series.
21500
+ *
21501
+ * `samples` is how many readings the bucket folded — it is the ACTIVITY for a
21502
+ * series like `motion`, and the confidence for one like `occupancy`. A bucket
21503
+ * that exists at all was measured; a bucket that is absent was not, which is a
21504
+ * different claim from zero and the reason this is a sparse series.
21505
+ */
21506
+ var LtsBucketSchema = object({
21507
+ scope: string(),
21508
+ bucketStart: number().int(),
21509
+ samples: number().int().nonnegative(),
21510
+ sum: number(),
21511
+ min: number(),
21512
+ max: number()
21513
+ });
21417
21514
  DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).readonly()), method(object({
21418
21515
  deviceId: number(),
21419
21516
  trackId: string()
@@ -21460,6 +21557,14 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
21460
21557
  kinds: array(string()).optional(),
21461
21558
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
21462
21559
  }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
21560
+ deviceId: number(),
21561
+ series: AnalyticsLtsSeriesSchema,
21562
+ /** Omit for EVERY scope of this series in the range. */
21563
+ scope: string().optional(),
21564
+ from: number(),
21565
+ to: number(),
21566
+ limit: number().int().positive().optional()
21567
+ }), array(LtsBucketSchema).readonly()), method(object({
21463
21568
  deviceId: number(),
21464
21569
  since: number(),
21465
21570
  until: number(),
@@ -22078,13 +22183,39 @@ var MotionSourceEnum = _enum([
22078
22183
  */
22079
22184
  var MotionSourcesSchema = array(MotionSourceEnum);
22080
22185
  /**
22081
- * Which root detectors a camera runs. Deliberately the SAME vocabulary
22082
- * post-analysis already tags every detection with (`DetectionSource`) rather
22083
- * than a second spelling of the same two ideas the value an operator picks
22084
- * here is the value that comes back on the track, the overlay and the debug
22085
- * row.
22186
+ * Which root detector a camera runs EXACTLY ONE.
22187
+ *
22188
+ * Deliberately the SAME vocabulary post-analysis already tags every detection
22189
+ * with (`DetectionSource`) rather than a second spelling of the same two
22190
+ * ideas: the value an operator picks here is the value that comes back on the
22191
+ * track, the overlay and the debug row.
22192
+ *
22193
+ * ## Why one, and why it is still an array
22194
+ *
22195
+ * Two roots on one camera is a capability nobody asked for and the operator has
22196
+ * since ruled out. It was never free: both planes feed the SAME per-device
22197
+ * stationary registry, both can promote the same parked object, and the two
22198
+ * detectors disagree about the same box by construction — which is the
22199
+ * disagreement D505 had to arbitrate. Removing the case removes the arbitration.
22200
+ *
22201
+ * The ARRAY shape survives because the stored settings and the attach payload
22202
+ * already speak it on every camera of every fleet, and a cap input that
22203
+ * suddenly refuses a stored value makes the camera un-attachable — a fail-closed
22204
+ * in the one direction that costs an operator their cameras. So the wire stays
22205
+ * tolerant and the TYPE is exact: a longer list is truncated to its first
22206
+ * element rather than refused, and `max(1)` is what every consumer can then
22207
+ * rely on.
22208
+ *
22209
+ * AT MOST one, not exactly one. The EMPTY list is a legal, deliberate pick —
22210
+ * a camera an operator left with no root detector — and `planDetectionSources`
22211
+ * has always said so: inventing a root for them is how a default acts without
22212
+ * anybody choosing it. Tightening this to `length(1)` broke that, and the
22213
+ * suite caught it.
22214
+ *
22215
+ * Measured before tightening (2026-09-15, this fleet, 6 h of attaches): 3327
22216
+ * `["pipeline"]` and 2 `["onboard"]`. Not one camera had two.
22086
22217
  */
22087
- var DetectionSourcesSchema = array(DetectionSourceSchema);
22218
+ var DetectionSourcesSchema = preprocess((value) => Array.isArray(value) && value.length > 1 ? value.slice(0, 1) : value, array(DetectionSourceSchema).max(1));
22088
22219
  /**
22089
22220
  * Input shape for `pipeline-runner.reportMotion` cap method. Exported
22090
22221
  * so cap-side consumers (the orchestrator forward, the runner addon's
@@ -22446,7 +22577,47 @@ var RunnerLocalMetricsSchema = object({
22446
22577
  queueDepth: number(),
22447
22578
  frameLazy: FrameLazyMetricsSchema.optional()
22448
22579
  });
22580
+ /**
22581
+ * Pipeline Runner capability — runtime detection workhorse.
22582
+ *
22583
+ * One instance per node. Receives camera assignments from
22584
+ * `addon-pipeline-orchestrator`, subscribes to the local stream-broker for
22585
+ * decoded frames, drains motion + detection queues, calls the local
22586
+ * `motion-detection` and `pipeline-executor` capabilities, and emits typed
22587
+ * `pipeline.inference-result` and `detection.motion-analysis` events on
22588
+ * the bus.
22589
+ *
22590
+ * Distinct from `pipeline-orchestrator` (the hub-side load balancer) — the
22591
+ * runner has zero knowledge of other agents, no global state, and never
22592
+ * makes assignment decisions itself.
22593
+ */
22594
+ /**
22595
+ * Why a runner declined to look at a camera out of band. Named rather than a
22596
+ * bare `false`: a camera asleep on battery (D173), one attached elsewhere, one
22597
+ * already mid-session and one already bursting are four different facts, and a
22598
+ * caller told only "no" cannot tell a protection from a bug.
22599
+ */
22600
+ var OccupancyBurstRefusalSchema = _enum([
22601
+ "not-attached",
22602
+ "battery-asleep",
22603
+ "not-watching",
22604
+ "already-bursting"
22605
+ ]);
22449
22606
  method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number()).readonly()), method(object({
22607
+ deviceId: number(),
22608
+ /** How many frames to collect. The caller sizes this against what its
22609
+ * own confirmation needs; the runner clamps it to the field's range. */
22610
+ frames: number().int().positive().optional(),
22611
+ /** Why, for the log. A burst nobody can attribute is a cost nobody can
22612
+ * defend — the periodic one at least has a timer to point at. */
22613
+ reason: string()
22614
+ }), object({
22615
+ started: boolean(),
22616
+ refusedBecause: OccupancyBurstRefusalSchema.optional()
22617
+ }), {
22618
+ auth: "admin",
22619
+ kind: "mutation"
22620
+ }), method(object({
22450
22621
  handle: FrameHandleSchema,
22451
22622
  bbox: NativeCropBboxSchema,
22452
22623
  maxWidth: number().int().positive().optional(),
@@ -34598,7 +34769,28 @@ var PerScopeBreakdownSchema = object({
34598
34769
  /** Total tracked objects in this scope (frame / zone / unzoned). */
34599
34770
  totalObjects: number().int().nonnegative(),
34600
34771
  /** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
34601
- byClass: record(string(), number().int().nonnegative())
34772
+ byClass: record(string(), number().int().nonnegative()),
34773
+ /**
34774
+ * Of `totalObjects`, how many are STANDING — objects the census holds,
34775
+ * present but not going anywhere (a parked car, a bin, a statue).
34776
+ *
34777
+ * The operator's question about a scene has two halves and they answer
34778
+ * different things: "what is parked here" is stable for hours and must not
34779
+ * flap, while "who is walking through" is the thing an alert is about. One
34780
+ * number could only serve one of them, and `totalObjects` served the first
34781
+ * badly — a passer-by moved it and every occupancy rule saw a change.
34782
+ *
34783
+ * `standing + transient === totalObjects`, always. The total keeps its old
34784
+ * meaning exactly, so no rule written against it changes behaviour.
34785
+ *
34786
+ * ABSENT means UNKNOWN, never zero (D393): a snapshot from a node older than
34787
+ * this field cannot say, and a reader that folds absence in as 0 reports "no
34788
+ * parked objects here" about a camera whose lot is full.
34789
+ */
34790
+ standingObjects: number().int().nonnegative().optional(),
34791
+ /** Of `totalObjects`, how many are passing through. See `standingObjects` —
34792
+ * same absence rule. */
34793
+ transientObjects: number().int().nonnegative().optional()
34602
34794
  });
34603
34795
  var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
34604
34796
  zoneId: string(),
@@ -34721,6 +34913,23 @@ var HistoryPointSchema = object({
34721
34913
  * triple covers the three spatial scopes (per-zone, frame-wide,
34722
34914
  * outside-any-zone). Crossing them with `className?` gives the full
34723
34915
  * combinatorial coverage the operator UI requested.
34916
+ *
34917
+ * ## ⚠️ The three history methods are DEPRECATED
34918
+ *
34919
+ * They are served from a 60-MINUTE in-memory ring. It dies with the process,
34920
+ * so after a restart they report nothing about a night that did happen, and any
34921
+ * window past an hour is silently clamped — a caller cannot tell a clamp from
34922
+ * an empty scene. The presets in the occupancy chart existed to hide that
34923
+ * horizon rather than to answer a question.
34924
+ *
34925
+ * Use `pipelineAnalytics.readLongTermSeries({ series: 'occupancy', scope })`
34926
+ * instead. Those rows are already written for every camera, ~60 bytes per
34927
+ * (camera, scope, 5-minute bucket), kept for a year, and survive a restart.
34928
+ * Spell the scope with `occupancyScope(zoneId?, className?)` — the same
34929
+ * function the writer uses, so the two cannot drift.
34930
+ *
34931
+ * The ring stays until these three go, and it is the only thing left reading
34932
+ * it. Do not add a fourth caller.
34724
34933
  */
34725
34934
  var zoneAnalyticsCapability = {
34726
34935
  name: "zone-analytics",
@@ -39580,6 +39789,12 @@ Object.freeze({
39580
39789
  addonId: null,
39581
39790
  access: "view"
39582
39791
  },
39792
+ "notificationRules.getClipRetentionAsks": {
39793
+ capName: "notification-rules",
39794
+ capScope: "system",
39795
+ addonId: null,
39796
+ access: "view"
39797
+ },
39583
39798
  "notificationRules.getConditionCatalog": {
39584
39799
  capName: "notification-rules",
39585
39800
  capScope: "system",
@@ -40090,6 +40305,12 @@ Object.freeze({
40090
40305
  addonId: null,
40091
40306
  access: "create"
40092
40307
  },
40308
+ "pipelineAnalytics.readLongTermSeries": {
40309
+ capName: "pipeline-analytics",
40310
+ capScope: "device",
40311
+ addonId: null,
40312
+ access: "view"
40313
+ },
40093
40314
  "pipelineAnalytics.rebuildObjectEmbeddings": {
40094
40315
  capName: "pipeline-analytics",
40095
40316
  capScope: "device",
@@ -40762,6 +40983,12 @@ Object.freeze({
40762
40983
  addonId: null,
40763
40984
  access: "create"
40764
40985
  },
40986
+ "pipelineRunner.requestOccupancyBurst": {
40987
+ capName: "pipeline-runner",
40988
+ capScope: "system",
40989
+ addonId: null,
40990
+ access: "create"
40991
+ },
40765
40992
  "pipelineRunner.runDetailSubtree": {
40766
40993
  capName: "pipeline-runner",
40767
40994
  capScope: "system",
@@ -43906,6 +44133,11 @@ Object.freeze({
43906
44133
  form: "single",
43907
44134
  optional: false
43908
44135
  }],
44136
+ "pipelineAnalytics.readLongTermSeries": [{
44137
+ name: "deviceId",
44138
+ form: "single",
44139
+ optional: false
44140
+ }],
43909
44141
  "pipelineAnalytics.rebuildObjectEmbeddings": [{
43910
44142
  name: "deviceId",
43911
44143
  form: "single",
@@ -44096,6 +44328,11 @@ Object.freeze({
44096
44328
  form: "single",
44097
44329
  optional: false
44098
44330
  }],
44331
+ "pipelineRunner.requestOccupancyBurst": [{
44332
+ name: "deviceId",
44333
+ form: "single",
44334
+ optional: false
44335
+ }],
44099
44336
  "pipelineRunner.runDetailSubtree": [{
44100
44337
  name: "deviceId",
44101
44338
  form: "single",
@@ -145323,6 +145560,29 @@ function buildOsdSection(snapshot, values, opts) {
145323
145560
  };
145324
145561
  }
145325
145562
  //#endregion
145563
+ //#region src/snapshot-wake-gate.ts
145564
+ /**
145565
+ * Decide whether a snapshot read may reach the camera's socket.
145566
+ *
145567
+ * Pure, and deliberately total: three inputs, no clock, no cooldown, no
145568
+ * fallible read (D49 — a gate that can be talked out of its answer by a
145569
+ * stale timestamp is not a gate).
145570
+ */
145571
+ function decideSnapshotCameraAccess(input) {
145572
+ if (!input.isBattery || !input.sleeping) return {
145573
+ kind: "proceed",
145574
+ stampCooldown: false
145575
+ };
145576
+ if (!input.force) return {
145577
+ kind: "refuse",
145578
+ reason: "sleeping"
145579
+ };
145580
+ return {
145581
+ kind: "proceed",
145582
+ stampCooldown: true
145583
+ };
145584
+ }
145585
+ //#endregion
145326
145586
  //#region src/raw-state.ts
145327
145587
  /**
145328
145588
  * Source tag for every raw-state blob this provider emits.
@@ -150117,9 +150377,9 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
150117
150377
  }
150118
150378
  registerNativeCapabilities() {
150119
150379
  this.ctx.registerNativeCap(snapshotCapability, {
150120
- getSnapshot: async ({ deviceId }) => {
150380
+ getSnapshot: async ({ deviceId, force }) => {
150121
150381
  if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
150122
- return this.fetchSnapshotWithSingleFlight();
150382
+ return this.fetchSnapshotWithSingleFlight(force === true);
150123
150383
  },
150124
150384
  invalidateCache: async () => {}
150125
150385
  });
@@ -150562,33 +150822,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
150562
150822
  status: this.state.battery
150563
150823
  }));
150564
150824
  }
150565
- /**
150566
- * Gate for a PROACTIVE camera read, checked BEFORE `ensureApi()`.
150567
- *
150568
- * The login itself is the wake. On a sleeping UDP/battery camera the
150569
- * lib's discovery + handshake nudges the firmware awake (same reason
150570
- * `refreshParentSettingsSnapshot` refuses to call `ensureApi` while
150571
- * asleep), so gating only the explicit `wakeUp()` call is useless —
150572
- * by the time we reach it the camera is already up. Production logs
150573
- * showed exactly that: a background read produced a full
150574
- * `Connecting to Reolink` → BCUDP discovery → `battery sleep state
150575
- * committed` (awake) → whole refresh cascade, on a camera that had
150576
- * been asleep for six minutes.
150577
- *
150578
- * Returns `false` when the caller must serve cache / bail out without
150579
- * touching the socket. Stamps the cooldown when it does let a wake
150580
- * through, so "at most one proactive wake per
150581
- * `PROACTIVE_WAKE_COOLDOWN_MS`" holds across ALL proactive callers
150582
- * rather than per-caller.
150583
- *
150584
- * Demand-driven paths never call this — see `canProactivelyWake`.
150585
- */
150586
- allowProactiveCameraAccess(reason) {
150587
- if (!this.isBattery || !this.sleeping) return true;
150588
- if (!this.canProactivelyWake(reason)) return false;
150589
- this.markWakeIssued();
150590
- return true;
150591
- }
150592
150825
  updateBatteryCache(info) {
150593
150826
  const mapped = this.mapBatteryInfo(info);
150594
150827
  const now = Date.now();
@@ -150868,12 +151101,27 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
150868
151101
  }
150869
151102
  await this.refreshCameraEmailConfig().catch(() => {});
150870
151103
  }
150871
- async fetchSnapshotWithSingleFlight() {
151104
+ /**
151105
+ * @param force - the caller's `snapshot.getSnapshot({ force })`. THE gate:
151106
+ * without it a sleeping battery camera is never touched, whatever the
151107
+ * cache holds. See `snapshot-wake-gate.ts` for why the flag — and not a
151108
+ * cooldown — is what decides.
151109
+ */
151110
+ async fetchSnapshotWithSingleFlight(force) {
150872
151111
  if (this.snapshotInFlight) return this.snapshotInFlight;
150873
- if (!this.allowProactiveCameraAccess("snapshot")) {
150874
- this.ctx.logger.debug("snapshot: skipped — sleeping battery cam inside wake cooldown", { tags: { deviceId: this.id } });
151112
+ const access = decideSnapshotCameraAccess({
151113
+ isBattery: this.isBattery,
151114
+ sleeping: this.sleeping,
151115
+ force
151116
+ });
151117
+ if (access.kind === "refuse") {
151118
+ this.ctx.logger.info("snapshot: refused — battery cam asleep and no operator force", {
151119
+ tags: { deviceId: this.id },
151120
+ meta: { reason: access.reason }
151121
+ });
150875
151122
  return null;
150876
151123
  }
151124
+ if (access.stampCooldown) this.markWakeIssued();
150877
151125
  const promise = (async () => {
150878
151126
  let api;
150879
151127
  try {
@@ -152596,11 +152844,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152596
152844
  };
152597
152845
  this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
152598
152846
  this.registerStreamSignalsCap();
152599
- if (this.runtimeState.getCapState(CAP_NAME) === void 0) this.runtimeState.setCapState(CAP_NAME, {
152600
- ...buildEmptyState(),
152601
- lastFetchedAt: Date.now()
152602
- });
152603
- else this.runtimeState.patchCapState(CAP_NAME, { supportedClasses: buildSupportedClasses() });
152847
+ if (this.runtimeState.getCapState(CAP_NAME) === void 0) {
152848
+ this.runtimeState.setCapState(CAP_NAME, {
152849
+ ...buildEmptyState(),
152850
+ lastFetchedAt: Date.now()
152851
+ });
152852
+ this.ctx.logger.info("Reolink onboard AI: no restored state — cold-start default (OFF)", { tags: { deviceId: this.id } });
152853
+ } else this.runtimeState.patchCapState(CAP_NAME, { supportedClasses: buildSupportedClasses() });
152604
152854
  this.ctx.logger.info("Reolink native-object-detection cap registered", { tags: { deviceId: this.id } });
152605
152855
  }
152606
152856
  /**
package/dist/addon.mjs CHANGED
@@ -5381,7 +5381,7 @@ var ZodIssueCode = {
5381
5381
  var ZodFirstPartyTypeKind;
5382
5382
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5383
5383
  //#endregion
5384
- //#region ../types/dist/sleep-BEyvfshj.mjs
5384
+ //#region ../types/dist/sleep-BDR76Ykr.mjs
5385
5385
  /**
5386
5386
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5387
5387
  * window to float samples (D455).
@@ -7266,6 +7266,40 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
7266
7266
  DeviceRole["GenericNotifier"] = "generic-notifier";
7267
7267
  return DeviceRole;
7268
7268
  }({});
7269
+ /**
7270
+ * Identity — preserves literal types for downstream inference.
7271
+ *
7272
+ * The constraint is `Record<string, unknown>` (not `CustomActionsSpec`) so
7273
+ * TypeScript does not widen each entry's literal `kind`/`auth` fields to
7274
+ * the broader unions declared on `CustomActionSpec`'s default generics.
7275
+ * Shape validity is enforced separately by the `customAction(...)` helper
7276
+ * whose return type is already a `CustomActionSpec<...>`.
7277
+ */
7278
+ function defineCustomActions(spec) {
7279
+ return spec;
7280
+ }
7281
+ function customAction(input, output, options) {
7282
+ return {
7283
+ input,
7284
+ output,
7285
+ kind: options?.kind ?? "query",
7286
+ auth: options?.auth ?? "protected",
7287
+ scope: options?.scope ?? { kind: "system" },
7288
+ ...options?.caller ? { caller: "required" } : {}
7289
+ };
7290
+ }
7291
+ function deviceCustomAction(input, output, options) {
7292
+ return {
7293
+ input,
7294
+ output,
7295
+ kind: options?.kind ?? "query",
7296
+ auth: options?.auth ?? "protected",
7297
+ scope: {
7298
+ kind: "device",
7299
+ ...options?.deviceTypes ? { deviceTypes: options.deviceTypes } : {}
7300
+ }
7301
+ };
7302
+ }
7269
7303
  var StaticDirOutputSchema = object({ staticDir: string() });
7270
7304
  var VersionOutputSchema = object({ version: string() });
7271
7305
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -12127,6 +12161,21 @@ var streamSignalsCapability = {
12127
12161
  mode: "singleton",
12128
12162
  deviceTypes: Object.values(DeviceType),
12129
12163
  runtimeState: StreamSignalsStatusSchema,
12164
+ /**
12165
+ * Runtime-state durability: **session** — mirrored in RAM, never written.
12166
+ *
12167
+ * The slice holds what the DEVICE says it can emit. That is a probed fact,
12168
+ * not an operator choice: the provider re-declares it on every registration,
12169
+ * so losing it loses nothing and persisting it would freeze an answer the
12170
+ * camera is entitled to change. Measured the same day on the sibling case —
12171
+ * `native-object-detection.supportedClasses` was persisted, and a firmware
12172
+ * class the camera really detected stayed missing for the life of the row
12173
+ * because the fix could not reach it.
12174
+ *
12175
+ * See `RuntimeStateDurability`. Enforced by
12176
+ * `scripts/check-runtime-state-durability.ts`.
12177
+ */
12178
+ durability: "session",
12130
12179
  methods: {
12131
12180
  /**
12132
12181
  * What this device can emit. Empty is a valid and common answer — most
@@ -13101,40 +13150,6 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
13101
13150
  content: string()
13102
13151
  })) }), { auth: "admin" });
13103
13152
  /**
13104
- * Identity — preserves literal types for downstream inference.
13105
- *
13106
- * The constraint is `Record<string, unknown>` (not `CustomActionsSpec`) so
13107
- * TypeScript does not widen each entry's literal `kind`/`auth` fields to
13108
- * the broader unions declared on `CustomActionSpec`'s default generics.
13109
- * Shape validity is enforced separately by the `customAction(...)` helper
13110
- * whose return type is already a `CustomActionSpec<...>`.
13111
- */
13112
- function defineCustomActions(spec) {
13113
- return spec;
13114
- }
13115
- function customAction(input, output, options) {
13116
- return {
13117
- input,
13118
- output,
13119
- kind: options?.kind ?? "query",
13120
- auth: options?.auth ?? "protected",
13121
- scope: options?.scope ?? { kind: "system" },
13122
- ...options?.caller ? { caller: "required" } : {}
13123
- };
13124
- }
13125
- function deviceCustomAction(input, output, options) {
13126
- return {
13127
- input,
13128
- output,
13129
- kind: options?.kind ?? "query",
13130
- auth: options?.auth ?? "protected",
13131
- scope: {
13132
- kind: "device",
13133
- ...options?.deviceTypes ? { deviceTypes: options.deviceTypes } : {}
13134
- }
13135
- };
13136
- }
13137
- /**
13138
13153
  * `custom-model-registry` — collection cap exposing operator-registered
13139
13154
  * custom detection models. Each provider (today: `addon-model-studio`)
13140
13155
  * contributes a list of `CustomModelDescriptor`s; the hub auto-concatenates
@@ -18815,6 +18830,48 @@ var NcAlarmConfigSchema = object({
18815
18830
  settings: NcAlarmSettingsSchema,
18816
18831
  coverage: array(NcAlarmModeCoverageSchema)
18817
18832
  });
18833
+ /**
18834
+ * ONE rule's demand on ONE camera's clip ring.
18835
+ *
18836
+ * A rule, not a camera: the broker takes the MAX over the asks that reach a
18837
+ * camera, so the answer stays a faithful description of the rules and the
18838
+ * bounding (the byte budget, the broker's own ceiling) stays where the cost
18839
+ * lives. A camera that appears in no ask is being told **nothing** — which is
18840
+ * a different thing from never having been told, and only the envelope
18841
+ * (`rulesLoaded`) can tell those apart.
18842
+ */
18843
+ var NcClipRetentionAskSchema = object({
18844
+ /**
18845
+ * The camera this ask is about. **Absent = every camera**, which is what a
18846
+ * rule with no `conditions.devices` means: it can fire anywhere, so it is
18847
+ * asking everywhere.
18848
+ */
18849
+ deviceId: number().int().nonnegative().optional(),
18850
+ /** Seconds of history the rule needs held for it. Never zero — a rule that
18851
+ * needs nothing produces no ask at all. */
18852
+ seconds: number().min(0).max(60),
18853
+ /** The profile the rule cuts from, when it named one. Absent = the cheapest
18854
+ * assigned, which is what the cut defaults to. */
18855
+ profile: CamProfileSchema.optional(),
18856
+ /** Who is asking — so a retention the operator did not expect names a rule. */
18857
+ ruleId: string(),
18858
+ ruleName: string(),
18859
+ /** Why this many seconds: the rule's own window, or the occupancy ceiling. */
18860
+ reason: _enum(["window", "occupancy"])
18861
+ });
18862
+ /**
18863
+ * The whole answer, with the one bit that keeps absence from reading as zero.
18864
+ *
18865
+ * `rulesLoaded` is false until the rule ledger has completed a load at least
18866
+ * once. A centre that is up but has not read its rules yet answers with an
18867
+ * empty ask list that means nothing at all — applying it would drop every
18868
+ * camera's ring to zero for the first events after a restart, which is the
18869
+ * footage this mechanism exists to keep.
18870
+ */
18871
+ var NcClipRetentionPlanSchema = object({
18872
+ rulesLoaded: boolean(),
18873
+ asks: array(NcClipRetentionAskSchema).readonly()
18874
+ });
18818
18875
  method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), method(object({ ruleId: string() }), object({ rule: NcRuleSchema.nullable() }), { auth: "admin" }), method(object({ rule: NcRuleInputSchema }), object({ rule: NcRuleSchema }), {
18819
18876
  kind: "mutation",
18820
18877
  auth: "admin",
@@ -18835,7 +18892,7 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
18835
18892
  }), object({ success: literal(true) }), {
18836
18893
  kind: "mutation",
18837
18894
  auth: "admin"
18838
- }), method(object({}), object({ mutedDeviceIds: array(number().int()).readonly() }), { auth: "admin" }), method(object({
18895
+ }), method(object({}), NcClipRetentionPlanSchema, { auth: "admin" }), method(object({}), object({ mutedDeviceIds: array(number().int()).readonly() }), { auth: "admin" }), method(object({
18839
18896
  deviceId: number().int(),
18840
18897
  muted: boolean()
18841
18898
  }), object({ success: literal(true) }), {
@@ -20020,13 +20077,26 @@ var TrackAudioLabelSchema = object({
20020
20077
  * - `audio` — an audio event on the camera itself that was anomalous for
20021
20078
  * THAT camera, loud, and heard while nothing visual was happening (D62).
20022
20079
  *
20080
+ * - `onboard` — the CAMERA's own firmware, paired with a decoded frame in
20081
+ * that frame's pixel space. A real root detector and a SPATIAL one: its
20082
+ * boxes are the same kind of fact `pipeline`'s are, produced by a different
20083
+ * detector.
20084
+ *
20085
+ * `onboard` was missing here while `DetectionSourceSchema` already had it, so
20086
+ * a persisted onboard track failed `safeParse` on read and came back with NO
20087
+ * source at all — which `isSpatialTrack` then admitted by accident, through the
20088
+ * `undefined` arm rather than by anybody's decision, and which `excludeSources`
20089
+ * could not name.
20090
+ *
20023
20091
  * The spatial subsystems (tracker association, occupancy count, re-id /
20024
20092
  * embedding, resurrection) MUST skip every synthetic source. Test for that
20025
- * with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
20026
- * check silently readmits every source added after it was written.
20093
+ * with `isSpatialTrack`, which allow-lists the spatial sources explicitly — a
20094
+ * `!== 'sensor'` check silently readmits every source added after it was
20095
+ * written.
20027
20096
  */
20028
20097
  var TrackSourceSchema = _enum([
20029
20098
  "pipeline",
20099
+ "onboard",
20030
20100
  "sensor",
20031
20101
  "audio"
20032
20102
  ]);
@@ -21409,6 +21479,33 @@ var RunReplayFrameProcessorResultSchema = object({ tracks: array(object({
21409
21479
  * solid is this track", cheaper than re-deriving it from a trajectory. */
21410
21480
  framesMatched: number().int()
21411
21481
  })).readonly() });
21482
+ /**
21483
+ * The long-term series a camera produces. A closed union so a typo cannot
21484
+ * invent one, and the ONE declaration of it — the addon's `AnalyticsLtsSeries`
21485
+ * is an alias of this, not a second list.
21486
+ */
21487
+ var AnalyticsLtsSeriesSchema = _enum([
21488
+ "motion",
21489
+ "audio-dbfs",
21490
+ "battery",
21491
+ "occupancy"
21492
+ ]);
21493
+ /**
21494
+ * One closed 5-minute bucket of a long-term series.
21495
+ *
21496
+ * `samples` is how many readings the bucket folded — it is the ACTIVITY for a
21497
+ * series like `motion`, and the confidence for one like `occupancy`. A bucket
21498
+ * that exists at all was measured; a bucket that is absent was not, which is a
21499
+ * different claim from zero and the reason this is a sparse series.
21500
+ */
21501
+ var LtsBucketSchema = object({
21502
+ scope: string(),
21503
+ bucketStart: number().int(),
21504
+ samples: number().int().nonnegative(),
21505
+ sum: number(),
21506
+ min: number(),
21507
+ max: number()
21508
+ });
21412
21509
  DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).readonly()), method(object({
21413
21510
  deviceId: number(),
21414
21511
  trackId: string()
@@ -21455,6 +21552,14 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
21455
21552
  kinds: array(string()).optional(),
21456
21553
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
21457
21554
  }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
21555
+ deviceId: number(),
21556
+ series: AnalyticsLtsSeriesSchema,
21557
+ /** Omit for EVERY scope of this series in the range. */
21558
+ scope: string().optional(),
21559
+ from: number(),
21560
+ to: number(),
21561
+ limit: number().int().positive().optional()
21562
+ }), array(LtsBucketSchema).readonly()), method(object({
21458
21563
  deviceId: number(),
21459
21564
  since: number(),
21460
21565
  until: number(),
@@ -22073,13 +22178,39 @@ var MotionSourceEnum = _enum([
22073
22178
  */
22074
22179
  var MotionSourcesSchema = array(MotionSourceEnum);
22075
22180
  /**
22076
- * Which root detectors a camera runs. Deliberately the SAME vocabulary
22077
- * post-analysis already tags every detection with (`DetectionSource`) rather
22078
- * than a second spelling of the same two ideas the value an operator picks
22079
- * here is the value that comes back on the track, the overlay and the debug
22080
- * row.
22181
+ * Which root detector a camera runs EXACTLY ONE.
22182
+ *
22183
+ * Deliberately the SAME vocabulary post-analysis already tags every detection
22184
+ * with (`DetectionSource`) rather than a second spelling of the same two
22185
+ * ideas: the value an operator picks here is the value that comes back on the
22186
+ * track, the overlay and the debug row.
22187
+ *
22188
+ * ## Why one, and why it is still an array
22189
+ *
22190
+ * Two roots on one camera is a capability nobody asked for and the operator has
22191
+ * since ruled out. It was never free: both planes feed the SAME per-device
22192
+ * stationary registry, both can promote the same parked object, and the two
22193
+ * detectors disagree about the same box by construction — which is the
22194
+ * disagreement D505 had to arbitrate. Removing the case removes the arbitration.
22195
+ *
22196
+ * The ARRAY shape survives because the stored settings and the attach payload
22197
+ * already speak it on every camera of every fleet, and a cap input that
22198
+ * suddenly refuses a stored value makes the camera un-attachable — a fail-closed
22199
+ * in the one direction that costs an operator their cameras. So the wire stays
22200
+ * tolerant and the TYPE is exact: a longer list is truncated to its first
22201
+ * element rather than refused, and `max(1)` is what every consumer can then
22202
+ * rely on.
22203
+ *
22204
+ * AT MOST one, not exactly one. The EMPTY list is a legal, deliberate pick —
22205
+ * a camera an operator left with no root detector — and `planDetectionSources`
22206
+ * has always said so: inventing a root for them is how a default acts without
22207
+ * anybody choosing it. Tightening this to `length(1)` broke that, and the
22208
+ * suite caught it.
22209
+ *
22210
+ * Measured before tightening (2026-09-15, this fleet, 6 h of attaches): 3327
22211
+ * `["pipeline"]` and 2 `["onboard"]`. Not one camera had two.
22081
22212
  */
22082
- var DetectionSourcesSchema = array(DetectionSourceSchema);
22213
+ var DetectionSourcesSchema = preprocess((value) => Array.isArray(value) && value.length > 1 ? value.slice(0, 1) : value, array(DetectionSourceSchema).max(1));
22083
22214
  /**
22084
22215
  * Input shape for `pipeline-runner.reportMotion` cap method. Exported
22085
22216
  * so cap-side consumers (the orchestrator forward, the runner addon's
@@ -22441,7 +22572,47 @@ var RunnerLocalMetricsSchema = object({
22441
22572
  queueDepth: number(),
22442
22573
  frameLazy: FrameLazyMetricsSchema.optional()
22443
22574
  });
22575
+ /**
22576
+ * Pipeline Runner capability — runtime detection workhorse.
22577
+ *
22578
+ * One instance per node. Receives camera assignments from
22579
+ * `addon-pipeline-orchestrator`, subscribes to the local stream-broker for
22580
+ * decoded frames, drains motion + detection queues, calls the local
22581
+ * `motion-detection` and `pipeline-executor` capabilities, and emits typed
22582
+ * `pipeline.inference-result` and `detection.motion-analysis` events on
22583
+ * the bus.
22584
+ *
22585
+ * Distinct from `pipeline-orchestrator` (the hub-side load balancer) — the
22586
+ * runner has zero knowledge of other agents, no global state, and never
22587
+ * makes assignment decisions itself.
22588
+ */
22589
+ /**
22590
+ * Why a runner declined to look at a camera out of band. Named rather than a
22591
+ * bare `false`: a camera asleep on battery (D173), one attached elsewhere, one
22592
+ * already mid-session and one already bursting are four different facts, and a
22593
+ * caller told only "no" cannot tell a protection from a bug.
22594
+ */
22595
+ var OccupancyBurstRefusalSchema = _enum([
22596
+ "not-attached",
22597
+ "battery-asleep",
22598
+ "not-watching",
22599
+ "already-bursting"
22600
+ ]);
22444
22601
  method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number()).readonly()), method(object({
22602
+ deviceId: number(),
22603
+ /** How many frames to collect. The caller sizes this against what its
22604
+ * own confirmation needs; the runner clamps it to the field's range. */
22605
+ frames: number().int().positive().optional(),
22606
+ /** Why, for the log. A burst nobody can attribute is a cost nobody can
22607
+ * defend — the periodic one at least has a timer to point at. */
22608
+ reason: string()
22609
+ }), object({
22610
+ started: boolean(),
22611
+ refusedBecause: OccupancyBurstRefusalSchema.optional()
22612
+ }), {
22613
+ auth: "admin",
22614
+ kind: "mutation"
22615
+ }), method(object({
22445
22616
  handle: FrameHandleSchema,
22446
22617
  bbox: NativeCropBboxSchema,
22447
22618
  maxWidth: number().int().positive().optional(),
@@ -34593,7 +34764,28 @@ var PerScopeBreakdownSchema = object({
34593
34764
  /** Total tracked objects in this scope (frame / zone / unzoned). */
34594
34765
  totalObjects: number().int().nonnegative(),
34595
34766
  /** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
34596
- byClass: record(string(), number().int().nonnegative())
34767
+ byClass: record(string(), number().int().nonnegative()),
34768
+ /**
34769
+ * Of `totalObjects`, how many are STANDING — objects the census holds,
34770
+ * present but not going anywhere (a parked car, a bin, a statue).
34771
+ *
34772
+ * The operator's question about a scene has two halves and they answer
34773
+ * different things: "what is parked here" is stable for hours and must not
34774
+ * flap, while "who is walking through" is the thing an alert is about. One
34775
+ * number could only serve one of them, and `totalObjects` served the first
34776
+ * badly — a passer-by moved it and every occupancy rule saw a change.
34777
+ *
34778
+ * `standing + transient === totalObjects`, always. The total keeps its old
34779
+ * meaning exactly, so no rule written against it changes behaviour.
34780
+ *
34781
+ * ABSENT means UNKNOWN, never zero (D393): a snapshot from a node older than
34782
+ * this field cannot say, and a reader that folds absence in as 0 reports "no
34783
+ * parked objects here" about a camera whose lot is full.
34784
+ */
34785
+ standingObjects: number().int().nonnegative().optional(),
34786
+ /** Of `totalObjects`, how many are passing through. See `standingObjects` —
34787
+ * same absence rule. */
34788
+ transientObjects: number().int().nonnegative().optional()
34597
34789
  });
34598
34790
  var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
34599
34791
  zoneId: string(),
@@ -34716,6 +34908,23 @@ var HistoryPointSchema = object({
34716
34908
  * triple covers the three spatial scopes (per-zone, frame-wide,
34717
34909
  * outside-any-zone). Crossing them with `className?` gives the full
34718
34910
  * combinatorial coverage the operator UI requested.
34911
+ *
34912
+ * ## ⚠️ The three history methods are DEPRECATED
34913
+ *
34914
+ * They are served from a 60-MINUTE in-memory ring. It dies with the process,
34915
+ * so after a restart they report nothing about a night that did happen, and any
34916
+ * window past an hour is silently clamped — a caller cannot tell a clamp from
34917
+ * an empty scene. The presets in the occupancy chart existed to hide that
34918
+ * horizon rather than to answer a question.
34919
+ *
34920
+ * Use `pipelineAnalytics.readLongTermSeries({ series: 'occupancy', scope })`
34921
+ * instead. Those rows are already written for every camera, ~60 bytes per
34922
+ * (camera, scope, 5-minute bucket), kept for a year, and survive a restart.
34923
+ * Spell the scope with `occupancyScope(zoneId?, className?)` — the same
34924
+ * function the writer uses, so the two cannot drift.
34925
+ *
34926
+ * The ring stays until these three go, and it is the only thing left reading
34927
+ * it. Do not add a fourth caller.
34719
34928
  */
34720
34929
  var zoneAnalyticsCapability = {
34721
34930
  name: "zone-analytics",
@@ -39575,6 +39784,12 @@ Object.freeze({
39575
39784
  addonId: null,
39576
39785
  access: "view"
39577
39786
  },
39787
+ "notificationRules.getClipRetentionAsks": {
39788
+ capName: "notification-rules",
39789
+ capScope: "system",
39790
+ addonId: null,
39791
+ access: "view"
39792
+ },
39578
39793
  "notificationRules.getConditionCatalog": {
39579
39794
  capName: "notification-rules",
39580
39795
  capScope: "system",
@@ -40085,6 +40300,12 @@ Object.freeze({
40085
40300
  addonId: null,
40086
40301
  access: "create"
40087
40302
  },
40303
+ "pipelineAnalytics.readLongTermSeries": {
40304
+ capName: "pipeline-analytics",
40305
+ capScope: "device",
40306
+ addonId: null,
40307
+ access: "view"
40308
+ },
40088
40309
  "pipelineAnalytics.rebuildObjectEmbeddings": {
40089
40310
  capName: "pipeline-analytics",
40090
40311
  capScope: "device",
@@ -40757,6 +40978,12 @@ Object.freeze({
40757
40978
  addonId: null,
40758
40979
  access: "create"
40759
40980
  },
40981
+ "pipelineRunner.requestOccupancyBurst": {
40982
+ capName: "pipeline-runner",
40983
+ capScope: "system",
40984
+ addonId: null,
40985
+ access: "create"
40986
+ },
40760
40987
  "pipelineRunner.runDetailSubtree": {
40761
40988
  capName: "pipeline-runner",
40762
40989
  capScope: "system",
@@ -43901,6 +44128,11 @@ Object.freeze({
43901
44128
  form: "single",
43902
44129
  optional: false
43903
44130
  }],
44131
+ "pipelineAnalytics.readLongTermSeries": [{
44132
+ name: "deviceId",
44133
+ form: "single",
44134
+ optional: false
44135
+ }],
43904
44136
  "pipelineAnalytics.rebuildObjectEmbeddings": [{
43905
44137
  name: "deviceId",
43906
44138
  form: "single",
@@ -44091,6 +44323,11 @@ Object.freeze({
44091
44323
  form: "single",
44092
44324
  optional: false
44093
44325
  }],
44326
+ "pipelineRunner.requestOccupancyBurst": [{
44327
+ name: "deviceId",
44328
+ form: "single",
44329
+ optional: false
44330
+ }],
44094
44331
  "pipelineRunner.runDetailSubtree": [{
44095
44332
  name: "deviceId",
44096
44333
  form: "single",
@@ -145318,6 +145555,29 @@ function buildOsdSection(snapshot, values, opts) {
145318
145555
  };
145319
145556
  }
145320
145557
  //#endregion
145558
+ //#region src/snapshot-wake-gate.ts
145559
+ /**
145560
+ * Decide whether a snapshot read may reach the camera's socket.
145561
+ *
145562
+ * Pure, and deliberately total: three inputs, no clock, no cooldown, no
145563
+ * fallible read (D49 — a gate that can be talked out of its answer by a
145564
+ * stale timestamp is not a gate).
145565
+ */
145566
+ function decideSnapshotCameraAccess(input) {
145567
+ if (!input.isBattery || !input.sleeping) return {
145568
+ kind: "proceed",
145569
+ stampCooldown: false
145570
+ };
145571
+ if (!input.force) return {
145572
+ kind: "refuse",
145573
+ reason: "sleeping"
145574
+ };
145575
+ return {
145576
+ kind: "proceed",
145577
+ stampCooldown: true
145578
+ };
145579
+ }
145580
+ //#endregion
145321
145581
  //#region src/raw-state.ts
145322
145582
  /**
145323
145583
  * Source tag for every raw-state blob this provider emits.
@@ -150112,9 +150372,9 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
150112
150372
  }
150113
150373
  registerNativeCapabilities() {
150114
150374
  this.ctx.registerNativeCap(snapshotCapability, {
150115
- getSnapshot: async ({ deviceId }) => {
150375
+ getSnapshot: async ({ deviceId, force }) => {
150116
150376
  if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
150117
- return this.fetchSnapshotWithSingleFlight();
150377
+ return this.fetchSnapshotWithSingleFlight(force === true);
150118
150378
  },
150119
150379
  invalidateCache: async () => {}
150120
150380
  });
@@ -150557,33 +150817,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
150557
150817
  status: this.state.battery
150558
150818
  }));
150559
150819
  }
150560
- /**
150561
- * Gate for a PROACTIVE camera read, checked BEFORE `ensureApi()`.
150562
- *
150563
- * The login itself is the wake. On a sleeping UDP/battery camera the
150564
- * lib's discovery + handshake nudges the firmware awake (same reason
150565
- * `refreshParentSettingsSnapshot` refuses to call `ensureApi` while
150566
- * asleep), so gating only the explicit `wakeUp()` call is useless —
150567
- * by the time we reach it the camera is already up. Production logs
150568
- * showed exactly that: a background read produced a full
150569
- * `Connecting to Reolink` → BCUDP discovery → `battery sleep state
150570
- * committed` (awake) → whole refresh cascade, on a camera that had
150571
- * been asleep for six minutes.
150572
- *
150573
- * Returns `false` when the caller must serve cache / bail out without
150574
- * touching the socket. Stamps the cooldown when it does let a wake
150575
- * through, so "at most one proactive wake per
150576
- * `PROACTIVE_WAKE_COOLDOWN_MS`" holds across ALL proactive callers
150577
- * rather than per-caller.
150578
- *
150579
- * Demand-driven paths never call this — see `canProactivelyWake`.
150580
- */
150581
- allowProactiveCameraAccess(reason) {
150582
- if (!this.isBattery || !this.sleeping) return true;
150583
- if (!this.canProactivelyWake(reason)) return false;
150584
- this.markWakeIssued();
150585
- return true;
150586
- }
150587
150820
  updateBatteryCache(info) {
150588
150821
  const mapped = this.mapBatteryInfo(info);
150589
150822
  const now = Date.now();
@@ -150863,12 +151096,27 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
150863
151096
  }
150864
151097
  await this.refreshCameraEmailConfig().catch(() => {});
150865
151098
  }
150866
- async fetchSnapshotWithSingleFlight() {
151099
+ /**
151100
+ * @param force - the caller's `snapshot.getSnapshot({ force })`. THE gate:
151101
+ * without it a sleeping battery camera is never touched, whatever the
151102
+ * cache holds. See `snapshot-wake-gate.ts` for why the flag — and not a
151103
+ * cooldown — is what decides.
151104
+ */
151105
+ async fetchSnapshotWithSingleFlight(force) {
150867
151106
  if (this.snapshotInFlight) return this.snapshotInFlight;
150868
- if (!this.allowProactiveCameraAccess("snapshot")) {
150869
- this.ctx.logger.debug("snapshot: skipped — sleeping battery cam inside wake cooldown", { tags: { deviceId: this.id } });
151107
+ const access = decideSnapshotCameraAccess({
151108
+ isBattery: this.isBattery,
151109
+ sleeping: this.sleeping,
151110
+ force
151111
+ });
151112
+ if (access.kind === "refuse") {
151113
+ this.ctx.logger.info("snapshot: refused — battery cam asleep and no operator force", {
151114
+ tags: { deviceId: this.id },
151115
+ meta: { reason: access.reason }
151116
+ });
150870
151117
  return null;
150871
151118
  }
151119
+ if (access.stampCooldown) this.markWakeIssued();
150872
151120
  const promise = (async () => {
150873
151121
  let api;
150874
151122
  try {
@@ -152591,11 +152839,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
152591
152839
  };
152592
152840
  this.ctx.registerNativeCap(nativeObjectDetectionCapability, provider);
152593
152841
  this.registerStreamSignalsCap();
152594
- if (this.runtimeState.getCapState(CAP_NAME) === void 0) this.runtimeState.setCapState(CAP_NAME, {
152595
- ...buildEmptyState(),
152596
- lastFetchedAt: Date.now()
152597
- });
152598
- else this.runtimeState.patchCapState(CAP_NAME, { supportedClasses: buildSupportedClasses() });
152842
+ if (this.runtimeState.getCapState(CAP_NAME) === void 0) {
152843
+ this.runtimeState.setCapState(CAP_NAME, {
152844
+ ...buildEmptyState(),
152845
+ lastFetchedAt: Date.now()
152846
+ });
152847
+ this.ctx.logger.info("Reolink onboard AI: no restored state — cold-start default (OFF)", { tags: { deviceId: this.id } });
152848
+ } else this.runtimeState.patchCapState(CAP_NAME, { supportedClasses: buildSupportedClasses() });
152599
152849
  this.ctx.logger.info("Reolink native-object-detection cap registered", { tags: { deviceId: this.id } });
152600
152850
  }
152601
152851
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.130",
3
+ "version": "1.2.132",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",
@@ -85,7 +85,7 @@
85
85
  "build": "vite build",
86
86
  "dev": "vite build --watch",
87
87
  "typecheck": "tsc --noEmit",
88
- "publish": "npm publish --access public"
88
+ "publish:npm": "npm publish --access public"
89
89
  },
90
90
  "dependencies": {
91
91
  "@apocaliss92/nodelink-js": "^0.7.0"