@camstack/addon-provider-reolink 1.2.92 → 1.2.93

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
@@ -26,7 +26,7 @@ let fs_promises = require("fs/promises");
26
26
  fs_promises = require_chunk.__toESM(fs_promises, 1);
27
27
  let node_os = require("node:os");
28
28
  node_os = require_chunk.__toESM(node_os);
29
- //#region ../types/dist/event-category-BZL-fdNj.mjs
29
+ //#region ../types/dist/event-category-zAv7pMUz.mjs
30
30
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
31
31
  EventCategory["SystemBoot"] = "system.boot";
32
32
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -398,6 +398,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
398
398
  */
399
399
  EventCategory["BatteryOnStatusChanged"] = "battery.onStatusChanged";
400
400
  /**
401
+ * Cap event fired by every device that registers the `network-link`
402
+ * capability. Mirrors the cap definition's `onStatusChanged`. Carries
403
+ * `{ deviceId, status: NetworkLinkStatus }` — a link switch or a signal
404
+ * reading that moved.
405
+ */
406
+ EventCategory["NetworkLinkOnStatusChanged"] = "network-link.onStatusChanged";
407
+ /**
401
408
  * Emitted by the battery cap provider WHEN `wakeForStream` enters the
402
409
  * "wake in progress" window — between the Baichuan wake-up issue and
403
410
  * the camera's first dialed-back RTP packet. The stream-broker
@@ -24498,6 +24505,108 @@ onStatusChanged: { data: object({
24498
24505
  volatileStateFields: ["lastUpdated"]
24499
24506
  };
24500
24507
  /**
24508
+ * Network-link snapshot. Same shape for every provider (a Reolink wifi
24509
+ * camera, a Home Assistant device with a signal-strength sensor, a Tapo
24510
+ * plug): one slice under `device.runtimeState['network-link']`, one badge,
24511
+ * one Home Assistant projection.
24512
+ */
24513
+ var NetworkLinkStatusSchema = object({
24514
+ /** The link the device is on. `'unknown'` = not read yet, not "no link". */
24515
+ type: _enum([
24516
+ "wifi",
24517
+ "ethernet",
24518
+ "cellular",
24519
+ "unknown"
24520
+ ]),
24521
+ /**
24522
+ * Link quality, 0..100 inclusive, normalised by the provider from whatever
24523
+ * the firmware reports (bars, RSSI, a vendor scale). **`null` means NOT
24524
+ * KNOWN or NOT APPLICABLE** — a wired link has no signal, and a wireless
24525
+ * one whose reading has not landed must not be drawn at 0 %. Consumers
24526
+ * SKIP a null rather than coerce it.
24527
+ */
24528
+ signalPercent: number().min(0).max(100).nullable(),
24529
+ /** Raw received signal strength in dBm, when the firmware reports one. */
24530
+ rssiDbm: number().optional(),
24531
+ /** Network name of a wireless link, when the firmware reports it. */
24532
+ ssid: string().optional(),
24533
+ /** Ms epoch of the last observation. Lets consumers reason about freshness. */
24534
+ lastUpdated: number()
24535
+ });
24536
+ /** The slice a provider seeds before its first read: nothing is known yet. */
24537
+ var NETWORK_LINK_UNKNOWN = {
24538
+ type: "unknown",
24539
+ signalPercent: null,
24540
+ lastUpdated: 0
24541
+ };
24542
+ /**
24543
+ * Normalise a signal the firmware reports as BARS (0..`maxBars`) to 0..100.
24544
+ * Out-of-range or non-finite input is not a reading: `null`.
24545
+ */
24546
+ function signalPercentFromBars(bars, maxBars) {
24547
+ if (bars === void 0 || !Number.isFinite(bars) || maxBars <= 0) return null;
24548
+ if (bars < 0 || bars > maxBars) return null;
24549
+ return Math.round(bars / maxBars * 100);
24550
+ }
24551
+ /**
24552
+ * Normalise an RSSI in dBm to 0..100 on the usual wifi scale: -100 dBm and
24553
+ * below is 0 %, -50 dBm and above is 100 %, linear between. Non-finite or
24554
+ * positive input is not an RSSI: `null`.
24555
+ */
24556
+ function signalPercentFromRssi(rssiDbm) {
24557
+ if (rssiDbm === void 0 || !Number.isFinite(rssiDbm) || rssiDbm > 0) return null;
24558
+ return Math.round((Math.min(-50, Math.max(-100, rssiDbm)) + 100) * 2);
24559
+ }
24560
+ var networkLinkCapability = {
24561
+ name: "network-link",
24562
+ scope: "device",
24563
+ deviceNative: true,
24564
+ mode: "singleton",
24565
+ deviceTypes: [
24566
+ DeviceType.Camera,
24567
+ DeviceType.Sensor,
24568
+ DeviceType.Button,
24569
+ DeviceType.Switch,
24570
+ DeviceType.Light,
24571
+ DeviceType.Lock,
24572
+ DeviceType.Siren
24573
+ ],
24574
+ methods: {},
24575
+ events: {
24576
+ /**
24577
+ * Emitted whenever the cached status changes (a link switch, a signal
24578
+ * reading that moved). Mirrored on the parent chain by the
24579
+ * DeviceEventPropagator like `battery.onStatusChanged`.
24580
+ */
24581
+ onStatusChanged: { data: object({
24582
+ deviceId: number(),
24583
+ status: NetworkLinkStatusSchema
24584
+ }) } },
24585
+ status: {
24586
+ schema: NetworkLinkStatusSchema,
24587
+ kind: "push",
24588
+ empty: NETWORK_LINK_UNKNOWN
24589
+ },
24590
+ /**
24591
+ * Runtime-state slice — every provider stores the same shape under
24592
+ * `device.runtimeState['network-link']`, read once by the badge and the
24593
+ * Home Assistant projector regardless of the driver.
24594
+ */
24595
+ runtimeState: NetworkLinkStatusSchema,
24596
+ /**
24597
+ * Runtime-state durability: **restored** — a link reading is slow to
24598
+ * change and a sleeping battery camera may not report for hours; the
24599
+ * restored slice is what the badge shows until the next read.
24600
+ *
24601
+ * See `RuntimeStateDurability`. Enforced by
24602
+ * `scripts/check-runtime-state-durability.ts`.
24603
+ */
24604
+ durability: "restored",
24605
+ /** Clock fields: written, but excluded from the compare that decides
24606
+ * whether persisting is worth a SQLite commit. */
24607
+ volatileStateFields: ["lastUpdated"]
24608
+ };
24609
+ /**
24501
24610
  * Generic boolean sensor — last-resort fallback when no domain-
24502
24611
  * specific binary cap fits (Home Assistant `binary_sensor` without a
24503
24612
  * known `device_class`, or a domain we haven't typed yet). Pure
@@ -33182,6 +33291,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
33182
33291
  motionTrigger: motionTriggerCapability,
33183
33292
  motionZones: motionZonesCapability,
33184
33293
  nativeObjectDetection: nativeObjectDetectionCapability,
33294
+ networkLink: networkLinkCapability,
33185
33295
  notifier: notifierCapability,
33186
33296
  numericSensor: numericSensorCapability,
33187
33297
  petFeeder: petFeederCapability,
@@ -233594,6 +233704,50 @@ async function populateReolinkMetadata(api, channel, target) {
233594
233704
  });
233595
233705
  }
233596
233706
  }
233707
+ /**
233708
+ * The link the label names. A label the firmware did not give is `unknown`,
233709
+ * UNLESS a wifi signal was read — a camera that answers a wifi signal is on
233710
+ * wifi whatever its label says.
233711
+ */
233712
+ function linkTypeOf(readout) {
233713
+ const label = (readout.activeLink ?? "").toLowerCase();
233714
+ if (label.includes("wifi") || label.includes("wlan") || label.includes("wireless")) return "wifi";
233715
+ if (label.includes("lan") || label.includes("eth") || label.includes("wire")) return "ethernet";
233716
+ if (/\b(4g|5g|lte|cell|sim)\b/.test(label)) return "cellular";
233717
+ if (readout.wifiSignal !== void 0) return "wifi";
233718
+ return "unknown";
233719
+ }
233720
+ /** True when the label names a wireless link — the only case worth a signal read. */
233721
+ function isWirelessLabel(activeLink) {
233722
+ const type = linkTypeOf({
233723
+ activeLink,
233724
+ wifiSignal: void 0
233725
+ });
233726
+ return type === "wifi" || type === "cellular";
233727
+ }
233728
+ /** 0..4 → bars; 5..100 → percent; negative → dBm; anything else → not a reading. */
233729
+ function signalPercentOf(raw) {
233730
+ if (raw === void 0 || !Number.isFinite(raw)) return null;
233731
+ if (raw < 0) return signalPercentFromRssi(raw);
233732
+ if (raw <= 4) return signalPercentFromBars(raw, 4);
233733
+ if (raw <= 100) return Math.round(raw);
233734
+ return null;
233735
+ }
233736
+ function mapNetworkReadout(readout, now) {
233737
+ const type = linkTypeOf(readout);
233738
+ const raw = readout.wifiSignal;
233739
+ const wireless = type === "wifi" || type === "cellular";
233740
+ const signalPercent = wireless ? signalPercentOf(raw) : null;
233741
+ const rssiDbm = wireless && raw !== void 0 && Number.isFinite(raw) && raw < 0 ? raw : void 0;
233742
+ const ssid = wireless && readout.ssid !== void 0 && readout.ssid !== "" ? readout.ssid : void 0;
233743
+ return {
233744
+ type,
233745
+ signalPercent,
233746
+ ...rssiDbm !== void 0 ? { rssiDbm } : {},
233747
+ ...ssid !== void 0 ? { ssid } : {},
233748
+ lastUpdated: now
233749
+ };
233750
+ }
233597
233751
  //#endregion
