@camstack/addon-provider-reolink 1.2.5 → 1.2.7

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 +308 -153
  2. package/dist/addon.mjs +308 -153
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -222620,6 +222620,33 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
222620
222620
  markWakeIssued() {
222621
222621
  this.lastProactiveWakeAt = Date.now();
222622
222622
  }
222623
+ /**
222624
+ * Gate for a PROACTIVE camera read, checked BEFORE `ensureApi()`.
222625
+ *
222626
+ * The login itself is the wake. On a sleeping UDP/battery camera the
222627
+ * lib's discovery + handshake nudges the firmware awake (same reason
222628
+ * `refreshParentSettingsSnapshot` refuses to call `ensureApi` while
222629
+ * asleep), so gating only the explicit `wakeUp()` call is useless —
222630
+ * by the time we reach it the camera is already up. Production logs
222631
+ * showed exactly that: a background read produced a full
222632
+ * `Connecting to Reolink` → BCUDP discovery → `battery sleep state
222633
+ * committed` (awake) → whole refresh cascade, on a camera that had
222634
+ * been asleep for six minutes.
222635
+ *
222636
+ * Returns `false` when the caller must serve cache / bail out without
222637
+ * touching the socket. Stamps the cooldown when it does let a wake
222638
+ * through, so "at most one proactive wake per
222639
+ * `PROACTIVE_WAKE_COOLDOWN_MS`" holds across ALL proactive callers
222640
+ * rather than per-caller.
222641
+ *
222642
+ * Demand-driven paths never call this — see `canProactivelyWake`.
222643
+ */
222644
+ allowProactiveCameraAccess(reason) {
222645
+ if (!this.isBattery || !this.sleeping) return true;
222646
+ if (!this.canProactivelyWake(reason)) return false;
222647
+ this.markWakeIssued();
222648
+ return true;
222649
+ }
222623
222650
  updateBatteryCache(info) {
222624
222651
  this.setCapSlice(batteryCapability, this.mapBatteryInfo(info));
222625
222652
  }
@@ -222865,6 +222892,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
222865
222892
  }
222866
222893
  async fetchSnapshotWithSingleFlight() {
222867
222894
  if (this.snapshotInFlight) return this.snapshotInFlight;
222895
+ if (!this.allowProactiveCameraAccess("snapshot")) {
222896
+ this.ctx.logger.debug("snapshot: skipped — sleeping battery cam inside wake cooldown", { tags: { deviceId: this.id } });
222897
+ return null;
222898
+ }
222868
222899
  const promise = (async () => {
222869
222900
  let api;
222870
222901
  try {
@@ -222882,23 +222913,16 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
222882
222913
  } catch {
222883
222914
  return true;
222884
222915
  }
222885
- })()) {
222886
- if (!this.canProactivelyWake("snapshot")) {
222887
- this.ctx.logger.debug("snapshot: skipped — sleeping battery cam inside wake cooldown", { tags: { deviceId: this.id } });
222888
- return null;
222889
- }
222890
- this.markWakeIssued();
222891
- try {
222892
- await api.wakeUp(this.getChannel(), {
222893
- waitAfterWakeMs: 1500,
222894
- attempts: 2
222895
- });
222896
- } catch (err) {
222897
- this.ctx.logger.debug("snapshot: pre-wake failed (will still try getSnapshot)", {
222898
- tags: { deviceId: this.id },
222899
- meta: { error: err instanceof Error ? err.message : String(err) }
222900
- });
222901
- }
222916
+ })()) try {
222917
+ await api.wakeUp(this.getChannel(), {
222918
+ waitAfterWakeMs: 1500,
222919
+ attempts: 2
222920
+ });
222921
+ } catch (err) {
222922
+ this.ctx.logger.debug("snapshot: pre-wake failed (will still try getSnapshot)", {
222923
+ tags: { deviceId: this.id },
222924
+ meta: { error: err instanceof Error ? err.message : String(err) }
222925
+ });
222902
222926
  }
