@camstack/addon-provider-reolink 1.2.28 → 1.2.29
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 +211 -10
- package/dist/addon.mjs +211 -10
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -21392,6 +21392,25 @@ var BatteryStatusSchema = object({
|
|
|
21392
21392
|
/** Ms epoch of the last observation. Lets consumers reason about freshness. */
|
|
21393
21393
|
lastUpdated: number(),
|
|
21394
21394
|
/**
|
|
21395
|
+
* Ms epoch of the last time the device PROVED it was reachable — a
|
|
21396
|
+
* completed firmware round-trip, an observed wake, or an inbound push
|
|
21397
|
+
* (firmware event, email). `0`/absent = never since this slice was born.
|
|
21398
|
+
*
|
|
21399
|
+
* This is the ONLY input that separates "asleep" from "gone", and it is
|
|
21400
|
+
* fed exclusively by PASSIVE signals: nothing may write it by reaching
|
|
21401
|
+
* for the radio, because a poll that confirms reachability is the same
|
|
21402
|
+
* poll that drains the battery. See {@link deriveBatteryPresence} — the
|
|
21403
|
+
* single derivation every consumer must use; no surface computes its own.
|
|
21404
|
+
*
|
|
21405
|
+
* It is deliberately NOT a clock in the
|
|
21406
|
+
* `scripts/check-runtime-state-durability.ts` sense: it is the
|
|
21407
|
+
* observation itself, and it is the only thing a 30-hour silence is
|
|
21408
|
+
* visible in. Writers quantise it (see `CONTACT_WRITE_QUANTUM_MS` in the
|
|
21409
|
+
* Reolink provider) so a value that means "recently" cannot cost a
|
|
21410
|
+
* SQLite commit per round-trip.
|
|
21411
|
+
*/
|
|
21412
|
+
lastContactAt: number().optional(),
|
|
21413
|
+
/**
|
|
21395
21414
|
* True when the source is a BINARY low-battery indicator (HA
|
|
21396
21415
|
* `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
|
|
21397
21416
|
* charge level — `percentage` is then a coarse stand-in (100 = normal,
|
|
@@ -27427,13 +27446,63 @@ var CamStreamDescriptorSchema = object({
|
|
|
27427
27446
|
* set of stream descriptors it can offer for the device, synchronously, so the
|
|
27428
27447
|
* broker can reconcile its registry against the authoritative provider state.
|
|
27429
27448
|
*/
|
|
27449
|
+
/**
|
|
27450
|
+
* The catalog as a DURABLE fact rather than a live answer.
|
|
27451
|
+
*
|
|
27452
|
+
* A battery camera's descriptors are profile-stable — they change when the
|
|
27453
|
+
* operator rewrites an encoder profile, not minute to minute — but building
|
|
27454
|
+
* them costs a Baichuan login, which on a sleeping Argus IS a wake. So the
|
|
27455
|
+
* provider is allowed to build them exactly once per profile and must serve
|
|
27456
|
+
* every later pull from a cache.
|
|
27457
|
+
*
|
|
27458
|
+
* Holding that cache only in RAM is what turned a restart into an outage. The
|
|
27459
|
+
* runner comes back with the camera asleep, `buildStreamCatalogUncached`
|
|
27460
|
+
* correctly refuses to wake it, the pull answers `[]`, the broker has no
|
|
27461
|
+
* cam-stream entry to build a broker from, and `webrtcSession.handleOffer`
|
|
27462
|
+
* fails with a flat "No broker for stream" — for as long as the camera sleeps,
|
|
27463
|
+
* which on a battery cam is most of the day. The camera was fine. The stream
|
|
27464
|
+
* was unreachable because the process had forgotten what the camera offers.
|
|
27465
|
+
*
|
|
27466
|
+
* Declaring it here puts it in `device-runtime-state`, the kernel's canonical
|
|
27467
|
+
* declared collection, with the same `restored` durability `battery` uses for
|
|
27468
|
+
* the same reason: the last known value is the only value there is while the
|
|
27469
|
+
* device is asleep. The broker's brokers are therefore always DEFINABLE — it
|
|
27470
|
+
* is the DIAL that wakes a camera, never the catalog (D173).
|
|
27471
|
+
*/
|
|
27472
|
+
var StreamCatalogStateSchema = object({
|
|
27473
|
+
/** The descriptors as last built from a real camera response. Never a guess:
|
|
27474
|
+
* a failed or refused build writes NOTHING, so a restored catalog is always
|
|
27475
|
+
* one the camera itself once produced. */
|
|
27476
|
+
descriptors: array(CamStreamDescriptorSchema),
|
|
27477
|
+
/** Ms epoch of the build that produced {@link descriptors}. Lets the wake
|
|
27478
|
+
* path decide whether the camera's own awake window is worth spending on a
|
|
27479
|
+
* re-read. */
|
|
27480
|
+
lastFetchedAt: number()
|
|
27481
|
+
});
|
|
27430
27482
|
var streamCatalogCapability = {
|
|
27431
27483
|
name: "stream-catalog",
|
|
27432
27484
|
scope: "device",
|
|
27433
27485
|
deviceNative: true,
|
|
27434
27486
|
mode: "singleton",
|
|
27435
27487
|
deviceTypes: [DeviceType.Camera],
|
|
27436
|
-
methods: { getCatalog: method(object({ deviceId: number().int().nonnegative() }), array(CamStreamDescriptorSchema).readonly()) }
|
|
27488
|
+
methods: { getCatalog: method(object({ deviceId: number().int().nonnegative() }), array(CamStreamDescriptorSchema).readonly()) },
|
|
27489
|
+
runtimeState: StreamCatalogStateSchema,
|
|
27490
|
+
/**
|
|
27491
|
+
* Runtime-state durability: **restored** — see the schema doc. A cold
|
|
27492
|
+
* catalog on a sleeping battery camera is not a slow first frame, it is a
|
|
27493
|
+
* camera that cannot be watched at all until it happens to wake.
|
|
27494
|
+
*
|
|
27495
|
+
* Churn is nil by construction: the slice is written only by a SUCCESSFUL
|
|
27496
|
+
* build, and a build only runs when there is no cached copy (or the copy is
|
|
27497
|
+
* a day old and the camera is awake anyway).
|
|
27498
|
+
*
|
|
27499
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
27500
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
27501
|
+
*/
|
|
27502
|
+
durability: "restored",
|
|
27503
|
+
/** Clock field: written, but excluded from the compare that decides whether
|
|
27504
|
+
* persisting is worth a SQLite commit — the descriptors are the value. */
|
|
27505
|
+
volatileStateFields: ["lastFetchedAt"]
|
|
27437
27506
|
};
|
|
27438
27507
|
/** One of the camera's stream profiles. */
|
|
27439
27508
|
var StreamProfileSchema = _enum([
|
|
@@ -29297,6 +29366,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
29297
29366
|
sceneMonitor: sceneMonitorCapability,
|
|
29298
29367
|
scriptRunner: scriptRunnerCapability,
|
|
29299
29368
|
smoke: smokeCapability,
|
|
29369
|
+
streamCatalog: streamCatalogCapability,
|
|
29300
29370
|
streamParams: streamParamsCapability,
|
|
29301
29371
|
switch: switchCapability,
|
|
29302
29372
|
tamper: tamperCapability,
|
|
@@ -230171,6 +230241,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
230171
230241
|
* retries.
|
|
230172
230242
|
*/
|
|
230173
230243
|
async onProbe() {
|
|
230244
|
+
if (this.isBattery && this.sleeping) {
|
|
230245
|
+
this.ctx.logger.info("onProbe skipped — battery cam is sleeping (no login, no wake)", {
|
|
230246
|
+
tags: { deviceId: this.id },
|
|
230247
|
+
meta: { probeRetriesAvoided: true }
|
|
230248
|
+
});
|
|
230249
|
+
return;
|
|
230250
|
+
}
|
|
230174
230251
|
let api;
|
|
230175
230252
|
try {
|
|
230176
230253
|
api = await this.ensureApi();
|
|
@@ -230871,9 +230948,21 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
230871
230948
|
status: slice
|
|
230872
230949
|
}));
|
|
230873
230950
|
});
|
|
230874
|
-
this.refreshBatteryFromApi();
|
|
230951
|
+
this.refreshBatteryFromApi("register");
|
|
230875
230952
|
}
|
|
230876
|
-
|
|
230953
|
+
/**
|
|
230954
|
+
* @param reason - `'register'` and `'periodic'` are OUR initiative and are
|
|
230955
|
+
* refused while the camera sleeps; `'wake'` and `'demand'` run because
|
|
230956
|
+
* something already has the camera awake or is entitled to wake it.
|
|
230957
|
+
*/
|
|
230958
|
+
async refreshBatteryFromApi(reason) {
|
|
230959
|
+
if (this.isBattery && this.sleeping && (reason === "register" || reason === "periodic")) {
|
|
230960
|
+
this.ctx.logger.debug("battery refresh skipped — cam sleeping, reading restored slice", {
|
|
230961
|
+
tags: { deviceId: this.id },
|
|
230962
|
+
meta: { reason }
|
|
230963
|
+
});
|
|
230964
|
+
return;
|
|
230965
|
+
}
|
|
230877
230966
|
let api;
|
|
230878
230967
|
try {
|
|
230879
230968
|
api = await this.ensureApi();
|
|
@@ -231031,7 +231120,14 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
231031
231120
|
return true;
|
|
231032
231121
|
}
|
|
231033
231122
|
updateBatteryCache(info) {
|
|
231034
|
-
|
|
231123
|
+
const mapped = this.mapBatteryInfo(info);
|
|
231124
|
+
const now = Date.now();
|
|
231125
|
+
const quantised = now - now % ReolinkCamera.CONTACT_WRITE_QUANTUM_MS;
|
|
231126
|
+
const previousContact = this.state.battery.lastContactAt ?? 0;
|
|
231127
|
+
this.setCapSlice(batteryCapability, {
|
|
231128
|
+
...mapped,
|
|
231129
|
+
lastContactAt: Math.max(previousContact, quantised)
|
|
231130
|
+
});
|
|
231035
231131
|
}
|
|
231036
231132
|
/**
|
|
231037
231133
|
* Battery cams require an explicit wake before cmd_id 109 will
|
|
@@ -233356,6 +233452,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
233356
233452
|
*/
|
|
233357
233453
|
async buildStreamCatalog() {
|
|
233358
233454
|
if (this.cachedStreamDescriptors?.length) return this.withLiveNativeSdp(this.cachedStreamDescriptors);
|
|
233455
|
+
const restored = this.restoreStreamCatalogFromLedger();
|
|
233456
|
+
if (restored) return this.withLiveNativeSdp(restored);
|
|
233359
233457
|
if (this.buildStreamCatalogInFlight) return this.withLiveNativeSdp(await this.buildStreamCatalogInFlight);
|
|
233360
233458
|
const build = this.buildStreamCatalogUncached();
|
|
233361
233459
|
this.buildStreamCatalogInFlight = build;
|
|
@@ -233528,6 +233626,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
233528
233626
|
autoEligible: e.autoEligible
|
|
233529
233627
|
}));
|
|
233530
233628
|
this.cachedStreamDescriptors = descriptors;
|
|
233629
|
+
this.persistStreamCatalogToLedger(descriptors);
|
|
233531
233630
|
return descriptors;
|
|
233532
233631
|
}
|
|
233533
233632
|
/** Profile-stable stream descriptors, cached after the first successful
|
|
@@ -233535,6 +233634,66 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
233535
233634
|
* sleeping battery cam is never woken by a catalog poll. Invalidated by
|
|
233536
233635
|
* `applyStreamProfilePatch` (codec/resolution may change). */
|
|
233537
233636
|
cachedStreamDescriptors;
|
|
233637
|
+
/**
|
|
233638
|
+
* Write the just-built catalog to the durable `stream-catalog` slice, so a
|
|
233639
|
+
* restart with the camera asleep still has descriptors to serve (D173).
|
|
233640
|
+
*
|
|
233641
|
+
* Best-effort by design: the RAM copy is already advanced by the caller, and
|
|
233642
|
+
* losing this write costs one cold catalog after the next restart — never a
|
|
233643
|
+
* wrong catalog. A build that FAILED writes nothing at all and therefore
|
|
233644
|
+
* cannot demote a good stored copy (D49's failure direction).
|
|
233645
|
+
*/
|
|
233646
|
+
persistStreamCatalogToLedger(descriptors) {
|
|
233647
|
+
if (descriptors.length === 0) return;
|
|
233648
|
+
try {
|
|
233649
|
+
const state = {
|
|
233650
|
+
descriptors: [...descriptors],
|
|
233651
|
+
lastFetchedAt: Date.now()
|
|
233652
|
+
};
|
|
233653
|
+
this.runtimeState.setCapState(streamCatalogCapability.name, state);
|
|
233654
|
+
this.ctx.logger.debug("stream catalog persisted to the durable slice", {
|
|
233655
|
+
tags: { deviceId: this.id },
|
|
233656
|
+
meta: { count: descriptors.length }
|
|
233657
|
+
});
|
|
233658
|
+
} catch (err) {
|
|
233659
|
+
this.ctx.logger.debug("stream catalog persist failed — RAM copy stands", {
|
|
233660
|
+
tags: { deviceId: this.id },
|
|
233661
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
233662
|
+
});
|
|
233663
|
+
}
|
|
233664
|
+
}
|
|
233665
|
+
/**
|
|
233666
|
+
* Rehydrate `cachedStreamDescriptors` from the durable slice. Returns the
|
|
233667
|
+
* restored descriptors, or `null` when there is nothing to restore.
|
|
233668
|
+
*
|
|
233669
|
+
* Logged at `info` when it fires: "these descriptors came from before the
|
|
233670
|
+
* restart" must never be something a reader has to infer (the DurableLedger
|
|
233671
|
+
* contract, D132).
|
|
233672
|
+
*/
|
|
233673
|
+
restoreStreamCatalogFromLedger() {
|
|
233674
|
+
const stored = this.runtimeState.getCapState(streamCatalogCapability.name);
|
|
233675
|
+
const descriptors = stored?.descriptors;
|
|
233676
|
+
if (!descriptors || descriptors.length === 0) return null;
|
|
233677
|
+
this.cachedStreamDescriptors = [...descriptors];
|
|
233678
|
+
this.ctx.logger.info("stream catalog restored from the durable slice (no camera contact)", {
|
|
233679
|
+
tags: { deviceId: this.id },
|
|
233680
|
+
meta: {
|
|
233681
|
+
count: descriptors.length,
|
|
233682
|
+
builtAt: stored?.lastFetchedAt ?? 0,
|
|
233683
|
+
ageMs: Date.now() - (stored?.lastFetchedAt ?? 0),
|
|
233684
|
+
sleeping: this.sleeping
|
|
233685
|
+
}
|
|
233686
|
+
});
|
|
233687
|
+
return this.cachedStreamDescriptors;
|
|
233688
|
+
}
|
|
233689
|
+
/**
|
|
233690
|
+
* How old a RESTORED catalog may get before a natural wake is worth spending
|
|
233691
|
+
* on a re-read. The catalog is profile-stable, so this is not about
|
|
233692
|
+
* freshness — it is the backstop for a profile changed by something that did
|
|
233693
|
+
* not invalidate the cache (a firmware update, an edit made on the Reolink
|
|
233694
|
+
* app). A day is several natural wakes on any camera that is working.
|
|
233695
|
+
*/
|
|
233696
|
+
static CATALOG_REFRESH_ON_WAKE_MS = 1440 * 6e4;
|
|
233538
233697
|
/** Single-flight guard for `buildStreamCatalog`. */
|
|
233539
233698
|
buildStreamCatalogInFlight = null;
|
|
233540
233699
|
/**
|
|
@@ -233997,7 +234156,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
233997
234156
|
auxAccessoryCount: this.auxAccessoryRefs.size
|
|
233998
234157
|
}
|
|
233999
234158
|
});
|
|
234000
|
-
await this.refreshBatteryFromApi().catch(() => {});
|
|
234159
|
+
await this.refreshBatteryFromApi("periodic").catch(() => {});
|
|
234001
234160
|
await this.alignAuxDevicesState("periodic").catch(() => {});
|
|
234002
234161
|
await this.refreshParentSettingsSnapshot().catch(() => {});
|
|
234003
234162
|
} finally {
|
|
@@ -234032,6 +234191,44 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
234032
234191
|
}
|
|
234033
234192
|
}
|
|
234034
234193
|
/**
|
|
234194
|
+
* Is this wake worth spending on a catalog re-read? See
|
|
234195
|
+
* `CATALOG_REFRESH_ON_WAKE_MS`. Held apart from `onWakeTransition` so the
|
|
234196
|
+
* decision is one expression a test can pin.
|
|
234197
|
+
*/
|
|
234198
|
+
shouldRebuildCatalogOnWake() {
|
|
234199
|
+
if (!this.cachedStreamDescriptors?.length) {
|
|
234200
|
+
if (!this.restoreStreamCatalogFromLedger()) return true;
|
|
234201
|
+
}
|
|
234202
|
+
const builtAt = this.runtimeState.getCapState(streamCatalogCapability.name)?.lastFetchedAt ?? 0;
|
|
234203
|
+
if (builtAt <= 0) return true;
|
|
234204
|
+
return Date.now() - builtAt > ReolinkCamera.CATALOG_REFRESH_ON_WAKE_MS;
|
|
234205
|
+
}
|
|
234206
|
+
/**
|
|
234207
|
+
* A PASSIVE proof of reachability just arrived — stamp `battery.lastContactAt`
|
|
234208
|
+
* so `deriveBatteryPresence` can tell "asleep" from "gone" (D173).
|
|
234209
|
+
*
|
|
234210
|
+
* Callable only from paths where the evidence cost us nothing: an inbound
|
|
234211
|
+
* firmware push, an observed wake, a round-trip somebody else's demand
|
|
234212
|
+
* already paid for. Never from a poll issued to answer this question — that
|
|
234213
|
+
* poll is the wake it is trying to detect.
|
|
234214
|
+
*
|
|
234215
|
+
* Quantised to `CONTACT_WRITE_QUANTUM_MS`: the value means "recently", and
|
|
234216
|
+
* writing it at millisecond resolution would put a SQLite commit behind
|
|
234217
|
+
* every Baichuan reply on the hub's busiest write path (the exact cost
|
|
234218
|
+
* `scripts/check-runtime-state-durability.ts` exists to bound).
|
|
234219
|
+
*/
|
|
234220
|
+
markPassiveContact() {
|
|
234221
|
+
if (!this.isBattery) return;
|
|
234222
|
+
const now = Date.now();
|
|
234223
|
+
const quantised = now - now % ReolinkCamera.CONTACT_WRITE_QUANTUM_MS;
|
|
234224
|
+
if (quantised <= (this.state.battery.lastContactAt ?? 0)) return;
|
|
234225
|
+
this.state.battery.lastContactAt = quantised;
|
|
234226
|
+
}
|
|
234227
|
+
/** Write granularity for `battery.lastContactAt` — see `markPassiveContact`.
|
|
234228
|
+
* Bounds the commit rate this field can cost at 12/hour/device, and only
|
|
234229
|
+
* for a device something is actually reaching. */
|
|
234230
|
+
static CONTACT_WRITE_QUANTUM_MS = 5 * 6e4;
|
|
234231
|
+
/**
|
|
234035
234232
|
* Shared wake-transition handler invoked by both the simpleEvent
|
|
234036
234233
|
* `awake` push (canonical fast path) and the sleep poll's
|
|
234037
234234
|
* `sleeping → awake` flip (backstop). Mirrors Scrypted's
|
|
@@ -234056,7 +234253,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
234056
234253
|
isBattery: this.isBattery
|
|
234057
234254
|
}
|
|
234058
234255
|
});
|
|
234059
|
-
if (
|
|
234256
|
+
if (this.shouldRebuildCatalogOnWake()) try {
|
|
234257
|
+
this.cachedStreamDescriptors = void 0;
|
|
234060
234258
|
if ((await this.buildStreamCatalog()).length > 0) this.ctx.eventBus.emit(createEvent(EventCategory.StreamParamsChanged, this.eventSource(), { deviceId: this.id }));
|
|
234061
234259
|
} catch (err) {
|
|
234062
234260
|
this.ctx.logger.debug("onWakeTransition: stream catalog build failed — will retry on next wake", {
|
|
@@ -235769,7 +235967,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
235769
235967
|
this.startSleepPoll();
|
|
235770
235968
|
this.startBatteryUpdatePolling();
|
|
235771
235969
|
this.registerBatteryIfSupported();
|
|
235772
|
-
this.refreshBatteryFromApi();
|
|
235970
|
+
this.refreshBatteryFromApi("demand");
|
|
235773
235971
|
} else this.startAlignAuxPolling();
|
|
235774
235972
|
this.resubscribeSimpleEvents(api, "adoptApi").catch((err) => {
|
|
235775
235973
|
this.ctx.logger.debug("Reolink adoptApi: simple-event subscribe failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
@@ -235881,7 +236079,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
235881
236079
|
this.startSleepPoll();
|
|
235882
236080
|
this.startBatteryUpdatePolling();
|
|
235883
236081
|
this.registerBatteryIfSupported();
|
|
235884
|
-
this.refreshBatteryFromApi();
|
|
236082
|
+
this.refreshBatteryFromApi("demand");
|
|
235885
236083
|
}
|
|
235886
236084
|
this.startWatchdogs();
|
|
235887
236085
|
this.probeAndPersistFeatures(api).catch((err) => {
|
|
@@ -235965,6 +236163,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
235965
236163
|
this.lastEventAt = Date.now();
|
|
235966
236164
|
this.consecutiveStaleHealthChecks = 0;
|
|
235967
236165
|
this.nextEventHealthCheckAt = 0;
|
|
236166
|
+
this.markPassiveContact();
|
|
235968
236167
|
const eventSource = this.eventSource();
|
|
235969
236168
|
if (event.type !== "battery") this.ctx.logger.info("Reolink simpleEvent received", { meta: {
|
|
235970
236169
|
type: event.type,
|
|
@@ -236397,6 +236596,9 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
236397
236596
|
const data = event.data;
|
|
236398
236597
|
if (data.parentDeviceId !== this.id) return;
|
|
236399
236598
|
const cid = typeof data.deviceId === "number" ? data.deviceId : null;
|
|
236599
|
+
if (cid !== null) {
|
|
236600
|
+
for (const [ch, did] of this.channelToDeviceId.entries()) if (did === cid) this.channelToDeviceId.delete(ch);
|
|
236601
|
+
}
|
|
236400
236602
|
this.ctx.logger.info("Reolink Hub: child unregistered externally — refreshing discovery", cid !== null ? { tags: { deviceId: cid } } : {});
|
|
236401
236603
|
this.refreshDiscoveryFromCamera().catch(() => {});
|
|
236402
236604
|
}));
|
|
@@ -236693,7 +236895,6 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
236693
236895
|
timeoutMs: HUB_DISCOVERY_REFRESH_TIMEOUT_MS
|
|
236694
236896
|
} });
|
|
236695
236897
|
const adoptedByChannel = await this.loadAdoptedChildrenByChannel();
|
|
236696
|
-
this.channelToDeviceId.clear();
|
|
236697
236898
|
for (const [channel, deviceId] of adoptedByChannel) this.channelToDeviceId.set(channel, deviceId);
|
|
236698
236899
|
try {
|
|
236699
236900
|
discovered = (await (await this.ensureApi()).getNvrChannelsSummary({
|
|
@@ -236701,7 +236902,7 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
236701
236902
|
timeoutMs: HUB_DISCOVERY_REFRESH_TIMEOUT_MS
|
|
236702
236903
|
})).devices.map((d) => {
|
|
236703
236904
|
const childNativeId = computeChildNativeId(this.stableId, d.channel, d.uid);
|
|
236704
|
-
const adoptedDeviceId =
|
|
236905
|
+
const adoptedDeviceId = this.channelToDeviceId.get(d.channel) ?? null;
|
|
236705
236906
|
return {
|
|
236706
236907
|
childNativeId,
|
|
236707
236908
|
name: d.name ?? `Channel ${d.channel}`,
|
package/dist/addon.mjs
CHANGED
|
@@ -21387,6 +21387,25 @@ var BatteryStatusSchema = object({
|
|
|
21387
21387
|
/** Ms epoch of the last observation. Lets consumers reason about freshness. */
|
|
21388
21388
|
lastUpdated: number(),
|
|
21389
21389
|
/**
|
|
21390
|
+
* Ms epoch of the last time the device PROVED it was reachable — a
|
|
21391
|
+
* completed firmware round-trip, an observed wake, or an inbound push
|
|
21392
|
+
* (firmware event, email). `0`/absent = never since this slice was born.
|
|
21393
|
+
*
|
|
21394
|
+
* This is the ONLY input that separates "asleep" from "gone", and it is
|
|
21395
|
+
* fed exclusively by PASSIVE signals: nothing may write it by reaching
|
|
21396
|
+
* for the radio, because a poll that confirms reachability is the same
|
|
21397
|
+
* poll that drains the battery. See {@link deriveBatteryPresence} — the
|
|
21398
|
+
* single derivation every consumer must use; no surface computes its own.
|
|
21399
|
+
*
|
|
21400
|
+
* It is deliberately NOT a clock in the
|
|
21401
|
+
* `scripts/check-runtime-state-durability.ts` sense: it is the
|
|
21402
|
+
* observation itself, and it is the only thing a 30-hour silence is
|
|
21403
|
+
* visible in. Writers quantise it (see `CONTACT_WRITE_QUANTUM_MS` in the
|
|
21404
|
+
* Reolink provider) so a value that means "recently" cannot cost a
|
|
21405
|
+
* SQLite commit per round-trip.
|
|
21406
|
+
*/
|
|
21407
|
+
lastContactAt: number().optional(),
|
|
21408
|
+
/**
|
|
21390
21409
|
* True when the source is a BINARY low-battery indicator (HA
|
|
21391
21410
|
* `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
|
|
21392
21411
|
* charge level — `percentage` is then a coarse stand-in (100 = normal,
|
|
@@ -27422,13 +27441,63 @@ var CamStreamDescriptorSchema = object({
|
|
|
27422
27441
|
* set of stream descriptors it can offer for the device, synchronously, so the
|
|
27423
27442
|
* broker can reconcile its registry against the authoritative provider state.
|
|
27424
27443
|
*/
|
|
27444
|
+
/**
|
|
27445
|
+
* The catalog as a DURABLE fact rather than a live answer.
|
|
27446
|
+
*
|
|
27447
|
+
* A battery camera's descriptors are profile-stable — they change when the
|
|
27448
|
+
* operator rewrites an encoder profile, not minute to minute — but building
|
|
27449
|
+
* them costs a Baichuan login, which on a sleeping Argus IS a wake. So the
|
|
27450
|
+
* provider is allowed to build them exactly once per profile and must serve
|
|
27451
|
+
* every later pull from a cache.
|
|
27452
|
+
*
|
|
27453
|
+
* Holding that cache only in RAM is what turned a restart into an outage. The
|
|
27454
|
+
* runner comes back with the camera asleep, `buildStreamCatalogUncached`
|
|
27455
|
+
* correctly refuses to wake it, the pull answers `[]`, the broker has no
|
|
27456
|
+
* cam-stream entry to build a broker from, and `webrtcSession.handleOffer`
|
|
27457
|
+
* fails with a flat "No broker for stream" — for as long as the camera sleeps,
|
|
27458
|
+
* which on a battery cam is most of the day. The camera was fine. The stream
|
|
27459
|
+
* was unreachable because the process had forgotten what the camera offers.
|
|
27460
|
+
*
|
|
27461
|
+
* Declaring it here puts it in `device-runtime-state`, the kernel's canonical
|
|
27462
|
+
* declared collection, with the same `restored` durability `battery` uses for
|
|
27463
|
+
* the same reason: the last known value is the only value there is while the
|
|
27464
|
+
* device is asleep. The broker's brokers are therefore always DEFINABLE — it
|
|
27465
|
+
* is the DIAL that wakes a camera, never the catalog (D173).
|
|
27466
|
+
*/
|
|
27467
|
+
var StreamCatalogStateSchema = object({
|
|
27468
|
+
/** The descriptors as last built from a real camera response. Never a guess:
|
|
27469
|
+
* a failed or refused build writes NOTHING, so a restored catalog is always
|
|
27470
|
+
* one the camera itself once produced. */
|
|
27471
|
+
descriptors: array(CamStreamDescriptorSchema),
|
|
27472
|
+
/** Ms epoch of the build that produced {@link descriptors}. Lets the wake
|
|
27473
|
+
* path decide whether the camera's own awake window is worth spending on a
|
|
27474
|
+
* re-read. */
|
|
27475
|
+
lastFetchedAt: number()
|
|
27476
|
+
});
|
|
27425
27477
|
var streamCatalogCapability = {
|
|
27426
27478
|
name: "stream-catalog",
|
|
27427
27479
|
scope: "device",
|
|
27428
27480
|
deviceNative: true,
|
|
27429
27481
|
mode: "singleton",
|
|
27430
27482
|
deviceTypes: [DeviceType.Camera],
|
|
27431
|
-
methods: { getCatalog: method(object({ deviceId: number().int().nonnegative() }), array(CamStreamDescriptorSchema).readonly()) }
|
|
27483
|
+
methods: { getCatalog: method(object({ deviceId: number().int().nonnegative() }), array(CamStreamDescriptorSchema).readonly()) },
|
|
27484
|
+
runtimeState: StreamCatalogStateSchema,
|
|
27485
|
+
/**
|
|
27486
|
+
* Runtime-state durability: **restored** — see the schema doc. A cold
|
|
27487
|
+
* catalog on a sleeping battery camera is not a slow first frame, it is a
|
|
27488
|
+
* camera that cannot be watched at all until it happens to wake.
|
|
27489
|
+
*
|
|
27490
|
+
* Churn is nil by construction: the slice is written only by a SUCCESSFUL
|
|
27491
|
+
* build, and a build only runs when there is no cached copy (or the copy is
|
|
27492
|
+
* a day old and the camera is awake anyway).
|
|
27493
|
+
*
|
|
27494
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
27495
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
27496
|
+
*/
|
|
27497
|
+
durability: "restored",
|
|
27498
|
+
/** Clock field: written, but excluded from the compare that decides whether
|
|
27499
|
+
* persisting is worth a SQLite commit — the descriptors are the value. */
|
|
27500
|
+
volatileStateFields: ["lastFetchedAt"]
|
|
27432
27501
|
};
|
|
27433
27502
|
/** One of the camera's stream profiles. */
|
|
27434
27503
|
var StreamProfileSchema = _enum([
|
|
@@ -29292,6 +29361,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
29292
29361
|
sceneMonitor: sceneMonitorCapability,
|
|
29293
29362
|
scriptRunner: scriptRunnerCapability,
|
|
29294
29363
|
smoke: smokeCapability,
|
|
29364
|
+
streamCatalog: streamCatalogCapability,
|
|
29295
29365
|
streamParams: streamParamsCapability,
|
|
29296
29366
|
switch: switchCapability,
|
|
29297
29367
|
tamper: tamperCapability,
|
|
@@ -230151,6 +230221,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
230151
230221
|
* retries.
|
|
230152
230222
|
*/
|
|
230153
230223
|
async onProbe() {
|
|
230224
|
+
if (this.isBattery && this.sleeping) {
|
|
230225
|
+
this.ctx.logger.info("onProbe skipped — battery cam is sleeping (no login, no wake)", {
|
|
230226
|
+
tags: { deviceId: this.id },
|
|
230227
|
+
meta: { probeRetriesAvoided: true }
|
|
230228
|
+
});
|
|
230229
|
+
return;
|
|
230230
|
+
}
|
|
230154
230231
|
let api;
|
|
230155
230232
|
try {
|
|
230156
230233
|
api = await this.ensureApi();
|
|
@@ -230851,9 +230928,21 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
230851
230928
|
status: slice
|
|
230852
230929
|
}));
|
|
230853
230930
|
});
|
|
230854
|
-
this.refreshBatteryFromApi();
|
|
230931
|
+
this.refreshBatteryFromApi("register");
|
|
230855
230932
|
}
|
|
230856
|
-
|
|
230933
|
+
/**
|
|
230934
|
+
* @param reason - `'register'` and `'periodic'` are OUR initiative and are
|
|
230935
|
+
* refused while the camera sleeps; `'wake'` and `'demand'` run because
|
|
230936
|
+
* something already has the camera awake or is entitled to wake it.
|
|
230937
|
+
*/
|
|
230938
|
+
async refreshBatteryFromApi(reason) {
|
|
230939
|
+
if (this.isBattery && this.sleeping && (reason === "register" || reason === "periodic")) {
|
|
230940
|
+
this.ctx.logger.debug("battery refresh skipped — cam sleeping, reading restored slice", {
|
|
230941
|
+
tags: { deviceId: this.id },
|
|
230942
|
+
meta: { reason }
|
|
230943
|
+
});
|
|
230944
|
+
return;
|
|
230945
|
+
}
|
|
230857
230946
|
let api;
|
|
230858
230947
|
try {
|
|
230859
230948
|
api = await this.ensureApi();
|
|
@@ -231011,7 +231100,14 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
231011
231100
|
return true;
|
|
231012
231101
|
}
|
|
231013
231102
|
updateBatteryCache(info) {
|
|
231014
|
-
|
|
231103
|
+
const mapped = this.mapBatteryInfo(info);
|
|
231104
|
+
const now = Date.now();
|
|
231105
|
+
const quantised = now - now % ReolinkCamera.CONTACT_WRITE_QUANTUM_MS;
|
|
231106
|
+
const previousContact = this.state.battery.lastContactAt ?? 0;
|
|
231107
|
+
this.setCapSlice(batteryCapability, {
|
|
231108
|
+
...mapped,
|
|
231109
|
+
lastContactAt: Math.max(previousContact, quantised)
|
|
231110
|
+
});
|
|
231015
231111
|
}
|
|
231016
231112
|
/**
|
|
231017
231113
|
* Battery cams require an explicit wake before cmd_id 109 will
|
|
@@ -233336,6 +233432,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
233336
233432
|
*/
|
|
233337
233433
|
async buildStreamCatalog() {
|
|
233338
233434
|
if (this.cachedStreamDescriptors?.length) return this.withLiveNativeSdp(this.cachedStreamDescriptors);
|
|
233435
|
+
const restored = this.restoreStreamCatalogFromLedger();
|
|
233436
|
+
if (restored) return this.withLiveNativeSdp(restored);
|
|
233339
233437
|
if (this.buildStreamCatalogInFlight) return this.withLiveNativeSdp(await this.buildStreamCatalogInFlight);
|
|
233340
233438
|
const build = this.buildStreamCatalogUncached();
|
|
233341
233439
|
this.buildStreamCatalogInFlight = build;
|
|
@@ -233508,6 +233606,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
233508
233606
|
autoEligible: e.autoEligible
|
|
233509
233607
|
}));
|
|
233510
233608
|
this.cachedStreamDescriptors = descriptors;
|
|
233609
|
+
this.persistStreamCatalogToLedger(descriptors);
|
|
233511
233610
|
return descriptors;
|
|
233512
233611
|
}
|
|
233513
233612
|
/** Profile-stable stream descriptors, cached after the first successful
|
|
@@ -233515,6 +233614,66 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
233515
233614
|
* sleeping battery cam is never woken by a catalog poll. Invalidated by
|
|
233516
233615
|
* `applyStreamProfilePatch` (codec/resolution may change). */
|
|
233517
233616
|
cachedStreamDescriptors;
|
|
233617
|
+
/**
|
|
233618
|
+
* Write the just-built catalog to the durable `stream-catalog` slice, so a
|
|
233619
|
+
* restart with the camera asleep still has descriptors to serve (D173).
|
|
233620
|
+
*
|
|
233621
|
+
* Best-effort by design: the RAM copy is already advanced by the caller, and
|
|
233622
|
+
* losing this write costs one cold catalog after the next restart — never a
|
|
233623
|
+
* wrong catalog. A build that FAILED writes nothing at all and therefore
|
|
233624
|
+
* cannot demote a good stored copy (D49's failure direction).
|
|
233625
|
+
*/
|
|
233626
|
+
persistStreamCatalogToLedger(descriptors) {
|
|
233627
|
+
if (descriptors.length === 0) return;
|
|
233628
|
+
try {
|
|
233629
|
+
const state = {
|
|
233630
|
+
descriptors: [...descriptors],
|
|
233631
|
+
lastFetchedAt: Date.now()
|
|
233632
|
+
};
|
|
233633
|
+
this.runtimeState.setCapState(streamCatalogCapability.name, state);
|
|
233634
|
+
this.ctx.logger.debug("stream catalog persisted to the durable slice", {
|
|
233635
|
+
tags: { deviceId: this.id },
|
|
233636
|
+
meta: { count: descriptors.length }
|
|
233637
|
+
});
|
|
233638
|
+
} catch (err) {
|
|
233639
|
+
this.ctx.logger.debug("stream catalog persist failed — RAM copy stands", {
|
|
233640
|
+
tags: { deviceId: this.id },
|
|
233641
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
233642
|
+
});
|
|
233643
|
+
}
|
|
233644
|
+
}
|
|
233645
|
+
/**
|
|
233646
|
+
* Rehydrate `cachedStreamDescriptors` from the durable slice. Returns the
|
|
233647
|
+
* restored descriptors, or `null` when there is nothing to restore.
|
|
233648
|
+
*
|
|
233649
|
+
* Logged at `info` when it fires: "these descriptors came from before the
|
|
233650
|
+
* restart" must never be something a reader has to infer (the DurableLedger
|
|
233651
|
+
* contract, D132).
|
|
233652
|
+
*/
|
|
233653
|
+
restoreStreamCatalogFromLedger() {
|
|
233654
|
+
const stored = this.runtimeState.getCapState(streamCatalogCapability.name);
|
|
233655
|
+
const descriptors = stored?.descriptors;
|
|
233656
|
+
if (!descriptors || descriptors.length === 0) return null;
|
|
233657
|
+
this.cachedStreamDescriptors = [...descriptors];
|
|
233658
|
+
this.ctx.logger.info("stream catalog restored from the durable slice (no camera contact)", {
|
|
233659
|
+
tags: { deviceId: this.id },
|
|
233660
|
+
meta: {
|
|
233661
|
+
count: descriptors.length,
|
|
233662
|
+
builtAt: stored?.lastFetchedAt ?? 0,
|
|
233663
|
+
ageMs: Date.now() - (stored?.lastFetchedAt ?? 0),
|
|
233664
|
+
sleeping: this.sleeping
|
|
233665
|
+
}
|
|
233666
|
+
});
|
|
233667
|
+
return this.cachedStreamDescriptors;
|
|
233668
|
+
}
|
|
233669
|
+
/**
|
|
233670
|
+
* How old a RESTORED catalog may get before a natural wake is worth spending
|
|
233671
|
+
* on a re-read. The catalog is profile-stable, so this is not about
|
|
233672
|
+
* freshness — it is the backstop for a profile changed by something that did
|
|
233673
|
+
* not invalidate the cache (a firmware update, an edit made on the Reolink
|
|
233674
|
+
* app). A day is several natural wakes on any camera that is working.
|
|
233675
|
+
*/
|
|
233676
|
+
static CATALOG_REFRESH_ON_WAKE_MS = 1440 * 6e4;
|
|
233518
233677
|
/** Single-flight guard for `buildStreamCatalog`. */
|
|
233519
233678
|
buildStreamCatalogInFlight = null;
|
|
233520
233679
|
/**
|
|
@@ -233977,7 +234136,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
233977
234136
|
auxAccessoryCount: this.auxAccessoryRefs.size
|
|
233978
234137
|
}
|
|
233979
234138
|
});
|
|
233980
|
-
await this.refreshBatteryFromApi().catch(() => {});
|
|
234139
|
+
await this.refreshBatteryFromApi("periodic").catch(() => {});
|
|
233981
234140
|
await this.alignAuxDevicesState("periodic").catch(() => {});
|
|
233982
234141
|
await this.refreshParentSettingsSnapshot().catch(() => {});
|
|
233983
234142
|
} finally {
|
|
@@ -234012,6 +234171,44 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
234012
234171
|
}
|
|
234013
234172
|
}
|
|
234014
234173
|
/**
|
|
234174
|
+
* Is this wake worth spending on a catalog re-read? See
|
|
234175
|
+
* `CATALOG_REFRESH_ON_WAKE_MS`. Held apart from `onWakeTransition` so the
|
|
234176
|
+
* decision is one expression a test can pin.
|
|
234177
|
+
*/
|
|
234178
|
+
shouldRebuildCatalogOnWake() {
|
|
234179
|
+
if (!this.cachedStreamDescriptors?.length) {
|
|
234180
|
+
if (!this.restoreStreamCatalogFromLedger()) return true;
|
|
234181
|
+
}
|
|
234182
|
+
const builtAt = this.runtimeState.getCapState(streamCatalogCapability.name)?.lastFetchedAt ?? 0;
|
|
234183
|
+
if (builtAt <= 0) return true;
|
|
234184
|
+
return Date.now() - builtAt > ReolinkCamera.CATALOG_REFRESH_ON_WAKE_MS;
|
|
234185
|
+
}
|
|
234186
|
+
/**
|
|
234187
|
+
* A PASSIVE proof of reachability just arrived — stamp `battery.lastContactAt`
|
|
234188
|
+
* so `deriveBatteryPresence` can tell "asleep" from "gone" (D173).
|
|
234189
|
+
*
|
|
234190
|
+
* Callable only from paths where the evidence cost us nothing: an inbound
|
|
234191
|
+
* firmware push, an observed wake, a round-trip somebody else's demand
|
|
234192
|
+
* already paid for. Never from a poll issued to answer this question — that
|
|
234193
|
+
* poll is the wake it is trying to detect.
|
|
234194
|
+
*
|
|
234195
|
+
* Quantised to `CONTACT_WRITE_QUANTUM_MS`: the value means "recently", and
|
|
234196
|
+
* writing it at millisecond resolution would put a SQLite commit behind
|
|
234197
|
+
* every Baichuan reply on the hub's busiest write path (the exact cost
|
|
234198
|
+
* `scripts/check-runtime-state-durability.ts` exists to bound).
|
|
234199
|
+
*/
|
|
234200
|
+
markPassiveContact() {
|
|
234201
|
+
if (!this.isBattery) return;
|
|
234202
|
+
const now = Date.now();
|
|
234203
|
+
const quantised = now - now % ReolinkCamera.CONTACT_WRITE_QUANTUM_MS;
|
|
234204
|
+
if (quantised <= (this.state.battery.lastContactAt ?? 0)) return;
|
|
234205
|
+
this.state.battery.lastContactAt = quantised;
|
|
234206
|
+
}
|
|
234207
|
+
/** Write granularity for `battery.lastContactAt` — see `markPassiveContact`.
|
|
234208
|
+
* Bounds the commit rate this field can cost at 12/hour/device, and only
|
|
234209
|
+
* for a device something is actually reaching. */
|
|
234210
|
+
static CONTACT_WRITE_QUANTUM_MS = 5 * 6e4;
|
|
234211
|
+
/**
|
|
234015
234212
|
* Shared wake-transition handler invoked by both the simpleEvent
|
|
234016
234213
|
* `awake` push (canonical fast path) and the sleep poll's
|
|
234017
234214
|
* `sleeping → awake` flip (backstop). Mirrors Scrypted's
|
|
@@ -234036,7 +234233,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
234036
234233
|
isBattery: this.isBattery
|
|
234037
234234
|
}
|
|
234038
234235
|
});
|
|
234039
|
-
if (
|
|
234236
|
+
if (this.shouldRebuildCatalogOnWake()) try {
|
|
234237
|
+
this.cachedStreamDescriptors = void 0;
|
|
234040
234238
|
if ((await this.buildStreamCatalog()).length > 0) this.ctx.eventBus.emit(createEvent(EventCategory.StreamParamsChanged, this.eventSource(), { deviceId: this.id }));
|
|
234041
234239
|
} catch (err) {
|
|
234042
234240
|
this.ctx.logger.debug("onWakeTransition: stream catalog build failed — will retry on next wake", {
|
|
@@ -235749,7 +235947,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
235749
235947
|
this.startSleepPoll();
|
|
235750
235948
|
this.startBatteryUpdatePolling();
|
|
235751
235949
|
this.registerBatteryIfSupported();
|
|
235752
|
-
this.refreshBatteryFromApi();
|
|
235950
|
+
this.refreshBatteryFromApi("demand");
|
|
235753
235951
|
} else this.startAlignAuxPolling();
|
|
235754
235952
|
this.resubscribeSimpleEvents(api, "adoptApi").catch((err) => {
|
|
235755
235953
|
this.ctx.logger.debug("Reolink adoptApi: simple-event subscribe failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
@@ -235861,7 +236059,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
235861
236059
|
this.startSleepPoll();
|
|
235862
236060
|
this.startBatteryUpdatePolling();
|
|
235863
236061
|
this.registerBatteryIfSupported();
|
|
235864
|
-
this.refreshBatteryFromApi();
|
|
236062
|
+
this.refreshBatteryFromApi("demand");
|
|
235865
236063
|
}
|
|
235866
236064
|
this.startWatchdogs();
|
|
235867
236065
|
this.probeAndPersistFeatures(api).catch((err) => {
|
|
@@ -235945,6 +236143,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
235945
236143
|
this.lastEventAt = Date.now();
|
|
235946
236144
|
this.consecutiveStaleHealthChecks = 0;
|
|
235947
236145
|
this.nextEventHealthCheckAt = 0;
|
|
236146
|
+
this.markPassiveContact();
|
|
235948
236147
|
const eventSource = this.eventSource();
|
|
235949
236148
|
if (event.type !== "battery") this.ctx.logger.info("Reolink simpleEvent received", { meta: {
|
|
235950
236149
|
type: event.type,
|
|
@@ -236377,6 +236576,9 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
236377
236576
|
const data = event.data;
|
|
236378
236577
|
if (data.parentDeviceId !== this.id) return;
|
|
236379
236578
|
const cid = typeof data.deviceId === "number" ? data.deviceId : null;
|
|
236579
|
+
if (cid !== null) {
|
|
236580
|
+
for (const [ch, did] of this.channelToDeviceId.entries()) if (did === cid) this.channelToDeviceId.delete(ch);
|
|
236581
|
+
}
|
|
236380
236582
|
this.ctx.logger.info("Reolink Hub: child unregistered externally — refreshing discovery", cid !== null ? { tags: { deviceId: cid } } : {});
|
|
236381
236583
|
this.refreshDiscoveryFromCamera().catch(() => {});
|
|
236382
236584
|
}));
|
|
@@ -236673,7 +236875,6 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
236673
236875
|
timeoutMs: HUB_DISCOVERY_REFRESH_TIMEOUT_MS
|
|
236674
236876
|
} });
|
|
236675
236877
|
const adoptedByChannel = await this.loadAdoptedChildrenByChannel();
|
|
236676
|
-
this.channelToDeviceId.clear();
|
|
236677
236878
|
for (const [channel, deviceId] of adoptedByChannel) this.channelToDeviceId.set(channel, deviceId);
|
|
236678
236879
|
try {
|
|
236679
236880
|
discovered = (await (await this.ensureApi()).getNvrChannelsSummary({
|
|
@@ -236681,7 +236882,7 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
236681
236882
|
timeoutMs: HUB_DISCOVERY_REFRESH_TIMEOUT_MS
|
|
236682
236883
|
})).devices.map((d) => {
|
|
236683
236884
|
const childNativeId = computeChildNativeId(this.stableId, d.channel, d.uid);
|
|
236684
|
-
const adoptedDeviceId =
|
|
236885
|
+
const adoptedDeviceId = this.channelToDeviceId.get(d.channel) ?? null;
|
|
236685
236886
|
return {
|
|
236686
236887
|
childNativeId,
|
|
236687
236888
|
name: d.name ?? `Channel ${d.channel}`,
|