233598
233752
  //#region src/error-classifier.ts
233599
233753
  /**
@@ -236393,6 +236547,58 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
236393
236547
  meta: { error: err instanceof Error ? err.message : String(err) }
236394
236548
  });
236395
236549
  });
236550
+ await this.refreshNetworkLinkFromApi(api, "probe");
236551
+ }
236552
+ /**
236553
+ * Register the `network-link` cap: the read path serves the slice, the
236554
+ * writers are {@link refreshNetworkLinkFromApi}. Seeded unknown so the
236555
+ * restored slice (if any) stays valid and a fresh device draws no link.
236556
+ */
236557
+ registerNetworkLink() {
236558
+ this.ctx.registerNativeCap(networkLinkCapability, { getStatus: async ({ deviceId }) => {
236559
+ if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
236560
+ return this.state.networkLink;
236561
+ } });
236562
+ if (this.getCapSlice(networkLinkCapability) === null) this.setCapSlice(networkLinkCapability, NETWORK_LINK_UNKNOWN);
236563
+ }
236564
+ /**
236565
+ * Read the active link and, on a wireless one, its signal and network name
236566
+ * over a socket that is ALREADY up. Called post-login and after a
236567
+ * successful battery read — never on its own timer, so a sleeping battery
236568
+ * camera is never woken for a bar count. Each read is best-effort with its
236569
+ * own bound; a failure keeps the last slice and says so at debug level.
236570
+ */
236571
+ async refreshNetworkLinkFromApi(api, reason) {
236572
+ const channel = this.getChannel();
236573
+ const timeoutMs = ReolinkCamera.NETWORK_LINK_READ_TIMEOUT_MS;
236574
+ try {
236575
+ const activeLink = (await api.getNetworkInfo(channel, { timeoutMs }))?.activeLink;
236576
+ const askWireless = activeLink === void 0 || isWirelessLabel(activeLink);
236577
+ const wifiSignal = askWireless ? (await api.getWifiSignal(channel, { timeoutMs }).catch(() => ({ signal: void 0 }))).signal : void 0;
236578
+ const mapped = mapNetworkReadout({
236579
+ activeLink,
236580
+ wifiSignal,
236581
+ ssid: askWireless && wifiSignal !== void 0 ? (await api.getWifi(channel, { timeoutMs }).catch(() => ({ ssid: void 0 }))).ssid : void 0
236582
+ }, Date.now());
236583
+ this.ctx.logger.debug("network link read", {
236584
+ tags: { deviceId: this.id },
236585
+ meta: {
236586
+ reason,
236587
+ activeLink,
236588
+ rawSignal: wifiSignal,
236589
+ ...mapped
236590
+ }
236591
+ });
236592
+ this.setCapSlice(networkLinkCapability, mapped);
236593
+ } catch (err) {
236594
+ this.ctx.logger.debug("network link read failed — keeping the last slice", {
236595
+ tags: { deviceId: this.id },
236596
+ meta: {
236597
+ reason,
236598
+ error: err instanceof Error ? err.message : String(err)
236599
+ }
236600
+ });
236601
+ }
236396
236602
  }