222903
222927
  const tryOnce = async (timeoutMs) => {
222904
222928
  const buf = await api.getSnapshot(this.getChannel(), {
@@ -223441,6 +223465,97 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223441
223465
  }
223442
223466
  }
223443
223467
  /**
223468
+ * Per-cap "warm the cache while the camera is up" closures, registered
223469
+ * by each device-config cap alongside its provider and run by
223470
+ * `onWakeTransition`.
223471
+ *
223472
+ * This is the OTHER half of the read discipline. Gating reads stops us
223473
+ * waking the camera, but on its own it leaves the caches empty — and an
223474
+ * empty cache is what the operator actually sees: the State panel reads
223475
+ * the runtime-state slices (`deviceState.getAllSnapshots`), so a
223476
+ * sleeping camera whose slices were never written shows null for
223477
+ * everything. Before the gate, the admin UI's 2.5s aggregate poll kept
223478
+ * those slices warm by waking the camera every time — the very bug we
223479
+ * fixed. So the refresh has to move to the moment the camera is
223480
+ * ALREADY up, which is exactly the wake transition.
223481
+ */
223482
+ capWarmers = /* @__PURE__ */ new Map();
223483
+ /** How fresh a slice must be for the wake warm-up to skip it. A battery
223484
+ * cam can wake many times an hour on motion; re-probing every cap on
223485
+ * every wake would spend the (~14s) window on data we already have. */
223486
+ static WAKE_WARM_MAX_AGE_MS = 10 * 6e4;
223487
+ registerCapWarmer(capName, warm) {
223488
+ this.capWarmers.set(capName, warm);
223489
+ }
223490
+ /**
223491
+ * Refresh the device-config cap caches during a wake window.
223492
+ *
223493
+ * SERIALIZED, like every other wake-path refresh — concurrent Baichuan
223494
+ * traffic on the single BCUDP stream starves the heavy reads. Each cap
223495
+ * is skipped when its slice is still fresh, and each failure is
223496
+ * swallowed: the window is short, partial progress accumulates across
223497
+ * wakes because the slices persist.
223498
+ */
223499
+ async warmCapSlicesOnWake() {
223500
+ const now = Date.now();
223501
+ let warmed = 0;
223502
+ let skipped = 0;
223503
+ for (const [capName, warm] of this.capWarmers) {
223504
+ const slice = this.runtimeState.getCapState(capName);
223505
+ const fetchedAt = typeof slice?.lastFetchedAt === "number" ? slice.lastFetchedAt : 0;
223506
+ if (fetchedAt > 0 && now - fetchedAt < ReolinkCamera.WAKE_WARM_MAX_AGE_MS) {
223507
+ skipped += 1;
223508
+ continue;
223509
+ }
223510
+ try {
223511
+ await warm();
223512
+ warmed += 1;
223513
+ } catch (err) {
223514
+ this.ctx.logger.debug("wake warm-up: cap refresh failed — retrying on next wake", {
223515
+ tags: { deviceId: this.id },
223516
+ meta: {
223517
+ capName,
223518
+ error: err instanceof Error ? err.message : String(err)
223519
+ }
223520
+ });
223521
+ }
223522
+ }
223523
+ this.ctx.logger.info("wake warm-up: cap caches refreshed", {
223524
+ tags: { deviceId: this.id },
223525
+ meta: {
223526
+ warmed,
223527
+ skipped,
223528
+ total: this.capWarmers.size
223529
+ }
223530
+ });
223531
+ }
223532
+ /**
223533
+ * Ask the snapshot wrapper for a frame while the camera is up, so its
223534
+ * cache holds something recent to serve for the whole sleep window.
223535
+ *
223536
+ * The wrapper already does the right thing on a miss — `resolveOutcome`
223537
+ * falls back to the stale frame rather than blanking the UI — but only
223538
+ * if a frame was ever captured. With reads gated, nothing captures one
223539
+ * any more: the tile stayed empty for as long as the camera slept.
223540
+ * `force: true` bypasses the wrapper's freshness gate; our own
223541
+ * proactive gate lets it through because `sleeping` is false here.
223542
+ * Mirrors the reference's `updateBatteryAndSnapshot`.
223543
+ */
223544
+ async warmSnapshotOnWake() {
223545
+ try {
223546
+ await this.ctx.api.snapshot.getSnapshot.query({
223547
+ deviceId: this.id,
223548
+ force: true
223549
+ });
223550
+ this.ctx.logger.debug("wake warm-up: snapshot cache refreshed", { tags: { deviceId: this.id } });
223551
+ } catch (err) {
223552
+ this.ctx.logger.debug("wake warm-up: snapshot refresh failed", {
223553
+ tags: { deviceId: this.id },
223554
+ meta: { error: err instanceof Error ? err.message : String(err) }
223555
+ });
223556
+ }
223557
+ }
223558
+ /**
223444
223559
  * Wrap a cap's `refreshFromCamera` so the READ side (the bridge's
223445
223560
  * stale-check behind `getStatus`) never wakes a sleeping battery cam.
223446
223561
  * The bridge then projects whatever the slice last held — which is
@@ -223545,6 +223660,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223545
223660
  this.ctx.eventBus.emit(createEvent(EventCategory.StreamParamsChanged, this.eventSource(), { deviceId: this.id }));
223546
223661
  }
223547
223662
  };
223663
+ this.registerCapWarmer(CAP_NAME, async () => {
223664
+ await provider.getOptions({ deviceId: this.id });
223665
+ await refreshFromCamera();
223666
+ });
223548
223667
  this.ctx.registerNativeCap(streamParamsCapability, provider);
223549
223668
  this.ctx.logger.info("Reolink stream-params cap registered", { tags: { deviceId: this.id } });
223550
223669
  }
@@ -223735,6 +223854,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223735
223854
  await refreshFromCamera();
223736
223855
  }
223737
223856
  };
223857
+ this.registerCapWarmer(CAP_NAME, async () => {
223858
+ await provider.getOptions({ deviceId: this.id });
223859
+ await refreshFromCamera();
223860
+ });
223738
223861
  this.ctx.registerNativeCap(motionZonesCapability, provider);
223739
223862
  this.ctx.logger.info("Reolink motion-zones cap registered", { tags: { deviceId: this.id } });
223740
223863
  }
@@ -223870,6 +223993,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223870
223993
  });
223871
223994
  }
223872
223995
  };
223996
+ this.registerCapWarmer(CAP_NAME, async () => {
223997
+ await provider.getOptions({ deviceId: this.id });
223998
+ await refreshFromCamera();
223999
+ });
223873
224000
  this.ctx.registerNativeCap(privacyMaskCapability, provider);
223874
224001
  this.ctx.logger.info("Reolink privacy-mask cap registered (read + enable + zone write)", { tags: { deviceId: this.id } });
223875
224002
  }
@@ -224005,6 +224132,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
224005
224132
  await refreshFromCamera();
224006
224133
  }
224007
224134
  };
224135
+ this.registerCapWarmer(CAP_NAME, async () => {
224136
+ await provider.getOptions({ deviceId: this.id });
224137
+ await refreshFromCamera();
224138
+ });
224008
224139
  this.ctx.registerNativeCap(dayNightCapability, provider);
224009
224140
  this.ctx.logger.info("Reolink day-night cap registered", { tags: { deviceId: this.id } });
224010
224141
  }
@@ -224142,6 +224273,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
224142
224273
  await refreshFromCamera();
224143
224274
  }
224144
224275
  };
224276
+ this.registerCapWarmer(CAP_NAME, async () => {
224277
+ await provider.getOptions({ deviceId: this.id });
224278
+ await refreshFromCamera();
224279
+ });
224145
224280
  this.ctx.registerNativeCap(imageSettingsCapability, provider);
224146
224281
  this.ctx.logger.info("Reolink image-settings cap registered", { tags: { deviceId: this.id } });
224147
224282
  }
@@ -224411,6 +224546,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
224411
224546
  await refreshFromCamera();
224412
224547
  }
224413
224548
  };
224549
+ this.registerCapWarmer(CAP_NAME, refreshFromCamera);
224414
224550
  this.ctx.registerNativeCap(ptzAutotrackCapability, provider);
224415
224551
  this.ctx.logger.info("Reolink ptz-autotrack cap registered", { tags: { deviceId: this.id } });
224416
224552
  }
@@ -224418,148 +224554,154 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
224418
224554
  if (this.ptzRegistered) return;
224419
224555
  if (!this.getProbeFlags().hasPtz) return;
224420
224556
  this.ptzRegistered = true;
224421
- this.ctx.registerNativeCap(ptzCapability, {
224422
- move: async ({ deviceId, pan, tilt, zoom, speed }) => {
224423
- if (deviceId !== this.id) return;
224424
- await this.runPtz(pan, tilt, zoom, speed, false);
224425
- },
224426
- continuousMove: async ({ deviceId, pan, tilt, zoom, speed }) => {
224427
- if (deviceId !== this.id) return;
224428
- await this.runPtz(pan, tilt, zoom, speed, true);
224429
- },
224430
- stop: async ({ deviceId }) => {
224431
- if (deviceId !== this.id) return;
224432
- const api = await this.ensureApi();
224433
- const channel = this.getChannel();
224434
- try {
224435
- await api.ptz(channel, {
224436
- action: "stop",
224437
- command: "Up"
224438
- });
224439
- } catch {}
224440
- },
224441
- getPresets: async ({ deviceId }) => {
224442
- if (deviceId !== this.id) return [];
224443
- try {
224557
+ {
224558
+ const ptzProvider = {
224559
+ move: async ({ deviceId, pan, tilt, zoom, speed }) => {
224560
+ if (deviceId !== this.id) return;
224561
+ await this.runPtz(pan, tilt, zoom, speed, false);
224562
+ },
224563
+ continuousMove: async ({ deviceId, pan, tilt, zoom, speed }) => {
224564
+ if (deviceId !== this.id) return;
224565
+ await this.runPtz(pan, tilt, zoom, speed, true);
224566
+ },
224567
+ stop: async ({ deviceId }) => {
224568
+ if (deviceId !== this.id) return;
224444
224569
  const api = await this.ensureApi();
224445
224570
  const channel = this.getChannel();
224446
- return (await api.getPtzPresets(channel)).map((p) => ({
224447
- id: String(p.id),
224448
- name: p.name
224449
- }));
224450
- } catch {
224451
- return [];
224452
- }
224453
- },
224454
- goToPreset: async ({ deviceId, presetId }) => {
224455
- if (deviceId !== this.id) return;
224456
- const api = await this.ensureApi();
224457
- const channel = this.getChannel();
224458
- const id = Number(presetId);
224459
- if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224460
- await api.moveToPtzPreset(channel, id);
224461
- },
224462
- savePreset: async ({ deviceId, presetId, name }) => {
224463
- if (deviceId !== this.id) return;
224464
- const api = await this.ensureApi();
224465
- const channel = this.getChannel();
224466
- const id = Number(presetId);
224467
- if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224468
- await api.setPtzPreset(channel, id, name);
224469
- },
224470
- deletePreset: async ({ deviceId, presetId }) => {
224471
- if (deviceId !== this.id) return;
224472
- const api = await this.ensureApi();
224473
- const channel = this.getChannel();
224474
- const id = Number(presetId);
224475
- if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224476
- await api.deletePtzPreset(channel, id);
224477
- },
224478
- getOptions: async ({ deviceId }) => {
224479
- if (deviceId !== this.id) return {
224480
- hasPan: false,
224481
- hasTilt: false,
224482
- hasZoom: false,
224483
- supportsPresets: false,
224484
- hasAutofocus: false
224485
- };
224486
- const hasAutofocus = this.config.get("deviceCache")?.autoFocusSnapshot?.supported === true;
224487
- return this.resolveCapOptions({
224488
- capName: "ptz",
224489
- schema: PtzOptionsSchema,
224490
- probe: async () => {
224491
- const { capabilities } = await (await this.ensureApi()).getDeviceCapabilities(this.getChannel());
224492
- return {
224493
- hasPan: capabilities.hasPan,
224494
- hasTilt: capabilities.hasTilt,
224495
- hasZoom: capabilities.hasZoom,
224496
- supportsPresets: capabilities.hasPresets,
224497
- hasAutofocus
224498
- };
224499
- },
224500
- fallback: () => {
224501
- const hasPtz = this.getProbeFlags().hasPtz === true;
224502
- return {
224503
- hasPan: hasPtz,
224504
- hasTilt: hasPtz,
224505
- hasZoom: hasPtz,
224506
- supportsPresets: hasPtz,
224507
- hasAutofocus
224508
- };
224571
+ try {
224572
+ await api.ptz(channel, {
224573
+ action: "stop",
224574
+ command: "Up"
224575
+ });
224576
+ } catch {}
224577
+ },
224578
+ getPresets: async ({ deviceId }) => {
224579
+ if (deviceId !== this.id) return [];
224580
+ try {
224581
+ const api = await this.ensureApi();
224582
+ const channel = this.getChannel();
224583
+ return (await api.getPtzPresets(channel)).map((p) => ({
224584
+ id: String(p.id),
224585
+ name: p.name
224586
+ }));
224587
+ } catch {
224588
+ return [];
224509
224589
  }
224510
- });
224511
- },
224512
- goHome: async ({ deviceId }) => {
224513
- if (deviceId !== this.id) return;
224514
- try {
224590
+ },
224591
+ goToPreset: async ({ deviceId, presetId }) => {
224592
+ if (deviceId !== this.id) return;
224515
224593
  const api = await this.ensureApi();
224516
224594
  const channel = this.getChannel();
224517
- await api.moveToPtzPreset(channel, 0);
224518
- } catch {}
224519
- },
224520
- getPosition: async ({ deviceId }) => {
224521
- if (deviceId !== this.id) return {
224522
- pan: 0,
224523
- tilt: 0,
224524
- zoom: 0
224525
- };
224526
- return {
224527
- pan: 0,
224528
- tilt: 0,
224529
- zoom: 0
224530
- };
224531
- },
224532
- getStatus: async ({ deviceId }) => {
224533
- if (deviceId !== this.id) return {
224534
- pan: 0,
224535
- tilt: 0,
224536
- zoom: 0,
224537
- autofocus: false
224538
- };
224539
- return {
224540
- pan: 0,
224541
- tilt: 0,
224542
- zoom: 0,
224543
- autofocus: (this.config.get("deviceCache")?.autoFocusSnapshot)?.enabled === true
224544
- };
224545
- },
224546
- setAutofocus: async ({ deviceId, enabled }) => {
224547
- if (deviceId !== this.id) return;
224548
- const api = await this.ensureApi();
224549
- const channel = this.getChannel();
224550
- await api.setAutoFocus(channel, enabled ? 0 : 1);
224551
- try {
224552
- const a = (await api.getAutoFocus(channel, { timeoutMs: 1500 }))?.body?.AutoFocus;
224553
- if (a) await this.config.setAll({ deviceCache: {
224554
- ...this.config.get("deviceCache"),
224555
- autoFocusSnapshot: {
224556
- enabled: typeof a.disable === "number" ? a.disable === 0 : null,
224557
- supported: true
224595
+ const id = Number(presetId);
224596
+ if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224597
+ await api.moveToPtzPreset(channel, id);
224598
+ },
224599
+ savePreset: async ({ deviceId, presetId, name }) => {
224600
+ if (deviceId !== this.id) return;
224601
+ const api = await this.ensureApi();
224602
+ const channel = this.getChannel();
224603
+ const id = Number(presetId);
224604
+ if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224605
+ await api.setPtzPreset(channel, id, name);
224606
+ },
224607
+ deletePreset: async ({ deviceId, presetId }) => {
224608
+ if (deviceId !== this.id) return;
224609
+ const api = await this.ensureApi();
224610
+ const channel = this.getChannel();
224611
+ const id = Number(presetId);
224612
+ if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224613
+ await api.deletePtzPreset(channel, id);
224614
+ },
224615
+ getOptions: async ({ deviceId }) => {
224616
+ if (deviceId !== this.id) return {
224617
+ hasPan: false,
224618
+ hasTilt: false,
224619
+ hasZoom: false,
224620
+ supportsPresets: false,
224621
+ hasAutofocus: false
224622
+ };
224623
+ const hasAutofocus = this.config.get("deviceCache")?.autoFocusSnapshot?.supported === true;
224624
+ return this.resolveCapOptions({
224625
+ capName: "ptz",
224626
+ schema: PtzOptionsSchema,
224627
+ probe: async () => {
224628
+ const { capabilities } = await (await this.ensureApi()).getDeviceCapabilities(this.getChannel());
224629
+ return {
224630
+ hasPan: capabilities.hasPan,
224631
+ hasTilt: capabilities.hasTilt,
224632
+ hasZoom: capabilities.hasZoom,
224633
+ supportsPresets: capabilities.hasPresets,
224634
+ hasAutofocus
224635
+ };
224636
+ },
224637
+ fallback: () => {
224638
+ const hasPtz = this.getProbeFlags().hasPtz === true;
224639
+ return {
224640
+ hasPan: hasPtz,
224641
+ hasTilt: hasPtz,
224642
+ hasZoom: hasPtz,
224643
+ supportsPresets: hasPtz,
224644
+ hasAutofocus
224645
+ };
224558
224646
  }
224559
- } });
224560
- } catch {}
224561
- }
224562
- });
224647
+ });
224648
+ },
224649
+ goHome: async ({ deviceId }) => {
224650
+ if (deviceId !== this.id) return;
224651
+ try {
224652
+ const api = await this.ensureApi();
224653
+ const channel = this.getChannel();
224654
+ await api.moveToPtzPreset(channel, 0);
224655
+ } catch {}
224656
+ },
224657
+ getPosition: async ({ deviceId }) => {
224658
+ if (deviceId !== this.id) return {
224659
+ pan: 0,
224660
+ tilt: 0,
224661
+ zoom: 0
224662
+ };
224663
+ return {
224664
+ pan: 0,
224665
+ tilt: 0,
224666
+ zoom: 0
224667
+ };
224668
+ },
224669
+ getStatus: async ({ deviceId }) => {
224670
+ if (deviceId !== this.id) return {
224671
+ pan: 0,
224672
+ tilt: 0,
224673
+ zoom: 0,
224674
+ autofocus: false
224675
+ };
224676
+ return {
224677
+ pan: 0,
224678
+ tilt: 0,
224679
+ zoom: 0,
224680
+ autofocus: (this.config.get("deviceCache")?.autoFocusSnapshot)?.enabled === true
224681
+ };
224682
+ },
224683
+ setAutofocus: async ({ deviceId, enabled }) => {
224684
+ if (deviceId !== this.id) return;
224685
+ const api = await this.ensureApi();
224686
+ const channel = this.getChannel();
224687
+ await api.setAutoFocus(channel, enabled ? 0 : 1);
224688
+ try {
224689
+ const a = (await api.getAutoFocus(channel, { timeoutMs: 1500 }))?.body?.AutoFocus;
224690
+ if (a) await this.config.setAll({ deviceCache: {
224691
+ ...this.config.get("deviceCache"),
224692
+ autoFocusSnapshot: {
224693
+ enabled: typeof a.disable === "number" ? a.disable === 0 : null,
224694
+ supported: true
224695
+ }
224696
+ } });
224697
+ } catch {}
224698
+ }
224699
+ };
224700
+ this.registerCapWarmer("ptz", async () => {
224701
+ await ptzProvider.getOptions({ deviceId: this.id });
224702
+ });
224703
+ this.ctx.registerNativeCap(ptzCapability, ptzProvider);
224704
+ }
224563
224705
  }
224564
224706
  /**
224565
224707
  * Translate normalized (-1..1) pan/tilt/zoom to one or more discrete
@@ -225262,6 +225404,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
225262
225404
  if (this.batteryUpdateInFlight) return this.batteryUpdateInFlight;
225263
225405
  const cycle = (async () => {
225264
225406
  try {
225407
+ if (!this.allowProactiveCameraAccess("battery-update")) {
225408
+ this.ctx.logger.debug("battery-update: cam sleeping — skipping cycle without connecting", { tags: { deviceId: this.id } });
225409
+ return;
225410
+ }
225265
225411
  let api;
225266
225412
  try {
225267
225413
  api = await this.ensureApi();
@@ -225360,6 +225506,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
225360
225506
  meta: { error: err instanceof Error ? err.message : String(err) }
225361
225507
  });
225362
225508
  }
225509
+ await this.warmCapSlicesOnWake();
225510
+ await this.warmSnapshotOnWake();
225363
225511
  await this.alignAuxDevicesState("wake");
225364
225512
  await this.refreshParentSettingsSnapshot();
225365
225513
  }
@@ -227113,6 +227261,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
227113
227261
  }
227114
227262
  if (this.api) return this.api;
227115
227263
  if (this.loginPromise) return this.loginPromise;
227264
+ if (this.isBattery && this.sleeping) {
227265
+ const frames = (/* @__PURE__ */ new Error("wake-attribution")).stack?.split("\n").slice(2, 7).join(" | ");
227266
+ this.ctx.logger.info("battery cam login while sleeping — this wakes the camera", {
227267
+ tags: { deviceId: this.id },
227268
+ meta: { caller: frames ?? "unavailable" }
227269
+ });
227270
+ }
227116
227271
  const host = this.config.get("host");
