@camstack/addon-provider-reolink 1.2.39 → 1.2.41
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 +145 -10
- package/dist/addon.mjs +145 -10
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -230195,6 +230195,15 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
230195
230195
|
static WAKE_HYSTERESIS_MS = 3e4;
|
|
230196
230196
|
static SLEEP_HYSTERESIS_MS = 5e3;
|
|
230197
230197
|
/**
|
|
230198
|
+
* How long a freshly committed state is protected from a hub-summary
|
|
230199
|
+
* awake→sleeping flip. The summary's per-channel sleep flag lags real
|
|
230200
|
+
* pushes by over a minute (measured >75 s on 2026-08-16); 3 min covers
|
|
230201
|
+
* two full discovery-refresh cycles of lag without letting a genuinely
|
|
230202
|
+
* stuck "awake" slice stand forever — the reconcile still wins after
|
|
230203
|
+
* the grace, which is exactly its recovery role.
|
|
230204
|
+
*/
|
|
230205
|
+
static HUB_SUMMARY_SLEEP_GRACE_MS = 18e4;
|
|
230206
|
+
/**
|
|
230198
230207
|
* Wall-clock ms of the last PROACTIVE wake — one we issued on our own
|
|
230199
230208
|
* initiative (background snapshot refresh), not because the operator
|
|
230200
230209
|
* or a stream consumer asked for the camera. Drives
|
|
@@ -231174,15 +231183,35 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
231174
231183
|
*
|
|
231175
231184
|
* @param source - `'hub-summary'` is AUTHORITATIVE (the NVR reports
|
|
231176
231185
|
* per-channel sleep state from its own firmware, not from socket
|
|
231177
|
-
* I/O inference) and therefore bypasses the hysteresis
|
|
231178
|
-
*
|
|
231186
|
+
* I/O inference) and therefore bypasses the hysteresis — with ONE
|
|
231187
|
+
* exception: the summary LAGS real push state (measured >75 s behind
|
|
231188
|
+
* an `awake` push, 2026-08-16), so an awake→sleeping flip from the
|
|
231189
|
+
* summary is refused while a fresher committed state is still inside
|
|
231190
|
+
* `HUB_SUMMARY_SLEEP_GRACE_MS`. Without the guard the 60 s reconcile
|
|
231191
|
+
* clobbered the fresh awake right back to sleeping for a full cycle —
|
|
231192
|
+
* the viewer flashed the live frame, then the sleeping overlay, then
|
|
231193
|
+
* live again. Sleeping→awake from the summary is always honoured
|
|
231194
|
+
* (it strands nothing; the camera wakes regardless).
|
|
231179
231195
|
* @returns `true` when the slice actually changed.
|
|
231180
231196
|
*/
|
|
231181
231197
|
commitSleepState(next, source) {
|
|
231182
231198
|
if (!this.isBattery) return false;
|
|
231183
231199
|
if (this.sleeping === next) return false;
|
|
231184
231200
|
if (source !== "hub-summary" && !this.acceptSleepingTransition(next)) return false;
|
|
231185
|
-
if (source === "hub-summary")
|
|
231201
|
+
if (source === "hub-summary") {
|
|
231202
|
+
const sinceCommitMs = Date.now() - this.sleepStateChangedAt;
|
|
231203
|
+
if (next === true && this.sleepStateChangedAt > 0 && sinceCommitMs < ReolinkCamera.HUB_SUMMARY_SLEEP_GRACE_MS) {
|
|
231204
|
+
this.ctx.logger.debug("hub-summary sleep flip refused — fresher state inside grace", {
|
|
231205
|
+
tags: { deviceId: this.id },
|
|
231206
|
+
meta: {
|
|
231207
|
+
sinceCommitMs,
|
|
231208
|
+
graceMs: ReolinkCamera.HUB_SUMMARY_SLEEP_GRACE_MS
|
|
231209
|
+
}
|
|
231210
|
+
});
|
|
231211
|
+
return false;
|
|
231212
|
+
}
|
|
231213
|
+
this.sleepStateChangedAt = Date.now();
|
|
231214
|
+
}
|
|
231186
231215
|
this.state.battery.sleeping = next;
|
|
231187
231216
|
this.ctx.logger.info("battery sleep state committed", {
|
|
231188
231217
|
tags: { deviceId: this.id },
|
|
@@ -231322,13 +231351,42 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
231322
231351
|
});
|
|
231323
231352
|
}
|
|
231324
231353
|
/**
|
|
231354
|
+
* Hub-fed battery refresh for an adopted child. The parent's
|
|
231355
|
+
* `getAllChannelsBatteryInfo` CGI carries per-channel percent/charge
|
|
231356
|
+
* state over the hub's own mains-powered socket, so it costs the
|
|
231357
|
+
* battery channel nothing — this is how a hub child's
|
|
231358
|
+
* percentage/charging ever moves (a sleeping child answers no poll of
|
|
231359
|
+
* its own, and its pushes only arrive on firmware transitions).
|
|
231360
|
+
*
|
|
231361
|
+
* Deliberately does NOT stamp `lastContactAt`: the value is the HUB's
|
|
231362
|
+
* cached read of the channel, not proof the camera itself answered —
|
|
231363
|
+
* folding it into contact would mask a genuinely gone camera from the
|
|
231364
|
+
* unreachable derivation (that is what pushes and streams are for).
|
|
231365
|
+
* `sleeping` is likewise untouched — sleep state has its own writers
|
|
231366
|
+
* (pushes + the guarded hub-summary reconcile).
|
|
231367
|
+
*/
|
|
231368
|
+
applyHubBatteryInfo(info) {
|
|
231369
|
+
if (!this.isBattery) return;
|
|
231370
|
+
const mapped = this.mapBatteryInfo({
|
|
231371
|
+
...info.batteryPercent !== void 0 ? { batteryPercent: info.batteryPercent } : {},
|
|
231372
|
+
...info.chargeStatus !== void 0 ? { chargeStatus: info.chargeStatus } : {},
|
|
231373
|
+
...info.adapterStatus !== void 0 ? { adapterStatus: info.adapterStatus } : {}
|
|
231374
|
+
});
|
|
231375
|
+
this.setCapSlice(batteryCapability, {
|
|
231376
|
+
...this.state.battery,
|
|
231377
|
+
percentage: mapped.percentage,
|
|
231378
|
+
charging: mapped.charging,
|
|
231379
|
+
lastUpdated: Date.now()
|
|
231380
|
+
});
|
|
231381
|
+
}
|
|
231382
|
+
/**
|
|
231325
231383
|
* Battery cams require an explicit wake before cmd_id 109 will
|
|
231326
231384
|
* answer reliably. The lib documents this pattern in
|
|
231327
|
-
* `predownloadRecordingMp4` (`ensureAwake`
|
|
231328
|
-
* operation → retry with longer wait on failure).
|
|
231329
|
-
* itself only re-`login()`s, which over BCUDP can
|
|
231330
|
-
* the camera firmware is fully up — the snapshot then
|
|
231331
|
-
* because the video subsystem isn't ready yet.
|
|
231385
|
+
* `predownloadRecordingMp3`/`predownloadRecordingMp4` (`ensureAwake`
|
|
231386
|
+
* → `wakeUp` → operation → retry with longer wait on failure).
|
|
231387
|
+
* `getSnapshot` itself only re-`login()`s, which over BCUDP can
|
|
231388
|
+
* complete before the camera firmware is fully up — the snapshot then
|
|
231389
|
+
* times out because the video subsystem isn't ready yet.
|
|
231332
231390
|
*
|
|
231333
231391
|
* Probe live testing (scripts/probe-reolink-snapshot.mts) confirmed:
|
|
231334
231392
|
* awake state: getSnapshot in ~1s
|
|
@@ -236782,6 +236840,12 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
236782
236840
|
* keeps the slice + map in sync after every change).
|
|
236783
236841
|
*/
|
|
236784
236842
|
channelToDeviceId = /* @__PURE__ */ new Map();
|
|
236843
|
+
/** One-shot: the raw per-channel battery payload is logged once per
|
|
236844
|
+
* process so the firmware's charge-field naming is on record. */
|
|
236845
|
+
batteryShapeLogged = false;
|
|
236846
|
+
/** Channels the last successful discovery refresh listed as present but
|
|
236847
|
+
* NOT adopted — their pushes are expected drops (debug), not faults. */
|
|
236848
|
+
knownUnadoptedChannels = /* @__PURE__ */ new Set();
|
|
236785
236849
|
/** Wall-clock ms of the last discovery refresh that included this
|
|
236786
236850
|
* child's channel. Drives the offline marker — a child that hasn't
|
|
236787
236851
|
* appeared in the firmware's channel list for `MISSING_OFFLINE_MS`
|
|
@@ -237195,6 +237259,11 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
237195
237259
|
};
|
|
237196
237260
|
this.runtimeState.setCapState(deviceDiscoveryCapability.name, slice);
|
|
237197
237261
|
if (lastError === null) {
|
|
237262
|
+
this.knownUnadoptedChannels.clear();
|
|
237263
|
+
for (const d of discovered) {
|
|
237264
|
+
const ch = d.metadata.rtspChannel;
|
|
237265
|
+
if (!d.alreadyAdopted && typeof ch === "number") this.knownUnadoptedChannels.add(ch);
|
|
237266
|
+
}
|
|
237198
237267
|
const adoptedCount = discovered.filter((d) => d.alreadyAdopted).length;
|
|
237199
237268
|
const availableCount = discovered.length - adoptedCount;
|
|
237200
237269
|
const onlineCount = discovered.filter((d) => d.status === "online").length;
|
|
@@ -237218,6 +237287,49 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
237218
237287
|
if (lastError === null) {
|
|
237219
237288
|
await this.reconcileAdoptedChildOnline(discovered);
|
|
237220
237289
|
await this.reconcileAdoptedChildSleepState(discovered);
|
|
237290
|
+
await this.reconcileAdoptedChildBattery();
|
|
237291
|
+
}
|
|
237292
|
+
}
|
|
237293
|
+
/**
|
|
237294
|
+
* Push the hub's per-channel battery read into each adopted battery
|
|
237295
|
+
* child. `getAllChannelsBatteryInfo` is one CGI on the hub's own
|
|
237296
|
+
* mains-powered socket — it never touches a battery camera — and it is
|
|
237297
|
+
* the ONLY way a sleeping hub child's percentage/charging ever moves:
|
|
237298
|
+
* the child answers no poll of its own while asleep, and its battery
|
|
237299
|
+
* pushes arrive only on firmware transitions (measured live: 3628 sat
|
|
237300
|
+
* at 80% / charging:none for 3 h while plugged in, 2026-08-16).
|
|
237301
|
+
*
|
|
237302
|
+
* Independent of the discovery call's success path: a battery-read
|
|
237303
|
+
* failure must not fail discovery, so it is caught and logged here.
|
|
237304
|
+
*/
|
|
237305
|
+
async reconcileAdoptedChildBattery() {
|
|
237306
|
+
if (this.channelToDeviceId.size === 0) return;
|
|
237307
|
+
try {
|
|
237308
|
+
const data = await (await this.ensureApi()).getAllChannelsBatteryInfo();
|
|
237309
|
+
const entries = Object.entries(data.batteryInfoData);
|
|
237310
|
+
if (entries.length === 0) return;
|
|
237311
|
+
if (!this.batteryShapeLogged) {
|
|
237312
|
+
this.batteryShapeLogged = true;
|
|
237313
|
+
this.ctx.logger.info("Reolink Hub: per-channel battery payload (first read)", { meta: { raw: entries.map(([ch, v]) => ({
|
|
237314
|
+
channel: Number(ch),
|
|
237315
|
+
...v
|
|
237316
|
+
})) } });
|
|
237317
|
+
}
|
|
237318
|
+
const all = await this.ctx.devices.getAll();
|
|
237319
|
+
for (const [chKey, info] of entries) {
|
|
237320
|
+
const deviceId = this.channelToDeviceId.get(Number(chKey));
|
|
237321
|
+
if (deviceId === void 0) continue;
|
|
237322
|
+
const child = all.find((d) => d.id === deviceId);
|
|
237323
|
+
if (!(child instanceof ReolinkCamera)) continue;
|
|
237324
|
+
const cgi = info.entries[0];
|
|
237325
|
+
child.applyHubBatteryInfo({
|
|
237326
|
+
batteryPercent: info.batteryLevel,
|
|
237327
|
+
...mapHubChargeStatus(cgi?.["chargeStatus"]) !== void 0 ? { chargeStatus: mapHubChargeStatus(cgi?.["chargeStatus"]) } : {},
|
|
237328
|
+
...mapHubAdapterStatus(cgi?.["adapterStatus"]) !== void 0 ? { adapterStatus: mapHubAdapterStatus(cgi?.["adapterStatus"]) } : {}
|
|
237329
|
+
});
|
|
237330
|
+
}
|
|
237331
|
+
} catch (err) {
|
|
237332
|
+
this.ctx.logger.debug("Reolink Hub: per-channel battery refresh failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
237221
237333
|
}
|
|
237222
237334
|
}
|
|
237223
237335
|
/**
|
|
@@ -237427,9 +237539,11 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
237427
237539
|
const now = Date.now();
|
|
237428
237540
|
if (now - last > 6e4) {
|
|
237429
237541
|
this.unroutedChannelLoggedAt.set(channel, now);
|
|
237430
|
-
this.
|
|
237542
|
+
const knownUnadopted = this.knownUnadoptedChannels.has(channel);
|
|
237543
|
+
this.ctx.logger[knownUnadopted ? "debug" : "warn"]("Reolink Hub: simpleEvent for an unmapped channel dropped", { meta: {
|
|
237431
237544
|
channel,
|
|
237432
|
-
type: ev?.type ?? "unknown"
|
|
237545
|
+
type: ev?.type ?? "unknown",
|
|
237546
|
+
knownUnadopted
|
|
237433
237547
|
} });
|
|
237434
237548
|
}
|
|
237435
237549
|
return;
|
|
@@ -237573,6 +237687,27 @@ function discoveredStatusFor(d) {
|
|
|
237573
237687
|
if (typeof d.state === "string" && d.state.length > 0) return "online";
|
|
237574
237688
|
return "unknown";
|
|
237575
237689
|
}
|
|
237690
|
+
/**
|
|
237691
|
+
* The hub CGI reports charge state as NUMERIC codes (measured live on the
|
|
237692
|
+
* Home Hub, 2026-08-16: a full, plugged Argus 3E reads chargeStatus 2 /
|
|
237693
|
+
* adapterStatus 1) while `mapBatteryInfo` speaks the standalone Baichuan
|
|
237694
|
+
* string vocabulary ('charging' / 'chargeComplete' / 'solarPanel'). Translate
|
|
237695
|
+
* at the hub boundary so the shared mapper stays untouched.
|
|
237696
|
+
* chargeStatus: 0 none · 1 charging · 2 charge complete
|
|
237697
|
+
* adapterStatus: 0 none · 1 DC adapter · 2 solar panel
|
|
237698
|
+
*/
|
|
237699
|
+
function mapHubChargeStatus(value) {
|
|
237700
|
+
if (typeof value === "string") return value;
|
|
237701
|
+
if (value === 0) return "none";
|
|
237702
|
+
if (value === 1) return "charging";
|
|
237703
|
+
if (value === 2) return "chargeComplete";
|
|
237704
|
+
}
|
|
237705
|
+
function mapHubAdapterStatus(value) {
|
|
237706
|
+
if (typeof value === "string") return value;
|
|
237707
|
+
if (value === 0) return "none";
|
|
237708
|
+
if (value === 1) return "dc";
|
|
237709
|
+
if (value === 2) return "solarPanel";
|
|
237710
|
+
}
|
|
237576
237711
|
//#endregion
|
|
237577
237712
|
//#region src/creation-schema.ts
|
|
237578
237713
|
/**
|
package/dist/addon.mjs
CHANGED
|
@@ -230175,6 +230175,15 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
230175
230175
|
static WAKE_HYSTERESIS_MS = 3e4;
|
|
230176
230176
|
static SLEEP_HYSTERESIS_MS = 5e3;
|
|
230177
230177
|
/**
|
|
230178
|
+
* How long a freshly committed state is protected from a hub-summary
|
|
230179
|
+
* awake→sleeping flip. The summary's per-channel sleep flag lags real
|
|
230180
|
+
* pushes by over a minute (measured >75 s on 2026-08-16); 3 min covers
|
|
230181
|
+
* two full discovery-refresh cycles of lag without letting a genuinely
|
|
230182
|
+
* stuck "awake" slice stand forever — the reconcile still wins after
|
|
230183
|
+
* the grace, which is exactly its recovery role.
|
|
230184
|
+
*/
|
|
230185
|
+
static HUB_SUMMARY_SLEEP_GRACE_MS = 18e4;
|
|
230186
|
+
/**
|
|
230178
230187
|
* Wall-clock ms of the last PROACTIVE wake — one we issued on our own
|
|
230179
230188
|
* initiative (background snapshot refresh), not because the operator
|
|
230180
230189
|
* or a stream consumer asked for the camera. Drives
|
|
@@ -231154,15 +231163,35 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
231154
231163
|
*
|
|
231155
231164
|
* @param source - `'hub-summary'` is AUTHORITATIVE (the NVR reports
|
|
231156
231165
|
* per-channel sleep state from its own firmware, not from socket
|
|
231157
|
-
* I/O inference) and therefore bypasses the hysteresis
|
|
231158
|
-
*
|
|
231166
|
+
* I/O inference) and therefore bypasses the hysteresis — with ONE
|
|
231167
|
+
* exception: the summary LAGS real push state (measured >75 s behind
|
|
231168
|
+
* an `awake` push, 2026-08-16), so an awake→sleeping flip from the
|
|
231169
|
+
* summary is refused while a fresher committed state is still inside
|
|
231170
|
+
* `HUB_SUMMARY_SLEEP_GRACE_MS`. Without the guard the 60 s reconcile
|
|
231171
|
+
* clobbered the fresh awake right back to sleeping for a full cycle —
|
|
231172
|
+
* the viewer flashed the live frame, then the sleeping overlay, then
|
|
231173
|
+
* live again. Sleeping→awake from the summary is always honoured
|
|
231174
|
+
* (it strands nothing; the camera wakes regardless).
|
|
231159
231175
|
* @returns `true` when the slice actually changed.
|
|
231160
231176
|
*/
|
|
231161
231177
|
commitSleepState(next, source) {
|
|
231162
231178
|
if (!this.isBattery) return false;
|
|
231163
231179
|
if (this.sleeping === next) return false;
|
|
231164
231180
|
if (source !== "hub-summary" && !this.acceptSleepingTransition(next)) return false;
|
|
231165
|
-
if (source === "hub-summary")
|
|
231181
|
+
if (source === "hub-summary") {
|
|
231182
|
+
const sinceCommitMs = Date.now() - this.sleepStateChangedAt;
|
|
231183
|
+
if (next === true && this.sleepStateChangedAt > 0 && sinceCommitMs < ReolinkCamera.HUB_SUMMARY_SLEEP_GRACE_MS) {
|
|
231184
|
+
this.ctx.logger.debug("hub-summary sleep flip refused — fresher state inside grace", {
|
|
231185
|
+
tags: { deviceId: this.id },
|
|
231186
|
+
meta: {
|
|
231187
|
+
sinceCommitMs,
|
|
231188
|
+
graceMs: ReolinkCamera.HUB_SUMMARY_SLEEP_GRACE_MS
|
|
231189
|
+
}
|
|
231190
|
+
});
|
|
231191
|
+
return false;
|
|
231192
|
+
}
|
|
231193
|
+
this.sleepStateChangedAt = Date.now();
|
|
231194
|
+
}
|
|
231166
231195
|
this.state.battery.sleeping = next;
|
|
231167
231196
|
this.ctx.logger.info("battery sleep state committed", {
|
|
231168
231197
|
tags: { deviceId: this.id },
|
|
@@ -231302,13 +231331,42 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
231302
231331
|
});
|
|
231303
231332
|
}
|
|
231304
231333
|
/**
|
|
231334
|
+
* Hub-fed battery refresh for an adopted child. The parent's
|
|
231335
|
+
* `getAllChannelsBatteryInfo` CGI carries per-channel percent/charge
|
|
231336
|
+
* state over the hub's own mains-powered socket, so it costs the
|
|
231337
|
+
* battery channel nothing — this is how a hub child's
|
|
231338
|
+
* percentage/charging ever moves (a sleeping child answers no poll of
|
|
231339
|
+
* its own, and its pushes only arrive on firmware transitions).
|
|
231340
|
+
*
|
|
231341
|
+
* Deliberately does NOT stamp `lastContactAt`: the value is the HUB's
|
|
231342
|
+
* cached read of the channel, not proof the camera itself answered —
|
|
231343
|
+
* folding it into contact would mask a genuinely gone camera from the
|
|
231344
|
+
* unreachable derivation (that is what pushes and streams are for).
|
|
231345
|
+
* `sleeping` is likewise untouched — sleep state has its own writers
|
|
231346
|
+
* (pushes + the guarded hub-summary reconcile).
|
|
231347
|
+
*/
|
|
231348
|
+
applyHubBatteryInfo(info) {
|
|
231349
|
+
if (!this.isBattery) return;
|
|
231350
|
+
const mapped = this.mapBatteryInfo({
|
|
231351
|
+
...info.batteryPercent !== void 0 ? { batteryPercent: info.batteryPercent } : {},
|
|
231352
|
+
...info.chargeStatus !== void 0 ? { chargeStatus: info.chargeStatus } : {},
|
|
231353
|
+
...info.adapterStatus !== void 0 ? { adapterStatus: info.adapterStatus } : {}
|
|
231354
|
+
});
|
|
231355
|
+
this.setCapSlice(batteryCapability, {
|
|
231356
|
+
...this.state.battery,
|
|
231357
|
+
percentage: mapped.percentage,
|
|
231358
|
+
charging: mapped.charging,
|
|
231359
|
+
lastUpdated: Date.now()
|
|
231360
|
+
});
|
|
231361
|
+
}
|
|
231362
|
+
/**
|
|
231305
231363
|
* Battery cams require an explicit wake before cmd_id 109 will
|
|
231306
231364
|
* answer reliably. The lib documents this pattern in
|
|
231307
|
-
* `predownloadRecordingMp4` (`ensureAwake`
|
|
231308
|
-
* operation → retry with longer wait on failure).
|
|
231309
|
-
* itself only re-`login()`s, which over BCUDP can
|
|
231310
|
-
* the camera firmware is fully up — the snapshot then
|
|
231311
|
-
* because the video subsystem isn't ready yet.
|
|
231365
|
+
* `predownloadRecordingMp3`/`predownloadRecordingMp4` (`ensureAwake`
|
|
231366
|
+
* → `wakeUp` → operation → retry with longer wait on failure).
|
|
231367
|
+
* `getSnapshot` itself only re-`login()`s, which over BCUDP can
|
|
231368
|
+
* complete before the camera firmware is fully up — the snapshot then
|
|
231369
|
+
* times out because the video subsystem isn't ready yet.
|
|
231312
231370
|
*
|
|
231313
231371
|
* Probe live testing (scripts/probe-reolink-snapshot.mts) confirmed:
|
|
231314
231372
|
* awake state: getSnapshot in ~1s
|
|
@@ -236762,6 +236820,12 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
236762
236820
|
* keeps the slice + map in sync after every change).
|
|
236763
236821
|
*/
|
|
236764
236822
|
channelToDeviceId = /* @__PURE__ */ new Map();
|
|
236823
|
+
/** One-shot: the raw per-channel battery payload is logged once per
|
|
236824
|
+
* process so the firmware's charge-field naming is on record. */
|
|
236825
|
+
batteryShapeLogged = false;
|
|
236826
|
+
/** Channels the last successful discovery refresh listed as present but
|
|
236827
|
+
* NOT adopted — their pushes are expected drops (debug), not faults. */
|
|
236828
|
+
knownUnadoptedChannels = /* @__PURE__ */ new Set();
|
|
236765
236829
|
/** Wall-clock ms of the last discovery refresh that included this
|
|
236766
236830
|
* child's channel. Drives the offline marker — a child that hasn't
|
|
236767
236831
|
* appeared in the firmware's channel list for `MISSING_OFFLINE_MS`
|
|
@@ -237175,6 +237239,11 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
237175
237239
|
};
|
|
237176
237240
|
this.runtimeState.setCapState(deviceDiscoveryCapability.name, slice);
|
|
237177
237241
|
if (lastError === null) {
|
|
237242
|
+
this.knownUnadoptedChannels.clear();
|
|
237243
|
+
for (const d of discovered) {
|
|
237244
|
+
const ch = d.metadata.rtspChannel;
|
|
237245
|
+
if (!d.alreadyAdopted && typeof ch === "number") this.knownUnadoptedChannels.add(ch);
|
|
237246
|
+
}
|
|
237178
237247
|
const adoptedCount = discovered.filter((d) => d.alreadyAdopted).length;
|
|
237179
237248
|
const availableCount = discovered.length - adoptedCount;
|
|
237180
237249
|
const onlineCount = discovered.filter((d) => d.status === "online").length;
|
|
@@ -237198,6 +237267,49 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
237198
237267
|
if (lastError === null) {
|
|
237199
237268
|
await this.reconcileAdoptedChildOnline(discovered);
|
|
237200
237269
|
await this.reconcileAdoptedChildSleepState(discovered);
|
|
237270
|
+
await this.reconcileAdoptedChildBattery();
|
|
237271
|
+
}
|
|
237272
|
+
}
|
|
237273
|
+
/**
|
|
237274
|
+
* Push the hub's per-channel battery read into each adopted battery
|
|
237275
|
+
* child. `getAllChannelsBatteryInfo` is one CGI on the hub's own
|
|
237276
|
+
* mains-powered socket — it never touches a battery camera — and it is
|
|
237277
|
+
* the ONLY way a sleeping hub child's percentage/charging ever moves:
|
|
237278
|
+
* the child answers no poll of its own while asleep, and its battery
|
|
237279
|
+
* pushes arrive only on firmware transitions (measured live: 3628 sat
|
|
237280
|
+
* at 80% / charging:none for 3 h while plugged in, 2026-08-16).
|
|
237281
|
+
*
|
|
237282
|
+
* Independent of the discovery call's success path: a battery-read
|
|
237283
|
+
* failure must not fail discovery, so it is caught and logged here.
|
|
237284
|
+
*/
|
|
237285
|
+
async reconcileAdoptedChildBattery() {
|
|
237286
|
+
if (this.channelToDeviceId.size === 0) return;
|
|
237287
|
+
try {
|
|
237288
|
+
const data = await (await this.ensureApi()).getAllChannelsBatteryInfo();
|
|
237289
|
+
const entries = Object.entries(data.batteryInfoData);
|
|
237290
|
+
if (entries.length === 0) return;
|
|
237291
|
+
if (!this.batteryShapeLogged) {
|
|
237292
|
+
this.batteryShapeLogged = true;
|
|
237293
|
+
this.ctx.logger.info("Reolink Hub: per-channel battery payload (first read)", { meta: { raw: entries.map(([ch, v]) => ({
|
|
237294
|
+
channel: Number(ch),
|
|
237295
|
+
...v
|
|
237296
|
+
})) } });
|
|
237297
|
+
}
|
|
237298
|
+
const all = await this.ctx.devices.getAll();
|
|
237299
|
+
for (const [chKey, info] of entries) {
|
|
237300
|
+
const deviceId = this.channelToDeviceId.get(Number(chKey));
|
|
237301
|
+
if (deviceId === void 0) continue;
|
|
237302
|
+
const child = all.find((d) => d.id === deviceId);
|
|
237303
|
+
if (!(child instanceof ReolinkCamera)) continue;
|
|
237304
|
+
const cgi = info.entries[0];
|
|
237305
|
+
child.applyHubBatteryInfo({
|
|
237306
|
+
batteryPercent: info.batteryLevel,
|
|
237307
|
+
...mapHubChargeStatus(cgi?.["chargeStatus"]) !== void 0 ? { chargeStatus: mapHubChargeStatus(cgi?.["chargeStatus"]) } : {},
|
|
237308
|
+
...mapHubAdapterStatus(cgi?.["adapterStatus"]) !== void 0 ? { adapterStatus: mapHubAdapterStatus(cgi?.["adapterStatus"]) } : {}
|
|
237309
|
+
});
|
|
237310
|
+
}
|
|
237311
|
+
} catch (err) {
|
|
237312
|
+
this.ctx.logger.debug("Reolink Hub: per-channel battery refresh failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
237201
237313
|
}
|
|
237202
237314
|
}
|
|
237203
237315
|
/**
|
|
@@ -237407,9 +237519,11 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
237407
237519
|
const now = Date.now();
|
|
237408
237520
|
if (now - last > 6e4) {
|
|
237409
237521
|
this.unroutedChannelLoggedAt.set(channel, now);
|
|
237410
|
-
this.
|
|
237522
|
+
const knownUnadopted = this.knownUnadoptedChannels.has(channel);
|
|
237523
|
+
this.ctx.logger[knownUnadopted ? "debug" : "warn"]("Reolink Hub: simpleEvent for an unmapped channel dropped", { meta: {
|
|
237411
237524
|
channel,
|
|
237412
|
-
type: ev?.type ?? "unknown"
|
|
237525
|
+
type: ev?.type ?? "unknown",
|
|
237526
|
+
knownUnadopted
|
|
237413
237527
|
} });
|
|
237414
237528
|
}
|
|
237415
237529
|
return;
|
|
@@ -237553,6 +237667,27 @@ function discoveredStatusFor(d) {
|
|
|
237553
237667
|
if (typeof d.state === "string" && d.state.length > 0) return "online";
|
|
237554
237668
|
return "unknown";
|
|
237555
237669
|
}
|
|
237670
|
+
/**
|
|
237671
|
+
* The hub CGI reports charge state as NUMERIC codes (measured live on the
|
|
237672
|
+
* Home Hub, 2026-08-16: a full, plugged Argus 3E reads chargeStatus 2 /
|
|
237673
|
+
* adapterStatus 1) while `mapBatteryInfo` speaks the standalone Baichuan
|
|
237674
|
+
* string vocabulary ('charging' / 'chargeComplete' / 'solarPanel'). Translate
|
|
237675
|
+
* at the hub boundary so the shared mapper stays untouched.
|
|
237676
|
+
* chargeStatus: 0 none · 1 charging · 2 charge complete
|
|
237677
|
+
* adapterStatus: 0 none · 1 DC adapter · 2 solar panel
|
|
237678
|
+
*/
|
|
237679
|
+
function mapHubChargeStatus(value) {
|
|
237680
|
+
if (typeof value === "string") return value;
|
|
237681
|
+
if (value === 0) return "none";
|
|
237682
|
+
if (value === 1) return "charging";
|
|
237683
|
+
if (value === 2) return "chargeComplete";
|
|
237684
|
+
}
|
|
237685
|
+
function mapHubAdapterStatus(value) {
|
|
237686
|
+
if (typeof value === "string") return value;
|
|
237687
|
+
if (value === 0) return "none";
|
|
237688
|
+
if (value === 1) return "dc";
|
|
237689
|
+
if (value === 2) return "solarPanel";
|
|
237690
|
+
}
|
|
237556
237691
|
//#endregion
|
|
237557
237692
|
//#region src/creation-schema.ts
|
|
237558
237693
|
/**
|