236397
236603
  /**
236398
236604
  * Phase 5 (kernel-driven) — fired after `onProbe()` + accessory
@@ -236950,6 +237156,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
236950
237156
  await (await this.ensureApi()).reboot(this.getChannel());
236951
237157
  return { success: true };
236952
237158
  } });
237159
+ this.registerNetworkLink();
236953
237160
  this.ctx.registerNativeCap(motionCapability, { isDetected: async ({ deviceId }) => {
236954
237161
  if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
236955
237162
  return this.state.motion.detected ?? false;
@@ -237204,6 +237411,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
237204
237411
  try {
237205
237412
  const info = await api.getBatteryInfo(this.getChannel());
237206
237413
  this.updateBatteryCache(info);
237414
+ await this.refreshNetworkLinkFromApi(api, "battery");
237207
237415
  } catch (err) {
237208
237416
  this.ctx.logger.debug("battery refresh failed", {
237209
237417
  tags: { deviceId: this.id },
@@ -240741,6 +240949,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
240741
240949
  /** Write granularity for `battery.lastContactAt` — see `markPassiveContact`.
240742
240950
  * Bounds the commit rate this field can cost at 12/hour/device, and only
240743
240951
  * for a device something is actually reaching. */
240952
+ /** Bound on each best-effort network read; three reads at most per refresh. */
240953
+ static NETWORK_LINK_READ_TIMEOUT_MS = 4e3;
240744
240954
  static CONTACT_WRITE_QUANTUM_MS = 5 * 6e4;