227117
227272
  const port = this.config.get("port");
227118
227273
  const username = this.config.get("username");
package/dist/addon.mjs CHANGED
@@ -222600,6 +222600,33 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
222600
222600
  markWakeIssued() {
222601
222601
  this.lastProactiveWakeAt = Date.now();
222602
222602
  }
222603
+ /**
222604
+ * Gate for a PROACTIVE camera read, checked BEFORE `ensureApi()`.
222605
+ *
222606
+ * The login itself is the wake. On a sleeping UDP/battery camera the
222607
+ * lib's discovery + handshake nudges the firmware awake (same reason
222608
+ * `refreshParentSettingsSnapshot` refuses to call `ensureApi` while
222609
+ * asleep), so gating only the explicit `wakeUp()` call is useless —
222610
+ * by the time we reach it the camera is already up. Production logs
222611
+ * showed exactly that: a background read produced a full
222612
+ * `Connecting to Reolink` → BCUDP discovery → `battery sleep state
222613
+ * committed` (awake) → whole refresh cascade, on a camera that had
222614
+ * been asleep for six minutes.
222615
+ *
222616
+ * Returns `false` when the caller must serve cache / bail out without
222617
+ * touching the socket. Stamps the cooldown when it does let a wake
222618
+ * through, so "at most one proactive wake per
222619
+ * `PROACTIVE_WAKE_COOLDOWN_MS`" holds across ALL proactive callers
222620
+ * rather than per-caller.
222621
+ *
222622
+ * Demand-driven paths never call this — see `canProactivelyWake`.
222623
+ */
222624
+ allowProactiveCameraAccess(reason) {
222625
+ if (!this.isBattery || !this.sleeping) return true;
222626
+ if (!this.canProactivelyWake(reason)) return false;
222627
+ this.markWakeIssued();
222628
+ return true;
222629
+ }
222603
222630
  updateBatteryCache(info) {
222604
222631
  this.setCapSlice(batteryCapability, this.mapBatteryInfo(info));
222605
222632
  }
