@camstack/addon-provider-reolink 1.2.131 → 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.
package/dist/addon.js CHANGED
@@ -20082,13 +20082,26 @@ var TrackAudioLabelSchema = object({
20082
20082
  * - `audio` — an audio event on the camera itself that was anomalous for
20083
20083
  * THAT camera, loud, and heard while nothing visual was happening (D62).
20084
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
+ *
20085
20096
  * The spatial subsystems (tracker association, occupancy count, re-id /
20086
20097
  * embedding, resurrection) MUST skip every synthetic source. Test for that
20087
- * with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
20088
- * 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.
20089
20101
  */
20090
20102
  var TrackSourceSchema = _enum([
20091
20103
  "pipeline",
20104
+ "onboard",
20092
20105
  "sensor",
20093
20106
  "audio"
20094
20107
  ]);
@@ -22170,13 +22183,39 @@ var MotionSourceEnum = _enum([
22170
22183
  */
22171
22184
  var MotionSourcesSchema = array(MotionSourceEnum);
22172
22185
  /**
22173
- * Which root detectors a camera runs. Deliberately the SAME vocabulary
22174
- * post-analysis already tags every detection with (`DetectionSource`) rather
22175
- * than a second spelling of the same two ideas the value an operator picks
22176
- * here is the value that comes back on the track, the overlay and the debug
22177
- * 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.
22178
22217
  */
22179
- var DetectionSourcesSchema = array(DetectionSourceSchema);
22218
+ var DetectionSourcesSchema = preprocess((value) => Array.isArray(value) && value.length > 1 ? value.slice(0, 1) : value, array(DetectionSourceSchema).max(1));
22180
22219
  /**
22181
22220
  * Input shape for `pipeline-runner.reportMotion` cap method. Exported
22182
22221
  * so cap-side consumers (the orchestrator forward, the runner addon's
@@ -145521,6 +145560,29 @@ function buildOsdSection(snapshot, values, opts) {
145521
145560
  };
145522
145561
  }
145523
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
145524
145586
  //#region src/raw-state.ts
145525
145587
  /**
145526
145588
  * Source tag for every raw-state blob this provider emits.
@@ -150315,9 +150377,9 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
150315
150377
  }
150316
150378
  registerNativeCapabilities() {
150317
150379
  this.ctx.registerNativeCap(snapshotCapability, {
150318
- getSnapshot: async ({ deviceId }) => {
150380
+ getSnapshot: async ({ deviceId, force }) => {
150319
150381
  if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
150320
- return this.fetchSnapshotWithSingleFlight();
150382
+ return this.fetchSnapshotWithSingleFlight(force === true);
150321
150383
  },
150322
150384
  invalidateCache: async () => {}
150323
150385
  });
@@ -150760,33 +150822,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
150760
150822
  status: this.state.battery
150761
150823
  }));
150762
150824
  }
150763
- /**
150764
- * Gate for a PROACTIVE camera read, checked BEFORE `ensureApi()`.
150765
- *
150766
- * The login itself is the wake. On a sleeping UDP/battery camera the
150767
- * lib's discovery + handshake nudges the firmware awake (same reason
150768
- * `refreshParentSettingsSnapshot` refuses to call `ensureApi` while
150769
- * asleep), so gating only the explicit `wakeUp()` call is useless —
150770
- * by the time we reach it the camera is already up. Production logs
150771
- * showed exactly that: a background read produced a full
150772
- * `Connecting to Reolink` → BCUDP discovery → `battery sleep state
150773
- * committed` (awake) → whole refresh cascade, on a camera that had
150774
- * been asleep for six minutes.
150775
- *
150776
- * Returns `false` when the caller must serve cache / bail out without
150777
- * touching the socket. Stamps the cooldown when it does let a wake
150778
- * through, so "at most one proactive wake per
150779
- * `PROACTIVE_WAKE_COOLDOWN_MS`" holds across ALL proactive callers
150780
- * rather than per-caller.
150781
- *
150782
- * Demand-driven paths never call this — see `canProactivelyWake`.
150783
- */
150784
- allowProactiveCameraAccess(reason) {
150785
- if (!this.isBattery || !this.sleeping) return true;
150786
- if (!this.canProactivelyWake(reason)) return false;
150787
- this.markWakeIssued();
150788
- return true;
150789
- }
150790
150825
  updateBatteryCache(info) {
150791
150826
  const mapped = this.mapBatteryInfo(info);
150792
150827
  const now = Date.now();
@@ -151066,12 +151101,27 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
151066
151101
  }
151067
151102
  await this.refreshCameraEmailConfig().catch(() => {});
151068
151103
  }
151069
- 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) {
151070
151111
  if (this.snapshotInFlight) return this.snapshotInFlight;
151071
- if (!this.allowProactiveCameraAccess("snapshot")) {
151072
- 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
+ });
151073
151122
  return null;
151074
151123
  }
151124
+ if (access.stampCooldown) this.markWakeIssued();
151075
151125
  const promise = (async () => {
151076
151126
  let api;
151077
151127
  try {
package/dist/addon.mjs CHANGED
@@ -20077,13 +20077,26 @@ var TrackAudioLabelSchema = object({
20077
20077
  * - `audio` — an audio event on the camera itself that was anomalous for
20078
20078
  * THAT camera, loud, and heard while nothing visual was happening (D62).
20079
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
+ *
20080
20091
  * The spatial subsystems (tracker association, occupancy count, re-id /
20081
20092
  * embedding, resurrection) MUST skip every synthetic source. Test for that
20082
- * with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
20083
- * 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.
20084
20096
  */
20085
20097
  var TrackSourceSchema = _enum([
20086
20098
  "pipeline",
20099
+ "onboard",
20087
20100
  "sensor",
20088
20101
  "audio"
20089
20102
  ]);
@@ -22165,13 +22178,39 @@ var MotionSourceEnum = _enum([
22165
22178
  */
22166
22179
  var MotionSourcesSchema = array(MotionSourceEnum);
22167
22180
  /**
22168
- * Which root detectors a camera runs. Deliberately the SAME vocabulary
22169
- * post-analysis already tags every detection with (`DetectionSource`) rather
22170
- * than a second spelling of the same two ideas the value an operator picks
22171
- * here is the value that comes back on the track, the overlay and the debug
22172
- * 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.
22173
22212
  */
22174
- var DetectionSourcesSchema = array(DetectionSourceSchema);
22213
+ var DetectionSourcesSchema = preprocess((value) => Array.isArray(value) && value.length > 1 ? value.slice(0, 1) : value, array(DetectionSourceSchema).max(1));
22175
22214
  /**
22176
22215
  * Input shape for `pipeline-runner.reportMotion` cap method. Exported
22177
22216
  * so cap-side consumers (the orchestrator forward, the runner addon's
@@ -145516,6 +145555,29 @@ function buildOsdSection(snapshot, values, opts) {
145516
145555
  };
145517
145556
  }
145518
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
145519
145581
  //#region src/raw-state.ts
145520
145582
  /**
145521
145583
  * Source tag for every raw-state blob this provider emits.
@@ -150310,9 +150372,9 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
150310
150372
  }
150311
150373
  registerNativeCapabilities() {
150312
150374
  this.ctx.registerNativeCap(snapshotCapability, {
150313
- getSnapshot: async ({ deviceId }) => {
150375
+ getSnapshot: async ({ deviceId, force }) => {
150314
150376
  if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
150315
- return this.fetchSnapshotWithSingleFlight();
150377
+ return this.fetchSnapshotWithSingleFlight(force === true);
150316
150378
  },
150317
150379
  invalidateCache: async () => {}
150318
150380
  });
@@ -150755,33 +150817,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
150755
150817
  status: this.state.battery
150756
150818
  }));
150757
150819
  }
150758
- /**
150759
- * Gate for a PROACTIVE camera read, checked BEFORE `ensureApi()`.
150760
- *
150761
- * The login itself is the wake. On a sleeping UDP/battery camera the
150762
- * lib's discovery + handshake nudges the firmware awake (same reason
150763
- * `refreshParentSettingsSnapshot` refuses to call `ensureApi` while
150764
- * asleep), so gating only the explicit `wakeUp()` call is useless —
150765
- * by the time we reach it the camera is already up. Production logs
150766
- * showed exactly that: a background read produced a full
150767
- * `Connecting to Reolink` → BCUDP discovery → `battery sleep state
150768
- * committed` (awake) → whole refresh cascade, on a camera that had
150769
- * been asleep for six minutes.
150770
- *
150771
- * Returns `false` when the caller must serve cache / bail out without
150772
- * touching the socket. Stamps the cooldown when it does let a wake
150773
- * through, so "at most one proactive wake per
150774
- * `PROACTIVE_WAKE_COOLDOWN_MS`" holds across ALL proactive callers
150775
- * rather than per-caller.
150776
- *
150777
- * Demand-driven paths never call this — see `canProactivelyWake`.
150778
- */
150779
- allowProactiveCameraAccess(reason) {
150780
- if (!this.isBattery || !this.sleeping) return true;
150781
- if (!this.canProactivelyWake(reason)) return false;
150782
- this.markWakeIssued();
150783
- return true;
150784
- }
150785
150820
  updateBatteryCache(info) {
150786
150821
  const mapped = this.mapBatteryInfo(info);
150787
150822
  const now = Date.now();
@@ -151061,12 +151096,27 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
151061
151096
  }
151062
151097
  await this.refreshCameraEmailConfig().catch(() => {});
151063
151098
  }
151064
- 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) {
151065
151106
  if (this.snapshotInFlight) return this.snapshotInFlight;
151066
- if (!this.allowProactiveCameraAccess("snapshot")) {
151067
- 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
+ });
151068
151117
  return null;
151069
151118
  }
151119
+ if (access.stampCooldown) this.markWakeIssued();
151070
151120
  const promise = (async () => {
151071
151121
  let api;
151072
151122
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.131",
3
+ "version": "1.2.132",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",