240745
240955
  /**
240746
240956
  * Shared wake-transition handler invoked by both the simpleEvent
package/dist/addon.mjs CHANGED
@@ -21,7 +21,7 @@ import netImpl from "net";
21
21
  import { fileURLToPath } from "url";
22
22
  import { mkdir } from "fs/promises";
23
23
  import os from "node:os";
24
- //#region ../types/dist/event-category-BZL-fdNj.mjs
24
+ //#region ../types/dist/event-category-zAv7pMUz.mjs
25
25
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
26
26
  EventCategory["SystemBoot"] = "system.boot";
27
27
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -393,6 +393,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
393
393
  */
394
394
  EventCategory["BatteryOnStatusChanged"] = "battery.onStatusChanged";
395
395
  /**
396
+ * Cap event fired by every device that registers the `network-link`
397
+ * capability. Mirrors the cap definition's `onStatusChanged`. Carries
398
+ * `{ deviceId, status: NetworkLinkStatus }` — a link switch or a signal
399
+ * reading that moved.
400
+ */
401
+ EventCategory["NetworkLinkOnStatusChanged"] = "network-link.onStatusChanged";
402
+ /**
396
403
  * Emitted by the battery cap provider WHEN `wakeForStream` enters the
397
404
  * "wake in progress" window — between the Baichuan wake-up issue and
398
405
  * the camera's first dialed-back RTP packet. The stream-broker
@@ -24493,6 +24500,108 @@ onStatusChanged: { data: object({
24493
24500
  volatileStateFields: ["lastUpdated"]
24494
24501
  };
24495
24502
  /**
24503
+ * Network-link snapshot. Same shape for every provider (a Reolink wifi
24504
+ * camera, a Home Assistant device with a signal-strength sensor, a Tapo
24505
+ * plug): one slice under `device.runtimeState['network-link']`, one badge,
24506
+ * one Home Assistant projection.
24507
+ */
24508
+ var NetworkLinkStatusSchema = object({
24509
+ /** The link the device is on. `'unknown'` = not read yet, not "no link". */
24510
+ type: _enum([
24511
+ "wifi",
24512
+ "ethernet",
24513
+ "cellular",
24514
+ "unknown"
24515
+ ]),
24516
+ /**
24517
+ * Link quality, 0..100 inclusive, normalised by the provider from whatever
24518
+ * the firmware reports (bars, RSSI, a vendor scale). **`null` means NOT
24519
+ * KNOWN or NOT APPLICABLE** — a wired link has no signal, and a wireless
24520
+ * one whose reading has not landed must not be drawn at 0 %. Consumers
24521
+ * SKIP a null rather than coerce it.
24522
+ */
24523
+ signalPercent: number().min(0).max(100).nullable(),
24524
+ /** Raw received signal strength in dBm, when the firmware reports one. */
24525
+ rssiDbm: number().optional(),
24526
+ /** Network name of a wireless link, when the firmware reports it. */
24527
+ ssid: string().optional(),
24528
+ /** Ms epoch of the last observation. Lets consumers reason about freshness. */
24529
+ lastUpdated: number()
24530
+ });
24531
+ /** The slice a provider seeds before its first read: nothing is known yet. */
24532
+ var NETWORK_LINK_UNKNOWN = {
24533
+ type: "unknown",
24534
+ signalPercent: null,
24535
+ lastUpdated: 0
24536
+ };
24537
+ /**
24538
+ * Normalise a signal the firmware reports as BARS (0..`maxBars`) to 0..100.
24539
+ * Out-of-range or non-finite input is not a reading: `null`.
24540
+ */
24541
+ function signalPercentFromBars(bars, maxBars) {
24542
+ if (bars === void 0 || !Number.isFinite(bars) || maxBars <= 0) return null;
24543
+ if (bars < 0 || bars > maxBars) return null;
24544
+ return Math.round(bars / maxBars * 100);
24545
+ }
24546
+ /**
24547
+ * Normalise an RSSI in dBm to 0..100 on the usual wifi scale: -100 dBm and
24548
+ * below is 0 %, -50 dBm and above is 100 %, linear between. Non-finite or
24549
+ * positive input is not an RSSI: `null`.
24550
+ */
24551
+ function signalPercentFromRssi(rssiDbm) {
24552
+ if (rssiDbm === void 0 || !Number.isFinite(rssiDbm) || rssiDbm > 0) return null;
24553
+ return Math.round((Math.min(-50, Math.max(-100, rssiDbm)) + 100) * 2);
24554
+ }
24555
+ var networkLinkCapability = {
24556
+ name: "network-link",
24557
+ scope: "device",
24558
+ deviceNative: true,
24559
+ mode: "singleton",
24560
+ deviceTypes: [
24561
+ DeviceType.Camera,
24562
+ DeviceType.Sensor,
24563
+ DeviceType.Button,
24564
+ DeviceType.Switch,
24565
+ DeviceType.Light,
24566
+ DeviceType.Lock,
24567
+ DeviceType.Siren
24568
+ ],
24569
+ methods: {},
24570
+ events: {
24571
+ /**
24572
+ * Emitted whenever the cached status changes (a link switch, a signal
24573
+ * reading that moved). Mirrored on the parent chain by the
24574
+ * DeviceEventPropagator like `battery.onStatusChanged`.
24575
+ */
24576
+ onStatusChanged: { data: object({
24577
+ deviceId: number(),
24578
+ status: NetworkLinkStatusSchema
24579
+ }) } },
24580
+ status: {
24581
+ schema: NetworkLinkStatusSchema,
24582
+ kind: "push",
24583
+ empty: NETWORK_LINK_UNKNOWN
24584
+ },
24585
+ /**
24586
+ * Runtime-state slice — every provider stores the same shape under
24587
+ * `device.runtimeState['network-link']`, read once by the badge and the
24588
+ * Home Assistant projector regardless of the driver.
24589
+ */
24590
+ runtimeState: NetworkLinkStatusSchema,
24591
+ /**
24592
+ * Runtime-state durability: **restored** — a link reading is slow to
24593
+ * change and a sleeping battery camera may not report for hours; the
24594
+ * restored slice is what the badge shows until the next read.
24595
+ *
24596
+ * See `RuntimeStateDurability`. Enforced by
24597
+ * `scripts/check-runtime-state-durability.ts`.
24598
+ */
24599
+ durability: "restored",
24600
+ /** Clock fields: written, but excluded from the compare that decides
24601
+ * whether persisting is worth a SQLite commit. */
24602
+ volatileStateFields: ["lastUpdated"]
24603
+ };
24604
+ /**
24496
24605
  * Generic boolean sensor — last-resort fallback when no domain-
24497
24606
  * specific binary cap fits (Home Assistant `binary_sensor` without a
24498
24607
  * known `device_class`, or a domain we haven't typed yet). Pure
@@ -33177,6 +33286,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
33177
33286
  motionTrigger: motionTriggerCapability,
33178
33287
  motionZones: motionZonesCapability,
33179
33288
  nativeObjectDetection: nativeObjectDetectionCapability,
33289
+ networkLink: networkLinkCapability,
33180
33290
  notifier: notifierCapability,
33181
33291
  numericSensor: numericSensorCapability,
33182
33292
  petFeeder: petFeederCapability,
@@ -233574,6 +233684,50 @@ async function populateReolinkMetadata(api, channel, target) {
233574
233684
  });
233575
233685
  }
233576
233686
  }
233687
+ /**
233688
+ * The link the label names. A label the firmware did not give is `unknown`,
233689
+ * UNLESS a wifi signal was read — a camera that answers a wifi signal is on
233690
+ * wifi whatever its label says.
233691
+ */
233692
+ function linkTypeOf(readout) {
233693
+ const label = (readout.activeLink ?? "").toLowerCase();
233694
+ if (label.includes("wifi") || label.includes("wlan") || label.includes("wireless")) return "wifi";
233695
+ if (label.includes("lan") || label.includes("eth") || label.includes("wire")) return "ethernet";
233696
+ if (/\b(4g|5g|lte|cell|sim)\b/.test(label)) return "cellular";
233697
+ if (readout.wifiSignal !== void 0) return "wifi";
233698
+ return "unknown";
233699
+ }
233700
+ /** True when the label names a wireless link — the only case worth a signal read. */
233701
+ function isWirelessLabel(activeLink) {
233702
+ const type = linkTypeOf({
233703
+ activeLink,
233704
+ wifiSignal: void 0
233705
+ });
233706
+ return type === "wifi" || type === "cellular";
233707
+ }
233708
+ /** 0..4 → bars; 5..100 → percent; negative → dBm; anything else → not a reading. */
233709
+ function signalPercentOf(raw) {
233710
+ if (raw === void 0 || !Number.isFinite(raw)) return null;
233711
+ if (raw < 0) return signalPercentFromRssi(raw);
233712
+ if (raw <= 4) return signalPercentFromBars(raw, 4);
233713
+ if (raw <= 100) return Math.round(raw);
233714
+ return null;
233715
+ }
233716
+ function mapNetworkReadout(readout, now) {
233717
+ const type = linkTypeOf(readout);
233718
+ const raw = readout.wifiSignal;
233719
+ const wireless = type === "wifi" || type === "cellular";
233720
+ const signalPercent = wireless ? signalPercentOf(raw) : null;
233721
+ const rssiDbm = wireless && raw !== void 0 && Number.isFinite(raw) && raw < 0 ? raw : void 0;
233722
+ const ssid = wireless && readout.ssid !== void 0 && readout.ssid !== "" ? readout.ssid : void 0;
233723
+ return {
233724
+ type,
233725
+ signalPercent,
233726
+ ...rssiDbm !== void 0 ? { rssiDbm } : {},
233727
+ ...ssid !== void 0 ? { ssid } : {},
233728
+ lastUpdated: now
233729
+ };
233730
+ }
233577
233731
  //#endregion