@@ -222845,6 +222872,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
222845
222872
  }
222846
222873
  async fetchSnapshotWithSingleFlight() {
222847
222874
  if (this.snapshotInFlight) return this.snapshotInFlight;
222875
+ if (!this.allowProactiveCameraAccess("snapshot")) {
222876
+ this.ctx.logger.debug("snapshot: skipped — sleeping battery cam inside wake cooldown", { tags: { deviceId: this.id } });
222877
+ return null;
222878
+ }
222848
222879
  const promise = (async () => {
222849
222880
  let api;
222850
222881
  try {
@@ -222862,23 +222893,16 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
222862
222893
  } catch {
222863
222894
  return true;
222864
222895
  }
222865
- })()) {
222866
- if (!this.canProactivelyWake("snapshot")) {
222867
- this.ctx.logger.debug("snapshot: skipped — sleeping battery cam inside wake cooldown", { tags: { deviceId: this.id } });
222868
- return null;
222869
- }
222870
- this.markWakeIssued();
222871
- try {
222872
- await api.wakeUp(this.getChannel(), {
222873
- waitAfterWakeMs: 1500,
222874
- attempts: 2
222875
- });
222876
- } catch (err) {
222877
- this.ctx.logger.debug("snapshot: pre-wake failed (will still try getSnapshot)", {
222878
- tags: { deviceId: this.id },
222879
- meta: { error: err instanceof Error ? err.message : String(err) }
222880
- });
222881
- }
222896
+ })()) try {
222897
+ await api.wakeUp(this.getChannel(), {
222898
+ waitAfterWakeMs: 1500,
222899
+ attempts: 2
222900
+ });
222901
+ } catch (err) {
222902
+ this.ctx.logger.debug("snapshot: pre-wake failed (will still try getSnapshot)", {
222903
+ tags: { deviceId: this.id },
222904
+ meta: { error: err instanceof Error ? err.message : String(err) }
222905
+ });
222882
222906
  }
