@camstack/addon-provider-reolink 1.2.38 → 1.2.40
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 +132 -11
- package/dist/addon.mjs +132 -11
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -19789,7 +19789,14 @@ targets: array(object({
|
|
|
19789
19789
|
"sleeping",
|
|
19790
19790
|
"unreachable",
|
|
19791
19791
|
"waking"
|
|
19792
|
-
]).nullable()
|
|
19792
|
+
]).nullable(),
|
|
19793
|
+
/** A battery camera (whatever its current state). An AWAKE battery
|
|
19794
|
+
* camera is deliberately NOT recaptured on the poll cadence — every
|
|
19795
|
+
* capture is a camera hit that would keep it out of sleep — so its
|
|
19796
|
+
* cached frame legitimately ages past the currency ceiling while
|
|
19797
|
+
* nothing is streaming. A surface must keep painting it (a fresh
|
|
19798
|
+
* frame is captured at each wake), not blank to "unavailable". */
|
|
19799
|
+
battery: boolean()
|
|
19793
19800
|
})))
|
|
19794
19801
|
},
|
|
19795
19802
|
status: {
|
|
@@ -230188,6 +230195,15 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
230188
230195
|
static WAKE_HYSTERESIS_MS = 3e4;
|
|
230189
230196
|
static SLEEP_HYSTERESIS_MS = 5e3;
|
|
230190
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
|
+
/**
|
|
230191
230207
|
* Wall-clock ms of the last PROACTIVE wake — one we issued on our own
|
|
230192
230208
|
* initiative (background snapshot refresh), not because the operator
|
|
230193
230209
|
* or a stream consumer asked for the camera. Drives
|
|
@@ -231167,15 +231183,35 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
231167
231183
|
*
|
|
231168
231184
|
* @param source - `'hub-summary'` is AUTHORITATIVE (the NVR reports
|
|
231169
231185
|
* per-channel sleep state from its own firmware, not from socket
|
|
231170
|
-
* I/O inference) and therefore bypasses the hysteresis
|
|
231171
|
-
*
|
|
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).
|
|
231172
231195
|
* @returns `true` when the slice actually changed.
|
|
231173
231196
|
*/
|
|
231174
231197
|
commitSleepState(next, source) {
|
|
231175
231198
|
if (!this.isBattery) return false;
|
|
231176
231199
|
if (this.sleeping === next) return false;
|
|
231177
231200
|
if (source !== "hub-summary" && !this.acceptSleepingTransition(next)) return false;
|
|
231178
|
-
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
|
+
}
|
|
231179
231215
|
this.state.battery.sleeping = next;
|
|
231180
231216
|
this.ctx.logger.info("battery sleep state committed", {
|
|
231181
231217
|
tags: { deviceId: this.id },
|
|
@@ -231315,13 +231351,42 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
231315
231351
|
});
|
|
231316
231352
|
}
|
|
231317
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
|
+
/**
|
|
231318
231383
|
* Battery cams require an explicit wake before cmd_id 109 will
|
|
231319
231384
|
* answer reliably. The lib documents this pattern in
|
|
231320
|
-
* `predownloadRecordingMp4` (`ensureAwake`
|
|
231321
|
-
* operation → retry with longer wait on failure).
|
|
231322
|
-
* itself only re-`login()`s, which over BCUDP can
|
|
231323
|
-
* the camera firmware is fully up — the snapshot then
|
|
231324
|
-
* 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.
|
|
231325
231390
|
*
|
|
231326
231391
|
* Probe live testing (scripts/probe-reolink-snapshot.mts) confirmed:
|
|
231327
231392
|
* awake state: getSnapshot in ~1s
|
|
@@ -236775,6 +236840,12 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
236775
236840
|
* keeps the slice + map in sync after every change).
|
|
236776
236841
|
*/
|
|
236777
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();
|
|
236778
236849
|
/** Wall-clock ms of the last discovery refresh that included this
|
|
236779
236850
|
* child's channel. Drives the offline marker — a child that hasn't
|
|
236780
236851
|
* appeared in the firmware's channel list for `MISSING_OFFLINE_MS`
|
|
@@ -237188,6 +237259,11 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
237188
237259
|
};
|
|
237189
237260
|
this.runtimeState.setCapState(deviceDiscoveryCapability.name, slice);
|
|
237190
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
|
+
}
|
|
237191
237267
|
const adoptedCount = discovered.filter((d) => d.alreadyAdopted).length;
|
|
237192
237268
|
const availableCount = discovered.length - adoptedCount;
|
|
237193
237269
|
const onlineCount = discovered.filter((d) => d.status === "online").length;
|
|
@@ -237211,6 +237287,49 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
237211
237287
|
if (lastError === null) {
|
|
237212
237288
|
await this.reconcileAdoptedChildOnline(discovered);
|
|
237213
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
|
+
...typeof cgi?.["chargeStatus"] === "string" ? { chargeStatus: cgi["chargeStatus"] } : {},
|
|
237328
|
+
...typeof cgi?.["adapterStatus"] === "string" ? { adapterStatus: 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) } });
|
|
237214
237333
|
}
|
|
237215
237334
|
}
|
|
237216
237335
|
/**
|
|
@@ -237420,9 +237539,11 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
237420
237539
|
const now = Date.now();
|
|
237421
237540
|
if (now - last > 6e4) {
|
|
237422
237541
|
this.unroutedChannelLoggedAt.set(channel, now);
|
|
237423
|
-
this.
|
|
237542
|
+
const knownUnadopted = this.knownUnadoptedChannels.has(channel);
|
|
237543
|
+
this.ctx.logger[knownUnadopted ? "debug" : "warn"]("Reolink Hub: simpleEvent for an unmapped channel dropped", { meta: {
|
|
237424
237544
|
channel,
|
|
237425
|
-
type: ev?.type ?? "unknown"
|
|
237545
|
+
type: ev?.type ?? "unknown",
|
|
237546
|
+
knownUnadopted
|
|
237426
237547
|
} });
|
|
237427
237548
|
}
|
|
237428
237549
|
return;
|
package/dist/addon.mjs
CHANGED
|
@@ -19784,7 +19784,14 @@ targets: array(object({
|
|
|
19784
19784
|
"sleeping",
|
|
19785
19785
|
"unreachable",
|
|
19786
19786
|
"waking"
|
|
19787
|
-
]).nullable()
|
|
19787
|
+
]).nullable(),
|
|
19788
|
+
/** A battery camera (whatever its current state). An AWAKE battery
|
|
19789
|
+
* camera is deliberately NOT recaptured on the poll cadence — every
|
|
19790
|
+
* capture is a camera hit that would keep it out of sleep — so its
|
|
19791
|
+
* cached frame legitimately ages past the currency ceiling while
|
|
19792
|
+
* nothing is streaming. A surface must keep painting it (a fresh
|
|
19793
|
+
* frame is captured at each wake), not blank to "unavailable". */
|
|
19794
|
+
battery: boolean()
|
|
19788
19795
|
})))
|
|
19789
19796
|
},
|
|
19790
19797
|
status: {
|
|
@@ -230168,6 +230175,15 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
230168
230175
|
static WAKE_HYSTERESIS_MS = 3e4;
|
|
230169
230176
|
static SLEEP_HYSTERESIS_MS = 5e3;
|
|
230170
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
|
+
/**
|
|
230171
230187
|
* Wall-clock ms of the last PROACTIVE wake — one we issued on our own
|
|
230172
230188
|
* initiative (background snapshot refresh), not because the operator
|
|
230173
230189
|
* or a stream consumer asked for the camera. Drives
|
|
@@ -231147,15 +231163,35 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
231147
231163
|
*
|
|
231148
231164
|
* @param source - `'hub-summary'` is AUTHORITATIVE (the NVR reports
|
|
231149
231165
|
* per-channel sleep state from its own firmware, not from socket
|
|
231150
|
-
* I/O inference) and therefore bypasses the hysteresis
|
|
231151
|
-
*
|
|
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).
|
|
231152
231175
|
* @returns `true` when the slice actually changed.
|
|
231153
231176
|
*/
|
|
231154
231177
|
commitSleepState(next, source) {
|
|
231155
231178
|
if (!this.isBattery) return false;
|
|
231156
231179
|
if (this.sleeping === next) return false;
|
|
231157
231180
|
if (source !== "hub-summary" && !this.acceptSleepingTransition(next)) return false;
|
|
231158
|
-
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
|
+
}
|
|
231159
231195
|
this.state.battery.sleeping = next;
|
|
231160
231196
|
this.ctx.logger.info("battery sleep state committed", {
|
|
231161
231197
|
tags: { deviceId: this.id },
|
|
@@ -231295,13 +231331,42 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
231295
231331
|
});
|
|
231296
231332
|
}
|
|
231297
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
|
+
/**
|
|
231298
231363
|
* Battery cams require an explicit wake before cmd_id 109 will
|
|
231299
231364
|
* answer reliably. The lib documents this pattern in
|
|
231300
|
-
* `predownloadRecordingMp4` (`ensureAwake`
|
|
231301
|
-
* operation → retry with longer wait on failure).
|
|
231302
|
-
* itself only re-`login()`s, which over BCUDP can
|
|
231303
|
-
* the camera firmware is fully up — the snapshot then
|
|
231304
|
-
* 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.
|
|
231305
231370
|
*
|
|
231306
231371
|
* Probe live testing (scripts/probe-reolink-snapshot.mts) confirmed:
|
|
231307
231372
|
* awake state: getSnapshot in ~1s
|
|
@@ -236755,6 +236820,12 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
236755
236820
|
* keeps the slice + map in sync after every change).
|
|
236756
236821
|
*/
|
|
236757
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();
|
|
236758
236829
|
/** Wall-clock ms of the last discovery refresh that included this
|
|
236759
236830
|
* child's channel. Drives the offline marker — a child that hasn't
|
|
236760
236831
|
* appeared in the firmware's channel list for `MISSING_OFFLINE_MS`
|
|
@@ -237168,6 +237239,11 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
237168
237239
|
};
|
|
237169
237240
|
this.runtimeState.setCapState(deviceDiscoveryCapability.name, slice);
|
|
237170
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
|
+
}
|
|
237171
237247
|
const adoptedCount = discovered.filter((d) => d.alreadyAdopted).length;
|
|
237172
237248
|
const availableCount = discovered.length - adoptedCount;
|
|
237173
237249
|
const onlineCount = discovered.filter((d) => d.status === "online").length;
|
|
@@ -237191,6 +237267,49 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
237191
237267
|
if (lastError === null) {
|
|
237192
237268
|
await this.reconcileAdoptedChildOnline(discovered);
|
|
237193
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
|
+
...typeof cgi?.["chargeStatus"] === "string" ? { chargeStatus: cgi["chargeStatus"] } : {},
|
|
237308
|
+
...typeof cgi?.["adapterStatus"] === "string" ? { adapterStatus: 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) } });
|
|
237194
237313
|
}
|
|
237195
237314
|
}
|
|
237196
237315
|
/**
|
|
@@ -237400,9 +237519,11 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
237400
237519
|
const now = Date.now();
|
|
237401
237520
|
if (now - last > 6e4) {
|
|
237402
237521
|
this.unroutedChannelLoggedAt.set(channel, now);
|
|
237403
|
-
this.
|
|
237522
|
+
const knownUnadopted = this.knownUnadoptedChannels.has(channel);
|
|
237523
|
+
this.ctx.logger[knownUnadopted ? "debug" : "warn"]("Reolink Hub: simpleEvent for an unmapped channel dropped", { meta: {
|
|
237404
237524
|
channel,
|
|
237405
|
-
type: ev?.type ?? "unknown"
|
|
237525
|
+
type: ev?.type ?? "unknown",
|
|
237526
|
+
knownUnadopted
|
|
237406
237527
|
} });
|
|
237407
237528
|
}
|
|
237408
237529
|
return;
|