233578
233732
  //#region src/error-classifier.ts
233579
233733
  /**
@@ -236373,6 +236527,58 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
236373
236527
  meta: { error: err instanceof Error ? err.message : String(err) }
236374
236528
  });
236375
236529
  });
236530
+ await this.refreshNetworkLinkFromApi(api, "probe");
236531
+ }
236532
+ /**
236533
+ * Register the `network-link` cap: the read path serves the slice, the
236534
+ * writers are {@link refreshNetworkLinkFromApi}. Seeded unknown so the
236535
+ * restored slice (if any) stays valid and a fresh device draws no link.
236536
+ */
236537
+ registerNetworkLink() {
236538
+ this.ctx.registerNativeCap(networkLinkCapability, { getStatus: async ({ deviceId }) => {
236539
+ if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
236540
+ return this.state.networkLink;
236541
+ } });
236542
+ if (this.getCapSlice(networkLinkCapability) === null) this.setCapSlice(networkLinkCapability, NETWORK_LINK_UNKNOWN);
236543
+ }
236544
+ /**
236545
+ * Read the active link and, on a wireless one, its signal and network name
236546
+ * over a socket that is ALREADY up. Called post-login and after a
236547
+ * successful battery read — never on its own timer, so a sleeping battery
236548
+ * camera is never woken for a bar count. Each read is best-effort with its
236549
+ * own bound; a failure keeps the last slice and says so at debug level.
236550
+ */
236551
+ async refreshNetworkLinkFromApi(api, reason) {
236552
+ const channel = this.getChannel();
236553
+ const timeoutMs = ReolinkCamera.NETWORK_LINK_READ_TIMEOUT_MS;
236554
+ try {
236555
+ const activeLink = (await api.getNetworkInfo(channel, { timeoutMs }))?.activeLink;
236556
+ const askWireless = activeLink === void 0 || isWirelessLabel(activeLink);
236557
+ const wifiSignal = askWireless ? (await api.getWifiSignal(channel, { timeoutMs }).catch(() => ({ signal: void 0 }))).signal : void 0;
236558
+ const mapped = mapNetworkReadout({
236559
+ activeLink,
236560
+ wifiSignal,
236561
+ ssid: askWireless && wifiSignal !== void 0 ? (await api.getWifi(channel, { timeoutMs }).catch(() => ({ ssid: void 0 }))).ssid : void 0
236562
+ }, Date.now());
236563
+ this.ctx.logger.debug("network link read", {
236564
+ tags: { deviceId: this.id },
236565
+ meta: {
236566
+ reason,
236567
+ activeLink,
236568
+ rawSignal: wifiSignal,
236569
+ ...mapped
236570
+ }
236571
+ });
236572
+ this.setCapSlice(networkLinkCapability, mapped);
236573
+ } catch (err) {
236574
+ this.ctx.logger.debug("network link read failed — keeping the last slice", {
236575
+ tags: { deviceId: this.id },
236576
+ meta: {
236577
+ reason,
236578
+ error: err instanceof Error ? err.message : String(err)
236579
+ }
236580
+ });
236581
+ }
236376
236582
  }
