@camstack/addon-provider-reolink 1.2.6 → 1.2.8

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 +257 -137
  2. package/dist/addon.mjs +257 -137
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -223465,6 +223465,97 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223465
223465
  }
223466
223466
  }
223467
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
+ /**
223468
223559
  * Wrap a cap's `refreshFromCamera` so the READ side (the bridge's
223469
223560
  * stale-check behind `getStatus`) never wakes a sleeping battery cam.
223470
223561
  * The bridge then projects whatever the slice last held — which is
@@ -223569,6 +223660,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223569
223660
  this.ctx.eventBus.emit(createEvent(EventCategory.StreamParamsChanged, this.eventSource(), { deviceId: this.id }));
223570
223661
  }
223571
223662
  };
223663
+ this.registerCapWarmer(CAP_NAME, async () => {
223664
+ await provider.getOptions({ deviceId: this.id });
223665
+ await refreshFromCamera();
223666
+ });
223572
223667
  this.ctx.registerNativeCap(streamParamsCapability, provider);
223573
223668
  this.ctx.logger.info("Reolink stream-params cap registered", { tags: { deviceId: this.id } });
223574
223669
  }
@@ -223759,6 +223854,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223759
223854
  await refreshFromCamera();
223760
223855
  }
223761
223856
  };
223857
+ this.registerCapWarmer(CAP_NAME, async () => {
223858
+ await provider.getOptions({ deviceId: this.id });
223859
+ await refreshFromCamera();
223860
+ });
223762
223861
  this.ctx.registerNativeCap(motionZonesCapability, provider);
223763
223862
  this.ctx.logger.info("Reolink motion-zones cap registered", { tags: { deviceId: this.id } });
223764
223863
  }
@@ -223894,6 +223993,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223894
223993
  });
223895
223994
  }
223896
223995
  };
223996
+ this.registerCapWarmer(CAP_NAME, async () => {
223997
+ await provider.getOptions({ deviceId: this.id });
223998
+ await refreshFromCamera();
223999
+ });
223897
224000
  this.ctx.registerNativeCap(privacyMaskCapability, provider);
223898
224001
  this.ctx.logger.info("Reolink privacy-mask cap registered (read + enable + zone write)", { tags: { deviceId: this.id } });
223899
224002
  }
@@ -224029,6 +224132,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
224029
224132
  await refreshFromCamera();
224030
224133
  }
224031
224134
  };
224135
+ this.registerCapWarmer(CAP_NAME, async () => {
224136
+ await provider.getOptions({ deviceId: this.id });
224137
+ await refreshFromCamera();
224138
+ });
224032
224139
  this.ctx.registerNativeCap(dayNightCapability, provider);
224033
224140
  this.ctx.logger.info("Reolink day-night cap registered", { tags: { deviceId: this.id } });
224034
224141
  }
@@ -224166,6 +224273,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
224166
224273
  await refreshFromCamera();
224167
224274
  }
224168
224275
  };
224276
+ this.registerCapWarmer(CAP_NAME, async () => {
224277
+ await provider.getOptions({ deviceId: this.id });
224278
+ await refreshFromCamera();
224279
+ });
224169
224280
  this.ctx.registerNativeCap(imageSettingsCapability, provider);
224170
224281
  this.ctx.logger.info("Reolink image-settings cap registered", { tags: { deviceId: this.id } });
224171
224282
  }
@@ -224435,6 +224546,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
224435
224546
  await refreshFromCamera();
224436
224547
  }
224437
224548
  };
224549
+ this.registerCapWarmer(CAP_NAME, refreshFromCamera);
224438
224550
  this.ctx.registerNativeCap(ptzAutotrackCapability, provider);
224439
224551
  this.ctx.logger.info("Reolink ptz-autotrack cap registered", { tags: { deviceId: this.id } });
224440
224552
  }
@@ -224442,148 +224554,154 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
224442
224554
  if (this.ptzRegistered) return;
224443
224555
  if (!this.getProbeFlags().hasPtz) return;
224444
224556
  this.ptzRegistered = true;
224445
- this.ctx.registerNativeCap(ptzCapability, {
224446
- move: async ({ deviceId, pan, tilt, zoom, speed }) => {
224447
- if (deviceId !== this.id) return;
224448
- await this.runPtz(pan, tilt, zoom, speed, false);
224449
- },
224450
- continuousMove: async ({ deviceId, pan, tilt, zoom, speed }) => {
224451
- if (deviceId !== this.id) return;
224452
- await this.runPtz(pan, tilt, zoom, speed, true);
224453
- },
224454
- stop: async ({ deviceId }) => {
224455
- if (deviceId !== this.id) return;
224456
- const api = await this.ensureApi();
224457
- const channel = this.getChannel();
224458
- try {
224459
- await api.ptz(channel, {
224460
- action: "stop",
224461
- command: "Up"
224462
- });
224463
- } catch {}
224464
- },
224465
- getPresets: async ({ deviceId }) => {
224466
- if (deviceId !== this.id) return [];
224467
- 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;
224468
224569
  const api = await this.ensureApi();
224469
224570
  const channel = this.getChannel();
224470
- return (await api.getPtzPresets(channel)).map((p) => ({
224471
- id: String(p.id),
224472
- name: p.name
224473
- }));
224474
- } catch {
224475
- return [];
224476
- }
224477
- },
224478
- goToPreset: async ({ deviceId, presetId }) => {
224479
- if (deviceId !== this.id) return;
224480
- const api = await this.ensureApi();
224481
- const channel = this.getChannel();
224482
- const id = Number(presetId);
224483
- if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224484
- await api.moveToPtzPreset(channel, id);
224485
- },
224486
- savePreset: async ({ deviceId, presetId, name }) => {
224487
- if (deviceId !== this.id) return;
224488
- const api = await this.ensureApi();
224489
- const channel = this.getChannel();
224490
- const id = Number(presetId);
224491
- if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224492
- await api.setPtzPreset(channel, id, name);
224493
- },
224494
- deletePreset: async ({ deviceId, presetId }) => {
224495
- if (deviceId !== this.id) return;
224496
- const api = await this.ensureApi();
224497
- const channel = this.getChannel();
224498
- const id = Number(presetId);
224499
- if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224500
- await api.deletePtzPreset(channel, id);
224501
- },
224502
- getOptions: async ({ deviceId }) => {
224503
- if (deviceId !== this.id) return {
224504
- hasPan: false,
224505
- hasTilt: false,
224506
- hasZoom: false,
224507
- supportsPresets: false,
224508
- hasAutofocus: false
224509
- };
224510
- const hasAutofocus = this.config.get("deviceCache")?.autoFocusSnapshot?.supported === true;
224511
- return this.resolveCapOptions({
224512
- capName: "ptz",
224513
- schema: PtzOptionsSchema,
224514
- probe: async () => {
224515
- const { capabilities } = await (await this.ensureApi()).getDeviceCapabilities(this.getChannel());
224516
- return {
224517
- hasPan: capabilities.hasPan,
224518
- hasTilt: capabilities.hasTilt,
224519
- hasZoom: capabilities.hasZoom,
224520
- supportsPresets: capabilities.hasPresets,
224521
- hasAutofocus
224522
- };
224523
- },
224524
- fallback: () => {
224525
- const hasPtz = this.getProbeFlags().hasPtz === true;
224526
- return {
224527
- hasPan: hasPtz,
224528
- hasTilt: hasPtz,
224529
- hasZoom: hasPtz,
224530
- supportsPresets: hasPtz,
224531
- hasAutofocus
224532
- };
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 [];
224533
224589
  }
224534
- });
224535
- },
224536
- goHome: async ({ deviceId }) => {
224537
- if (deviceId !== this.id) return;
224538
- try {
224590
+ },
224591
+ goToPreset: async ({ deviceId, presetId }) => {
224592
+ if (deviceId !== this.id) return;
224539
224593
  const api = await this.ensureApi();
224540
224594
  const channel = this.getChannel();
224541
- await api.moveToPtzPreset(channel, 0);
224542
- } catch {}
224543
- },
224544
- getPosition: async ({ deviceId }) => {
224545
- if (deviceId !== this.id) return {
224546
- pan: 0,
224547
- tilt: 0,
224548
- zoom: 0
224549
- };
224550
- return {
224551
- pan: 0,
224552
- tilt: 0,
224553
- zoom: 0
224554
- };
224555
- },
224556
- getStatus: async ({ deviceId }) => {
224557
- if (deviceId !== this.id) return {
224558
- pan: 0,
224559
- tilt: 0,
224560
- zoom: 0,
224561
- autofocus: false
224562
- };
224563
- return {
224564
- pan: 0,
224565
- tilt: 0,
224566
- zoom: 0,
224567
- autofocus: (this.config.get("deviceCache")?.autoFocusSnapshot)?.enabled === true
224568
- };
224569
- },
224570
- setAutofocus: async ({ deviceId, enabled }) => {
224571
- if (deviceId !== this.id) return;
224572
- const api = await this.ensureApi();
224573
- const channel = this.getChannel();
224574
- await api.setAutoFocus(channel, enabled ? 0 : 1);
224575
- try {
224576
- const a = (await api.getAutoFocus(channel, { timeoutMs: 1500 }))?.body?.AutoFocus;
224577
- if (a) await this.config.setAll({ deviceCache: {
224578
- ...this.config.get("deviceCache"),
224579
- autoFocusSnapshot: {
224580
- enabled: typeof a.disable === "number" ? a.disable === 0 : null,
224581
- 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
+ };
224582
224646
  }
224583
- } });
224584
- } catch {}
224585
- }
224586
- });
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
+ }
224587
224705
  }
224588
224706
  /**
224589
224707
  * Translate normalized (-1..1) pan/tilt/zoom to one or more discrete
@@ -225286,7 +225404,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
225286
225404
  if (this.batteryUpdateInFlight) return this.batteryUpdateInFlight;
225287
225405
  const cycle = (async () => {
225288
225406
  try {
225289
- if (!this.allowProactiveCameraAccess("battery-update")) {
225407
+ if (this.isBattery && this.sleeping) {
225290
225408
  this.ctx.logger.debug("battery-update: cam sleeping — skipping cycle without connecting", { tags: { deviceId: this.id } });
225291
225409
  return;
225292
225410
  }
@@ -225388,6 +225506,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
225388
225506
  meta: { error: err instanceof Error ? err.message : String(err) }
225389
225507
  });
225390
225508
  }
225509
+ await this.warmCapSlicesOnWake();
225510
+ await this.warmSnapshotOnWake();
225391
225511
  await this.alignAuxDevicesState("wake");
225392
225512
  await this.refreshParentSettingsSnapshot();
225393
225513
  }
package/dist/addon.mjs CHANGED
@@ -223445,6 +223445,97 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223445
223445
  }
223446
223446
  }
223447
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
+ /**
223448
223539
  * Wrap a cap's `refreshFromCamera` so the READ side (the bridge's
223449
223540
  * stale-check behind `getStatus`) never wakes a sleeping battery cam.
223450
223541
  * The bridge then projects whatever the slice last held — which is
@@ -223549,6 +223640,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223549
223640
  this.ctx.eventBus.emit(createEvent(EventCategory.StreamParamsChanged, this.eventSource(), { deviceId: this.id }));
223550
223641
  }
223551
223642
  };
223643
+ this.registerCapWarmer(CAP_NAME, async () => {
223644
+ await provider.getOptions({ deviceId: this.id });
223645
+ await refreshFromCamera();
223646
+ });
223552
223647
  this.ctx.registerNativeCap(streamParamsCapability, provider);
223553
223648
  this.ctx.logger.info("Reolink stream-params cap registered", { tags: { deviceId: this.id } });
223554
223649
  }
@@ -223739,6 +223834,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223739
223834
  await refreshFromCamera();
223740
223835
  }
223741
223836
  };
223837
+ this.registerCapWarmer(CAP_NAME, async () => {
223838
+ await provider.getOptions({ deviceId: this.id });
223839
+ await refreshFromCamera();
223840
+ });
223742
223841
  this.ctx.registerNativeCap(motionZonesCapability, provider);
223743
223842
  this.ctx.logger.info("Reolink motion-zones cap registered", { tags: { deviceId: this.id } });
223744
223843
  }
@@ -223874,6 +223973,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
223874
223973
  });
223875
223974
  }
223876
223975
  };
223976
+ this.registerCapWarmer(CAP_NAME, async () => {
223977
+ await provider.getOptions({ deviceId: this.id });
223978
+ await refreshFromCamera();
223979
+ });
223877
223980
  this.ctx.registerNativeCap(privacyMaskCapability, provider);
223878
223981
  this.ctx.logger.info("Reolink privacy-mask cap registered (read + enable + zone write)", { tags: { deviceId: this.id } });
223879
223982
  }
@@ -224009,6 +224112,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
224009
224112
  await refreshFromCamera();
224010
224113
  }
224011
224114
  };
224115
+ this.registerCapWarmer(CAP_NAME, async () => {
224116
+ await provider.getOptions({ deviceId: this.id });
224117
+ await refreshFromCamera();
224118
+ });
224012
224119
  this.ctx.registerNativeCap(dayNightCapability, provider);
224013
224120
  this.ctx.logger.info("Reolink day-night cap registered", { tags: { deviceId: this.id } });
224014
224121
  }
@@ -224146,6 +224253,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
224146
224253
  await refreshFromCamera();
224147
224254
  }
224148
224255
  };
224256
+ this.registerCapWarmer(CAP_NAME, async () => {
224257
+ await provider.getOptions({ deviceId: this.id });
224258
+ await refreshFromCamera();
224259
+ });
224149
224260
  this.ctx.registerNativeCap(imageSettingsCapability, provider);
224150
224261
  this.ctx.logger.info("Reolink image-settings cap registered", { tags: { deviceId: this.id } });
224151
224262
  }
@@ -224415,6 +224526,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
224415
224526
  await refreshFromCamera();
224416
224527
  }
224417
224528
  };
224529
+ this.registerCapWarmer(CAP_NAME, refreshFromCamera);
224418
224530
  this.ctx.registerNativeCap(ptzAutotrackCapability, provider);
224419
224531
  this.ctx.logger.info("Reolink ptz-autotrack cap registered", { tags: { deviceId: this.id } });
224420
224532
  }
@@ -224422,148 +224534,154 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
224422
224534
  if (this.ptzRegistered) return;
224423
224535
  if (!this.getProbeFlags().hasPtz) return;
224424
224536
  this.ptzRegistered = true;
224425
- this.ctx.registerNativeCap(ptzCapability, {
224426
- move: async ({ deviceId, pan, tilt, zoom, speed }) => {
224427
- if (deviceId !== this.id) return;
224428
- await this.runPtz(pan, tilt, zoom, speed, false);
224429
- },
224430
- continuousMove: async ({ deviceId, pan, tilt, zoom, speed }) => {
224431
- if (deviceId !== this.id) return;
224432
- await this.runPtz(pan, tilt, zoom, speed, true);
224433
- },
224434
- stop: async ({ deviceId }) => {
224435
- if (deviceId !== this.id) return;
224436
- const api = await this.ensureApi();
224437
- const channel = this.getChannel();
224438
- try {
224439
- await api.ptz(channel, {
224440
- action: "stop",
224441
- command: "Up"
224442
- });
224443
- } catch {}
224444
- },
224445
- getPresets: async ({ deviceId }) => {
224446
- if (deviceId !== this.id) return [];
224447
- 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;
224448
224549
  const api = await this.ensureApi();
224449
224550
  const channel = this.getChannel();
224450
- return (await api.getPtzPresets(channel)).map((p) => ({
224451
- id: String(p.id),
224452
- name: p.name
224453
- }));
224454
- } catch {
224455
- return [];
224456
- }
224457
- },
224458
- goToPreset: async ({ deviceId, presetId }) => {
224459
- if (deviceId !== this.id) return;
224460
- const api = await this.ensureApi();
224461
- const channel = this.getChannel();
224462
- const id = Number(presetId);
224463
- if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224464
- await api.moveToPtzPreset(channel, id);
224465
- },
224466
- savePreset: async ({ deviceId, presetId, name }) => {
224467
- if (deviceId !== this.id) return;
224468
- const api = await this.ensureApi();
224469
- const channel = this.getChannel();
224470
- const id = Number(presetId);
224471
- if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224472
- await api.setPtzPreset(channel, id, name);
224473
- },
224474
- deletePreset: async ({ deviceId, presetId }) => {
224475
- if (deviceId !== this.id) return;
224476
- const api = await this.ensureApi();
224477
- const channel = this.getChannel();
224478
- const id = Number(presetId);
224479
- if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
224480
- await api.deletePtzPreset(channel, id);
224481
- },
224482
- getOptions: async ({ deviceId }) => {
224483
- if (deviceId !== this.id) return {
224484
- hasPan: false,
224485
- hasTilt: false,
224486
- hasZoom: false,
224487
- supportsPresets: false,
224488
- hasAutofocus: false
224489
- };
224490
- const hasAutofocus = this.config.get("deviceCache")?.autoFocusSnapshot?.supported === true;
224491
- return this.resolveCapOptions({
224492
- capName: "ptz",
224493
- schema: PtzOptionsSchema,
224494
- probe: async () => {
224495
- const { capabilities } = await (await this.ensureApi()).getDeviceCapabilities(this.getChannel());
224496
- return {
224497
- hasPan: capabilities.hasPan,
224498
- hasTilt: capabilities.hasTilt,
224499
- hasZoom: capabilities.hasZoom,
224500
- supportsPresets: capabilities.hasPresets,
224501
- hasAutofocus
224502
- };
224503
- },
224504
- fallback: () => {
224505
- const hasPtz = this.getProbeFlags().hasPtz === true;
224506
- return {
224507
- hasPan: hasPtz,
224508
- hasTilt: hasPtz,
224509
- hasZoom: hasPtz,
224510
- supportsPresets: hasPtz,
224511
- hasAutofocus
224512
- };
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 [];
224513
224569
  }
224514
- });
224515
- },
224516
- goHome: async ({ deviceId }) => {
224517
- if (deviceId !== this.id) return;
224518
- try {
224570
+ },
224571
+ goToPreset: async ({ deviceId, presetId }) => {
224572
+ if (deviceId !== this.id) return;
224519
224573
  const api = await this.ensureApi();
224520
224574
  const channel = this.getChannel();
224521
- await api.moveToPtzPreset(channel, 0);
224522
- } catch {}
224523
- },
224524
- getPosition: async ({ deviceId }) => {
224525
- if (deviceId !== this.id) return {
224526
- pan: 0,
224527
- tilt: 0,
224528
- zoom: 0
224529
- };
224530
- return {
224531
- pan: 0,
224532
- tilt: 0,
224533
- zoom: 0
224534
- };
224535
- },
224536
- getStatus: async ({ deviceId }) => {
224537
- if (deviceId !== this.id) return {
224538
- pan: 0,
224539
- tilt: 0,
224540
- zoom: 0,
224541
- autofocus: false
224542
- };
224543
- return {
224544
- pan: 0,
224545
- tilt: 0,
224546
- zoom: 0,
224547
- autofocus: (this.config.get("deviceCache")?.autoFocusSnapshot)?.enabled === true
224548
- };
224549
- },
224550
- setAutofocus: async ({ deviceId, enabled }) => {
224551
- if (deviceId !== this.id) return;
224552
- const api = await this.ensureApi();
224553
- const channel = this.getChannel();
224554
- await api.setAutoFocus(channel, enabled ? 0 : 1);
224555
- try {
224556
- const a = (await api.getAutoFocus(channel, { timeoutMs: 1500 }))?.body?.AutoFocus;
224557
- if (a) await this.config.setAll({ deviceCache: {
224558
- ...this.config.get("deviceCache"),
224559
- autoFocusSnapshot: {
224560
- enabled: typeof a.disable === "number" ? a.disable === 0 : null,
224561
- 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
+ };
224562
224626
  }
224563
- } });
224564
- } catch {}
224565
- }
224566
- });
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
+ }
224567
224685
  }
224568
224686
  /**
224569
224687
  * Translate normalized (-1..1) pan/tilt/zoom to one or more discrete
@@ -225266,7 +225384,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
225266
225384
  if (this.batteryUpdateInFlight) return this.batteryUpdateInFlight;
225267
225385
  const cycle = (async () => {
225268
225386
  try {
225269
- if (!this.allowProactiveCameraAccess("battery-update")) {
225387
+ if (this.isBattery && this.sleeping) {
225270
225388
  this.ctx.logger.debug("battery-update: cam sleeping — skipping cycle without connecting", { tags: { deviceId: this.id } });
225271
225389
  return;
225272
225390
  }
@@ -225368,6 +225486,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
225368
225486
  meta: { error: err instanceof Error ? err.message : String(err) }
225369
225487
  });
225370
225488
  }
225489
+ await this.warmCapSlicesOnWake();
225490
+ await this.warmSnapshotOnWake();
225371
225491
  await this.alignAuxDevicesState("wake");
225372
225492
  await this.refreshParentSettingsSnapshot();
225373
225493
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",