222883
222907
  const tryOnce = async (timeoutMs) => {
222884
222908
  const buf = await api.getSnapshot(this.getChannel(), {
@@ -223421,6 +223445,97 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223421
223445
  }
223422
223446
  }
223423
223447
  /**
223448
+ * Per-cap "warm the cache while the camera is up" closures, registered
223449
+ * by each device-config cap alongside its provider and run by
223450
+ * `onWakeTransition`.
223451
+ *
223452
+ * This is the OTHER half of the read discipline. Gating reads stops us
223453
+ * waking the camera, but on its own it leaves the caches empty — and an
223454
+ * empty cache is what the operator actually sees: the State panel reads
223455
+ * the runtime-state slices (`deviceState.getAllSnapshots`), so a
223456
+ * sleeping camera whose slices were never written shows null for
223457
+ * everything. Before the gate, the admin UI's 2.5s aggregate poll kept
223458
+ * those slices warm by waking the camera every time — the very bug we
223459
+ * fixed. So the refresh has to move to the moment the camera is
223460
+ * ALREADY up, which is exactly the wake transition.
223461
+ */
223462
+ capWarmers = /* @__PURE__ */ new Map();
223463
+ /** How fresh a slice must be for the wake warm-up to skip it. A battery
223464
+ * cam can wake many times an hour on motion; re-probing every cap on
223465
+ * every wake would spend the (~14s) window on data we already have. */
223466
+ static WAKE_WARM_MAX_AGE_MS = 10 * 6e4;
223467
+ registerCapWarmer(capName, warm) {
223468
+ this.capWarmers.set(capName, warm);
223469
+ }
223470
+ /**
223471
+ * Refresh the device-config cap caches during a wake window.
223472
+ *
223473
+ * SERIALIZED, like every other wake-path refresh — concurrent Baichuan
223474
+ * traffic on the single BCUDP stream starves the heavy reads. Each cap
223475
+ * is skipped when its slice is still fresh, and each failure is
223476
+ * swallowed: the window is short, partial progress accumulates across
223477
+ * wakes because the slices persist.
223478
+ */
223479
+ async warmCapSlicesOnWake() {
223480
+ const now = Date.now();
223481
+ let warmed = 0;
223482
+ let skipped = 0;
223483
+ for (const [capName, warm] of this.capWarmers) {
223484
+ const slice = this.runtimeState.getCapState(capName);
223485
+ const fetchedAt = typeof slice?.lastFetchedAt === "number" ? slice.lastFetchedAt : 0;
223486
+ if (fetchedAt > 0 && now - fetchedAt < ReolinkCamera.WAKE_WARM_MAX_AGE_MS) {
223487
+ skipped += 1;
223488
+ continue;
223489
+ }
223490
+ try {
223491
+ await warm();
223492
+ warmed += 1;
223493
+ } catch (err) {
223494
+ this.ctx.logger.debug("wake warm-up: cap refresh failed — retrying on next wake", {
223495
+ tags: { deviceId: this.id },
223496
+ meta: {
223497
+ capName,
223498
+ error: err instanceof Error ? err.message : String(err)
223499
+ }
223500
+ });
223501
+ }
223502
+ }
223503
+ this.ctx.logger.info("wake warm-up: cap caches refreshed", {
223504
+ tags: { deviceId: this.id },
223505
+ meta: {
223506
+ warmed,
223507
+ skipped,
223508
+ total: this.capWarmers.size
223509
+ }
223510
+ });
223511
+ }
223512
+ /**
223513
+ * Ask the snapshot wrapper for a frame while the camera is up, so its
223514
+ * cache holds something recent to serve for the whole sleep window.
223515
+ *
223516
+ * The wrapper already does the right thing on a miss — `resolveOutcome`
223517
+ * falls back to the stale frame rather than blanking the UI — but only
223518
+ * if a frame was ever captured. With reads gated, nothing captures one
223519
+ * any more: the tile stayed empty for as long as the camera slept.
223520
+ * `force: true` bypasses the wrapper's freshness gate; our own
223521
+ * proactive gate lets it through because `sleeping` is false here.
223522
+ * Mirrors the reference's `updateBatteryAndSnapshot`.
223523
+ */
223524
+ async warmSnapshotOnWake() {
223525
+ try {
223526
+ await this.ctx.api.snapshot.getSnapshot.query({
223527
+ deviceId: this.id,
223528
+ force: true
223529
+ });
223530
+ this.ctx.logger.debug("wake warm-up: snapshot cache refreshed", { tags: { deviceId: this.id } });
223531
+ } catch (err) {
223532
+ this.ctx.logger.debug("wake warm-up: snapshot refresh failed", {
223533
+ tags: { deviceId: this.id },
223534
+ meta: { error: err instanceof Error ? err.message : String(err) }
223535
+ });
223536
+ }
223537
+ }
223538
+ /**
223424
223539
  * Wrap a cap's `refreshFromCamera` so the READ side (the bridge's
223425
223540
  * stale-check behind `getStatus`) never wakes a sleeping battery cam.
223426
223541
  * The bridge then projects whatever the slice last held — which is
@@ -223525,6 +223640,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223525
223640
  this.ctx.eventBus.emit(createEvent(EventCategory.StreamParamsChanged, this.eventSource(), { deviceId: this.id }));
223526
223641
  }
223527
223642
  };
223643
+ this.registerCapWarmer(CAP_NAME, async () => {
223644
+ await provider.getOptions({ deviceId: this.id });
223645
+ await refreshFromCamera();
223646
+ });
223528
223647
  this.ctx.registerNativeCap(streamParamsCapability, provider);
223529
223648
  this.ctx.logger.info("Reolink stream-params cap registered", { tags: { deviceId: this.id } });
223530
223649
  }
@@ -223715,6 +223834,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223715
223834
  await refreshFromCamera();
223716
223835
  }
223717
223836
  };
223837
+ this.registerCapWarmer(CAP_NAME, async () => {
223838
+ await provider.getOptions({ deviceId: this.id });
223839
+ await refreshFromCamera();
223840
+ });
223718
223841
  this.ctx.registerNativeCap(motionZonesCapability, provider);
223719
223842
  this.ctx.logger.info("Reolink motion-zones cap registered", { tags: { deviceId: this.id } });
223720
223843
  }
@@ -223850,6 +223973,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223850
223973
  });
223851
223974
  }
223852
223975
  };
223976
+ this.registerCapWarmer(CAP_NAME, async () => {
223977
+ await provider.getOptions({ deviceId: this.id });
223978
+ await refreshFromCamera();
223979
+ });
223853
223980
  this.ctx.registerNativeCap(privacyMaskCapability, provider);
223854
223981
  this.ctx.logger.info("Reolink privacy-mask cap registered (read + enable + zone write)", { tags: { deviceId: this.id } });
223855
223982
  }
@@ -223985,6 +224112,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223985
224112
  await refreshFromCamera();
223986
224113
  }
223987
224114
  };
224115
+ this.registerCapWarmer(CAP_NAME, async () => {
224116
+ await provider.getOptions({ deviceId: this.id });
224117
+ await refreshFromCamera();
224118
+ });
223988
224119
  this.ctx.registerNativeCap(dayNightCapability, provider);
223989
224120
  this.ctx.logger.info("Reolink day-night cap registered", { tags: { deviceId: this.id } });
223990
224121
  }
@@ -224122,6 +224253,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
224122
224253
  await refreshFromCamera();
224123
224254
  }
224124
224255
  };
224256
+ this.registerCapWarmer(CAP_NAME, async () => {
224257
+ await provider.getOptions({ deviceId: this.id });
224258
+ await refreshFromCamera();
224259
+ });
224125
224260
  this.ctx.registerNativeCap(imageSettingsCapability, provider);
224126
224261
  this.ctx.logger.info("Reolink image-settings cap registered", { tags: { deviceId: this.id } });
224127
224262
  }
@@ -224391,6 +224526,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
224391
224526
  await refreshFromCamera();
224392
224527
  }
224393
224528
  };
224529
+ this.registerCapWarmer(CAP_NAME, refreshFromCamera);
224394
224530
  this.ctx.registerNativeCap(ptzAutotrackCapability, provider);
224395
224531
  this.ctx.logger.info("Reolink ptz-autotrack cap registered", { tags: { deviceId: this.id } });
224396
224532
  }
@@ -224398,148 +224534,154 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
224398
224534
  if (this.ptzRegistered) return;
224399
224535
  if (!this.getProbeFlags().hasPtz) return;
224400
224536
  this.ptzRegistered = true;
224401
- this.ctx.registerNativeCap(ptzCapability, {
224402
- move: async ({ deviceId, pan, tilt, zoom, speed }) => {
224403
- if (deviceId !== this.id) return;
224404
- await this.runPtz(pan, tilt, zoom, speed, false);
224405
- },
224406
- continuousMove: async ({ deviceId, pan, tilt, zoom, speed }) => {
224407
- if (deviceId !== this.id) return;
224408
- await this.runPtz(pan, tilt, zoom, speed, true);
224409
- },
224410
- stop: async ({ deviceId }) => {
224411
- if (deviceId !== this.id) return;
224412
- const api = await this.ensureApi();
224413
- const channel = this.getChannel();
224414
- try {
224415
- await api.ptz(channel, {
224416
- action: "stop",
224417
- command: "Up"
224418
- });
224419
- } catch {}
224420
- },
224421
- getPresets: async ({ deviceId }) => {
224422
- if (deviceId !== this.id) return [];
224423
- try {
224537
+ {
224538
+ const ptzProvider = {
224539
+ move: async ({ deviceId, pan, tilt, zoom, speed }) => {
224540
+ if (deviceId !== this.id) return;
224541
+ await this.runPtz(pan, tilt, zoom, speed, false);
224542
+ },
224543
+ continuousMove: async ({ deviceId, pan, tilt, zoom, speed }) => {
224544
+ if (deviceId !== this.id) return;
224545
+ await this.runPtz(pan, tilt, zoom, speed, true);
224546
+ },
224547
+ stop: async ({ deviceId }) => {
224548
+ if (deviceId !== this.id) return;
224424
224549
  const api = await this.ensureApi();
224425
224550
  const channel = this.getChannel();
224426
- return (await api.getPtzPresets(channel)).map((p) => ({
224427
- id: String(p.id),
224428
- name: p.name
224429
- }));
224430
- } catch {
224431
- return [];
224432
- }
224433
- },
224434
- goToPreset: async ({ deviceId, presetId }) => {
224435
- if (deviceId !== this.id) return;
224436
- const api = await this.ensureApi();
224437
- const channel = this.getChannel();
224438
- const id = Number(presetId);
224439
- if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224440
- await api.moveToPtzPreset(channel, id);
224441
- },
224442
- savePreset: async ({ deviceId, presetId, name }) => {
224443
- if (deviceId !== this.id) return;
224444
- const api = await this.ensureApi();
224445
- const channel = this.getChannel();
224446
- const id = Number(presetId);
224447
- if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224448
- await api.setPtzPreset(channel, id, name);
224449
- },
224450
- deletePreset: async ({ deviceId, presetId }) => {
224451
- if (deviceId !== this.id) return;
224452
- const api = await this.ensureApi();
224453
- const channel = this.getChannel();
224454
- const id = Number(presetId);
224455
- if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224456
- await api.deletePtzPreset(channel, id);
224457
- },
224458
- getOptions: async ({ deviceId }) => {
224459
- if (deviceId !== this.id) return {
224460
- hasPan: false,
224461
- hasTilt: false,
224462
- hasZoom: false,
224463
- supportsPresets: false,
224464
- hasAutofocus: false
224465
- };
224466
- const hasAutofocus = this.config.get("deviceCache")?.autoFocusSnapshot?.supported === true;
224467
- return this.resolveCapOptions({
224468
- capName: "ptz",
224469
- schema: PtzOptionsSchema,
224470
- probe: async () => {
224471
- const { capabilities } = await (await this.ensureApi()).getDeviceCapabilities(this.getChannel());
224472
- return {
224473
- hasPan: capabilities.hasPan,
224474
- hasTilt: capabilities.hasTilt,
224475
- hasZoom: capabilities.hasZoom,
224476
- supportsPresets: capabilities.hasPresets,
224477
- hasAutofocus
224478
- };
224479
- },
224480
- fallback: () => {
224481
- const hasPtz = this.getProbeFlags().hasPtz === true;
224482
- return {
224483
- hasPan: hasPtz,
224484
- hasTilt: hasPtz,
224485
- hasZoom: hasPtz,
224486
- supportsPresets: hasPtz,
224487
- hasAutofocus
224488
- };
224551
+ try {
224552
+ await api.ptz(channel, {
224553
+ action: "stop",
224554
+ command: "Up"
224555
+ });
224556
+ } catch {}
224557
+ },
224558
+ getPresets: async ({ deviceId }) => {
224559
+ if (deviceId !== this.id) return [];
224560
+ try {
224561
+ const api = await this.ensureApi();
224562
+ const channel = this.getChannel();
224563
+ return (await api.getPtzPresets(channel)).map((p) => ({
224564
+ id: String(p.id),
224565
+ name: p.name
224566
+ }));
224567
+ } catch {
224568
+ return [];
224489
224569
  }
224490
- });
224491
- },
224492
- goHome: async ({ deviceId }) => {
224493
- if (deviceId !== this.id) return;
224494
- try {
224570
+ },
224571
+ goToPreset: async ({ deviceId, presetId }) => {
224572
+ if (deviceId !== this.id) return;
224495
224573
  const api = await this.ensureApi();
224496
224574
  const channel = this.getChannel();
224497
- await api.moveToPtzPreset(channel, 0);
224498
- } catch {}
224499
- },
224500
- getPosition: async ({ deviceId }) => {
224501
- if (deviceId !== this.id) return {
224502
- pan: 0,
224503
- tilt: 0,
224504
- zoom: 0
224505
- };
224506
- return {
224507
- pan: 0,
224508
- tilt: 0,
224509
- zoom: 0
224510
- };
224511
- },
224512
- getStatus: async ({ deviceId }) => {
224513
- if (deviceId !== this.id) return {
224514
- pan: 0,
224515
- tilt: 0,
224516
- zoom: 0,
224517
- autofocus: false
224518
- };
224519
- return {
224520
- pan: 0,
224521
- tilt: 0,
224522
- zoom: 0,
224523
- autofocus: (this.config.get("deviceCache")?.autoFocusSnapshot)?.enabled === true
224524
- };
224525
- },
224526
- setAutofocus: async ({ deviceId, enabled }) => {
224527
- if (deviceId !== this.id) return;
224528
- const api = await this.ensureApi();
224529
- const channel = this.getChannel();
224530
- await api.setAutoFocus(channel, enabled ? 0 : 1);
224531
- try {
224532
- const a = (await api.getAutoFocus(channel, { timeoutMs: 1500 }))?.body?.AutoFocus;
224533
- if (a) await this.config.setAll({ deviceCache: {
224534
- ...this.config.get("deviceCache"),
224535
- autoFocusSnapshot: {
224536
- enabled: typeof a.disable === "number" ? a.disable === 0 : null,
224537
- supported: true
224575
+ const id = Number(presetId);
224576
+ if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224577
+ await api.moveToPtzPreset(channel, id);
224578
+ },
224579
+ savePreset: async ({ deviceId, presetId, name }) => {
224580
+ if (deviceId !== this.id) return;
224581
+ const api = await this.ensureApi();
224582
+ const channel = this.getChannel();
224583
+ const id = Number(presetId);
224584
+ if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224585
+ await api.setPtzPreset(channel, id, name);
224586
+ },
224587
+ deletePreset: async ({ deviceId, presetId }) => {
224588
+ if (deviceId !== this.id) return;
224589
+ const api = await this.ensureApi();
224590
+ const channel = this.getChannel();
224591
+ const id = Number(presetId);
224592
+ if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224593
+ await api.deletePtzPreset(channel, id);
224594
+ },
224595
+ getOptions: async ({ deviceId }) => {
224596
+ if (deviceId !== this.id) return {
224597
+ hasPan: false,
224598
+ hasTilt: false,
224599
+ hasZoom: false,
224600
+ supportsPresets: false,
224601
+ hasAutofocus: false
224602
+ };
224603
+ const hasAutofocus = this.config.get("deviceCache")?.autoFocusSnapshot?.supported === true;
224604
+ return this.resolveCapOptions({
224605
+ capName: "ptz",
224606
+ schema: PtzOptionsSchema,
224607
+ probe: async () => {
224608
+ const { capabilities } = await (await this.ensureApi()).getDeviceCapabilities(this.getChannel());
224609
+ return {
224610
+ hasPan: capabilities.hasPan,
224611
+ hasTilt: capabilities.hasTilt,
224612
+ hasZoom: capabilities.hasZoom,
224613
+ supportsPresets: capabilities.hasPresets,
224614
+ hasAutofocus
224615
+ };
224616
+ },
224617
+ fallback: () => {
224618
+ const hasPtz = this.getProbeFlags().hasPtz === true;
224619
+ return {
224620
+ hasPan: hasPtz,
224621
+ hasTilt: hasPtz,
224622
+ hasZoom: hasPtz,
224623
+ supportsPresets: hasPtz,
224624
+ hasAutofocus
224625
+ };
224538
224626
  }
224539
- } });
224540
- } catch {}
224541
- }
224542
- });
224627
+ });
224628
+ },
224629
+ goHome: async ({ deviceId }) => {
224630
+ if (deviceId !== this.id) return;
224631
+ try {
224632
+ const api = await this.ensureApi();
224633
+ const channel = this.getChannel();
224634
+ await api.moveToPtzPreset(channel, 0);
224635
+ } catch {}
224636
+ },
224637
+ getPosition: async ({ deviceId }) => {
224638
+ if (deviceId !== this.id) return {
224639
+ pan: 0,
224640
+ tilt: 0,
224641
+ zoom: 0
224642
+ };
224643
+ return {
224644
+ pan: 0,
224645
+ tilt: 0,
224646
+ zoom: 0
224647
+ };
224648
+ },
224649
+ getStatus: async ({ deviceId }) => {
224650
+ if (deviceId !== this.id) return {
224651
+ pan: 0,
224652
+ tilt: 0,
224653
+ zoom: 0,
224654
+ autofocus: false
224655
+ };
224656
+ return {
224657
+ pan: 0,
224658
+ tilt: 0,
224659
+ zoom: 0,
224660
+ autofocus: (this.config.get("deviceCache")?.autoFocusSnapshot)?.enabled === true
224661
+ };
224662
+ },
224663
+ setAutofocus: async ({ deviceId, enabled }) => {
224664
+ if (deviceId !== this.id) return;
224665
+ const api = await this.ensureApi();
224666
+ const channel = this.getChannel();
224667
+ await api.setAutoFocus(channel, enabled ? 0 : 1);
224668
+ try {
224669
+ const a = (await api.getAutoFocus(channel, { timeoutMs: 1500 }))?.body?.AutoFocus;
224670
+ if (a) await this.config.setAll({ deviceCache: {
224671
+ ...this.config.get("deviceCache"),
224672
+ autoFocusSnapshot: {
224673
+ enabled: typeof a.disable === "number" ? a.disable === 0 : null,
224674
+ supported: true
224675
+ }
224676
+ } });
224677
+ } catch {}
224678
+ }
224679
+ };
224680
+ this.registerCapWarmer("ptz", async () => {
224681
+ await ptzProvider.getOptions({ deviceId: this.id });
224682
+ });
224683
+ this.ctx.registerNativeCap(ptzCapability, ptzProvider);
224684
+ }
224543
224685
  }
224544
224686
  /**
224545
224687
  * Translate normalized (-1..1) pan/tilt/zoom to one or more discrete
@@ -225242,6 +225384,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
225242
225384
  if (this.batteryUpdateInFlight) return this.batteryUpdateInFlight;
225243
225385
  const cycle = (async () => {
225244
225386
  try {
225387
+ if (!this.allowProactiveCameraAccess("battery-update")) {
225388
+ this.ctx.logger.debug("battery-update: cam sleeping — skipping cycle without connecting", { tags: { deviceId: this.id } });
225389
+ return;
225390
+ }
225245
225391
  let api;
225246
225392
  try {
225247
225393
  api = await this.ensureApi();
@@ -225340,6 +225486,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
225340
225486
  meta: { error: err instanceof Error ? err.message : String(err) }
225341
225487
  });
225342
225488
  }
225489
+ await this.warmCapSlicesOnWake();
225490
+ await this.warmSnapshotOnWake();
225343
225491
  await this.alignAuxDevicesState("wake");
225344
225492
  await this.refreshParentSettingsSnapshot();
225345
225493
  }
@@ -227093,6 +227241,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
227093
227241
  }
227094
227242
  if (this.api) return this.api;
227095
227243
  if (this.loginPromise) return this.loginPromise;
227244
+ if (this.isBattery && this.sleeping) {
227245
+ const frames = (/* @__PURE__ */ new Error("wake-attribution")).stack?.split("\n").slice(2, 7).join(" | ");
227246
+ this.ctx.logger.info("battery cam login while sleeping — this wakes the camera", {
227247
+ tags: { deviceId: this.id },
227248
+ meta: { caller: frames ?? "unavailable" }
227249
+ });
227250
+ }
227096
227251
  const host = this.config.get("host");
227097
227252
  const port = this.config.get("port");
227098
227253
  const username = this.config.get("username");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",