236377
236583
  /**
236378
236584
  * Phase 5 (kernel-driven) — fired after `onProbe()` + accessory
@@ -236930,6 +237136,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
236930
237136
  await (await this.ensureApi()).reboot(this.getChannel());
236931
237137
  return { success: true };
236932
237138
  } });
237139
+ this.registerNetworkLink();
236933
237140
  this.ctx.registerNativeCap(motionCapability, { isDetected: async ({ deviceId }) => {
236934
237141
  if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
236935
237142
  return this.state.motion.detected ?? false;
@@ -237184,6 +237391,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
237184
237391
  try {
237185
237392
  const info = await api.getBatteryInfo(this.getChannel());
237186
237393
  this.updateBatteryCache(info);
237394
+ await this.refreshNetworkLinkFromApi(api, "battery");
237187
237395
  } catch (err) {
237188
237396
  this.ctx.logger.debug("battery refresh failed", {
237189
237397
  tags: { deviceId: this.id },
@@ -240721,6 +240929,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
240721
240929
  /** Write granularity for `battery.lastContactAt` — see `markPassiveContact`.
240722
240930
  * Bounds the commit rate this field can cost at 12/hour/device, and only
240723
240931
  * for a device something is actually reaching. */
240932
+ /** Bound on each best-effort network read; three reads at most per refresh. */
240933
+ static NETWORK_LINK_READ_TIMEOUT_MS = 4e3;
240724
240934
  static CONTACT_WRITE_QUANTUM_MS = 5 * 6e4;
240725
240935
  /**
240726
240936
  * Shared wake-transition handler invoked by both the simpleEvent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.92",
3
+ "version": "1.2.93",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",