@camstack/addon-provider-reolink 1.2.87 → 1.2.88

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
@@ -13648,7 +13648,12 @@ var ConfigEntrySchema = object({
13648
13648
  value: unknown(),
13649
13649
  description: string().optional()
13650
13650
  });
13651
- var LinkedDevicesModeSchema = _enum(["auto", "manual"]);
13651
+ /**
13652
+ * Only `manual` since D356: the operator picks, nothing links itself. The
13653
+ * field survives on the wire because a viewer already OTA'd parses it —
13654
+ * drop it once no shipped client reads `mode`.
13655
+ */
13656
+ var LinkedDevicesModeSchema = _enum(["manual"]);
13652
13657
  /** One resolved linked device — the compact projection consumers need. */
13653
13658
  var LinkedDeviceSchema = object({
13654
13659
  deviceId: number(),
@@ -34222,6 +34227,17 @@ var BaseDeviceProvider = class extends BaseAddon {
34222
34227
  return toDeviceSummary(device, this.addonId);
34223
34228
  }
34224
34229
  };
34230
+ /**
34231
+ * Default silence budget before a sleeping battery camera is called gone.
34232
+ *
34233
+ * Sized off the mechanism that produces contact, not off taste: a battery cam
34234
+ * on this deployment surfaces a firmware push (battery level, sleep/wake, or a
34235
+ * motion email) on the order of hours even with no visitors, and the snapshot
34236
+ * wrapper's own battery window is 1 hour. Six hours is therefore several
34237
+ * missed opportunities, not one — so a single quiet night does not raise a
34238
+ * fault, and a genuinely flat camera is named well inside a day.
34239
+ */
34240
+ var BATTERY_UNREACHABLE_AFTER_MS = 360 * 6e4;
34225
34241
  DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
34226
34242
  new Set(Object.values(DeviceType));
34227
34243
  DeviceFeature.BatteryOperated;
@@ -231810,6 +231826,15 @@ function reportsPowerSource(info) {
231810
231826
  return info.adapterStatus !== void 0 || info.chargeStatus !== void 0;
231811
231827
  }
231812
231828
  //#endregion
231829
+ //#region src/battery-contact-expiry.ts
231830
+ function shouldWakeForContact(input) {
231831
+ if (input.hubChild) return false;
231832
+ if (!input.sleeping) return false;
231833
+ const last = input.lastContactAt;
231834
+ if (typeof last !== "number" || last <= 0) return false;
231835
+ return input.nowMs - last + input.tickMs >= input.unreachableAfterMs;
231836
+ }
231837
+ //#endregion
231813
231838
  //#region src/log-channels.ts
231814
231839
  /**
231815
231840
  * The diagnostic log CHANNELS `provider-reolink` declares.
@@ -240470,6 +240495,17 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
240470
240495
  const cycle = (async () => {
240471
240496
  try {
240472
240497
  if (this.isBattery && this.sleeping) {
240498
+ if (shouldWakeForContact({
240499
+ nowMs: Date.now(),
240500
+ lastContactAt: this.state.battery.lastContactAt,
240501
+ sleeping: true,
240502
+ hubChild: this.isHubChild(),
240503
+ unreachableAfterMs: 216e5,
240504
+ tickMs: ReolinkCamera.BATTERY_UPDATE_INTERVAL_MS
240505
+ })) {
240506
+ await this.wakeForContactRefresh();
240507
+ return;
240508
+ }
240473
240509
  this.ctx.logger.debug("battery-update: cam sleeping — skipping cycle without connecting", { tags: { deviceId: this.id } });
240474
240510
  return;
240475
240511
  }
@@ -240514,6 +240550,43 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
240514
240550
  this.batteryUpdateInFlight = cycle;
240515
240551
  return cycle;
240516
240552
  }
240553
+ /**
240554
+ * D355: wake a standalone battery camera ONCE because its last passive
240555
+ * contact is about to expire, and refresh every slice while it is up.
240556
+ * Bounded by `canProactivelyWake` (the shared cooldown) and by the fact
240557
+ * that a wake that answers resets the silence. A wake that fails is left
240558
+ * alone: the silence then reaches the budget and `deriveBatteryPresence`
240559
+ * says `unreachable` — for the first time on evidence.
240560
+ */
240561
+ async wakeForContactRefresh() {
240562
+ if (!this.canProactivelyWake("contact-expiry")) return;
240563
+ this.markWakeIssued();
240564
+ const lastContactAt = this.state.battery.lastContactAt ?? 0;
240565
+ const silenceMs = Date.now() - lastContactAt;
240566
+ this.ctx.logger.info("battery contact expiring — waking once to refresh every slice (D355)", {
240567
+ tags: { deviceId: this.id },
240568
+ meta: {
240569
+ silenceMs,
240570
+ budgetMs: BATTERY_UNREACHABLE_AFTER_MS
240571
+ }
240572
+ });
240573
+ try {
240574
+ await this.ensureApi();
240575
+ } catch (err) {
240576
+ this.ctx.logger.warn("battery contact wake FAILED — silence continues and the camera will read unreachable at the budget", {
240577
+ tags: { deviceId: this.id },
240578
+ meta: {
240579
+ silenceMs,
240580
+ error: err instanceof Error ? err.message : String(err)
240581
+ }
240582
+ });
240583
+ return;
240584
+ }
240585
+ await this.refreshBatteryFromApi("demand").catch(() => {});
240586
+ await this.warmCapSlicesOnWake().catch(() => {});
240587
+ await this.alignAuxDevicesState("wake").catch(() => {});
240588
+ await this.warmSnapshotOnWake().catch(() => {});
240589
+ }
240517
240590
  /** Arm the periodic battery-cam refresh timer. No-op for wired cams
240518
240591
  * (their state is kept fresh by the 10 s aux-align poll and push
240519
240592
  * events). Idempotent. */
package/dist/addon.mjs CHANGED
@@ -13643,7 +13643,12 @@ var ConfigEntrySchema = object({
13643
13643
  value: unknown(),
13644
13644
  description: string().optional()
13645
13645
  });
13646
- var LinkedDevicesModeSchema = _enum(["auto", "manual"]);
13646
+ /**
13647
+ * Only `manual` since D356: the operator picks, nothing links itself. The
13648
+ * field survives on the wire because a viewer already OTA'd parses it —
13649
+ * drop it once no shipped client reads `mode`.
13650
+ */
13651
+ var LinkedDevicesModeSchema = _enum(["manual"]);
13647
13652
  /** One resolved linked device — the compact projection consumers need. */
13648
13653
  var LinkedDeviceSchema = object({
13649
13654
  deviceId: number(),
@@ -34217,6 +34222,17 @@ var BaseDeviceProvider = class extends BaseAddon {
34217
34222
  return toDeviceSummary(device, this.addonId);
34218
34223
  }
34219
34224
  };
34225
+ /**
34226
+ * Default silence budget before a sleeping battery camera is called gone.
34227
+ *
34228
+ * Sized off the mechanism that produces contact, not off taste: a battery cam
34229
+ * on this deployment surfaces a firmware push (battery level, sleep/wake, or a
34230
+ * motion email) on the order of hours even with no visitors, and the snapshot
34231
+ * wrapper's own battery window is 1 hour. Six hours is therefore several
34232
+ * missed opportunities, not one — so a single quiet night does not raise a
34233
+ * fault, and a genuinely flat camera is named well inside a day.
34234
+ */
34235
+ var BATTERY_UNREACHABLE_AFTER_MS = 360 * 6e4;
34220
34236
  DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
34221
34237
  new Set(Object.values(DeviceType));
34222
34238
  DeviceFeature.BatteryOperated;
@@ -231790,6 +231806,15 @@ function reportsPowerSource(info) {
231790
231806
  return info.adapterStatus !== void 0 || info.chargeStatus !== void 0;
231791
231807
  }
231792
231808
  //#endregion
231809
+ //#region src/battery-contact-expiry.ts
231810
+ function shouldWakeForContact(input) {
231811
+ if (input.hubChild) return false;
231812
+ if (!input.sleeping) return false;
231813
+ const last = input.lastContactAt;
231814
+ if (typeof last !== "number" || last <= 0) return false;
231815
+ return input.nowMs - last + input.tickMs >= input.unreachableAfterMs;
231816
+ }
231817
+ //#endregion
231793
231818
  //#region src/log-channels.ts
231794
231819
  /**
231795
231820
  * The diagnostic log CHANNELS `provider-reolink` declares.
@@ -240450,6 +240475,17 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
240450
240475
  const cycle = (async () => {
240451
240476
  try {
240452
240477
  if (this.isBattery && this.sleeping) {
240478
+ if (shouldWakeForContact({
240479
+ nowMs: Date.now(),
240480
+ lastContactAt: this.state.battery.lastContactAt,
240481
+ sleeping: true,
240482
+ hubChild: this.isHubChild(),
240483
+ unreachableAfterMs: 216e5,
240484
+ tickMs: ReolinkCamera.BATTERY_UPDATE_INTERVAL_MS
240485
+ })) {
240486
+ await this.wakeForContactRefresh();
240487
+ return;
240488
+ }
240453
240489
  this.ctx.logger.debug("battery-update: cam sleeping — skipping cycle without connecting", { tags: { deviceId: this.id } });
240454
240490
  return;
240455
240491
  }
@@ -240494,6 +240530,43 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
240494
240530
  this.batteryUpdateInFlight = cycle;
240495
240531
  return cycle;
240496
240532
  }
240533
+ /**
240534
+ * D355: wake a standalone battery camera ONCE because its last passive
240535
+ * contact is about to expire, and refresh every slice while it is up.
240536
+ * Bounded by `canProactivelyWake` (the shared cooldown) and by the fact
240537
+ * that a wake that answers resets the silence. A wake that fails is left
240538
+ * alone: the silence then reaches the budget and `deriveBatteryPresence`
240539
+ * says `unreachable` — for the first time on evidence.
240540
+ */
240541
+ async wakeForContactRefresh() {
240542
+ if (!this.canProactivelyWake("contact-expiry")) return;
240543
+ this.markWakeIssued();
240544
+ const lastContactAt = this.state.battery.lastContactAt ?? 0;
240545
+ const silenceMs = Date.now() - lastContactAt;
240546
+ this.ctx.logger.info("battery contact expiring — waking once to refresh every slice (D355)", {
240547
+ tags: { deviceId: this.id },
240548
+ meta: {
240549
+ silenceMs,
240550
+ budgetMs: BATTERY_UNREACHABLE_AFTER_MS
240551
+ }
240552
+ });
240553
+ try {
240554
+ await this.ensureApi();
240555
+ } catch (err) {
240556
+ this.ctx.logger.warn("battery contact wake FAILED — silence continues and the camera will read unreachable at the budget", {
240557
+ tags: { deviceId: this.id },
240558
+ meta: {
240559
+ silenceMs,
240560
+ error: err instanceof Error ? err.message : String(err)
240561
+ }
240562
+ });
240563
+ return;
240564
+ }
240565
+ await this.refreshBatteryFromApi("demand").catch(() => {});
240566
+ await this.warmCapSlicesOnWake().catch(() => {});
240567
+ await this.alignAuxDevicesState("wake").catch(() => {});
240568
+ await this.warmSnapshotOnWake().catch(() => {});
240569
+ }
240497
240570
  /** Arm the periodic battery-cam refresh timer. No-op for wired cams
240498
240571
  * (their state is kept fresh by the 10 s aux-align poll and push
240499
240572
  * events). Idempotent. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.87",
3
+ "version": "1.2.88",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",