@camstack/addon-provider-reolink 1.2.5 → 1.2.7
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 +308 -153
- package/dist/addon.mjs +308 -153
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -222620,6 +222620,33 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
222620
222620
|
markWakeIssued() {
|
|
222621
222621
|
this.lastProactiveWakeAt = Date.now();
|
|
222622
222622
|
}
|
|
222623
|
+
/**
|
|
222624
|
+
* Gate for a PROACTIVE camera read, checked BEFORE `ensureApi()`.
|
|
222625
|
+
*
|
|
222626
|
+
* The login itself is the wake. On a sleeping UDP/battery camera the
|
|
222627
|
+
* lib's discovery + handshake nudges the firmware awake (same reason
|
|
222628
|
+
* `refreshParentSettingsSnapshot` refuses to call `ensureApi` while
|
|
222629
|
+
* asleep), so gating only the explicit `wakeUp()` call is useless —
|
|
222630
|
+
* by the time we reach it the camera is already up. Production logs
|
|
222631
|
+
* showed exactly that: a background read produced a full
|
|
222632
|
+
* `Connecting to Reolink` → BCUDP discovery → `battery sleep state
|
|
222633
|
+
* committed` (awake) → whole refresh cascade, on a camera that had
|
|
222634
|
+
* been asleep for six minutes.
|
|
222635
|
+
*
|
|
222636
|
+
* Returns `false` when the caller must serve cache / bail out without
|
|
222637
|
+
* touching the socket. Stamps the cooldown when it does let a wake
|
|
222638
|
+
* through, so "at most one proactive wake per
|
|
222639
|
+
* `PROACTIVE_WAKE_COOLDOWN_MS`" holds across ALL proactive callers
|
|
222640
|
+
* rather than per-caller.
|
|
222641
|
+
*
|
|
222642
|
+
* Demand-driven paths never call this — see `canProactivelyWake`.
|
|
222643
|
+
*/
|
|
222644
|
+
allowProactiveCameraAccess(reason) {
|
|
222645
|
+
if (!this.isBattery || !this.sleeping) return true;
|
|
222646
|
+
if (!this.canProactivelyWake(reason)) return false;
|
|
222647
|
+
this.markWakeIssued();
|
|
222648
|
+
return true;
|
|
222649
|
+
}
|
|
222623
222650
|
updateBatteryCache(info) {
|
|
222624
222651
|
this.setCapSlice(batteryCapability, this.mapBatteryInfo(info));
|
|
222625
222652
|
}
|
|
@@ -222865,6 +222892,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
222865
222892
|
}
|
|
222866
222893
|
async fetchSnapshotWithSingleFlight() {
|
|
222867
222894
|
if (this.snapshotInFlight) return this.snapshotInFlight;
|
|
222895
|
+
if (!this.allowProactiveCameraAccess("snapshot")) {
|
|
222896
|
+
this.ctx.logger.debug("snapshot: skipped — sleeping battery cam inside wake cooldown", { tags: { deviceId: this.id } });
|
|
222897
|
+
return null;
|
|
222898
|
+
}
|
|
222868
222899
|
const promise = (async () => {
|
|
222869
222900
|
let api;
|
|
222870
222901
|
try {
|
|
@@ -222882,23 +222913,16 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
222882
222913
|
} catch {
|
|
222883
222914
|
return true;
|
|
222884
222915
|
}
|
|
222885
|
-
})()) {
|
|
222886
|
-
|
|
222887
|
-
|
|
222888
|
-
|
|
222889
|
-
}
|
|
222890
|
-
|
|
222891
|
-
try {
|
|
222892
|
-
|
|
222893
|
-
|
|
222894
|
-
|
|
222895
|
-
});
|
|
222896
|
-
} catch (err) {
|
|
222897
|
-
this.ctx.logger.debug("snapshot: pre-wake failed (will still try getSnapshot)", {
|
|
222898
|
-
tags: { deviceId: this.id },
|
|
222899
|
-
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
222900
|
-
});
|
|
222901
|
-
}
|
|
222916
|
+
})()) try {
|
|
222917
|
+
await api.wakeUp(this.getChannel(), {
|
|
222918
|
+
waitAfterWakeMs: 1500,
|
|
222919
|
+
attempts: 2
|
|
222920
|
+
});
|
|
222921
|
+
} catch (err) {
|
|
222922
|
+
this.ctx.logger.debug("snapshot: pre-wake failed (will still try getSnapshot)", {
|
|
222923
|
+
tags: { deviceId: this.id },
|
|
222924
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
222925
|
+
});
|
|
222902
222926
|
}
|
|
222903
222927
|
const tryOnce = async (timeoutMs) => {
|
|
222904
222928
|
const buf = await api.getSnapshot(this.getChannel(), {
|
|
@@ -223441,6 +223465,97 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
223441
223465
|
}
|
|
223442
223466
|
}
|
|
223443
223467
|
/**
|
|
223468
|
+
* Per-cap "warm the cache while the camera is up" closures, registered
|
|
223469
|
+
* by each device-config cap alongside its provider and run by
|
|
223470
|
+
* `onWakeTransition`.
|
|
223471
|
+
*
|
|
223472
|
+
* This is the OTHER half of the read discipline. Gating reads stops us
|
|
223473
|
+
* waking the camera, but on its own it leaves the caches empty — and an
|
|
223474
|
+
* empty cache is what the operator actually sees: the State panel reads
|
|
223475
|
+
* the runtime-state slices (`deviceState.getAllSnapshots`), so a
|
|
223476
|
+
* sleeping camera whose slices were never written shows null for
|
|
223477
|
+
* everything. Before the gate, the admin UI's 2.5s aggregate poll kept
|
|
223478
|
+
* those slices warm by waking the camera every time — the very bug we
|
|
223479
|
+
* fixed. So the refresh has to move to the moment the camera is
|
|
223480
|
+
* ALREADY up, which is exactly the wake transition.
|
|
223481
|
+
*/
|
|
223482
|
+
capWarmers = /* @__PURE__ */ new Map();
|
|
223483
|
+
/** How fresh a slice must be for the wake warm-up to skip it. A battery
|
|
223484
|
+
* cam can wake many times an hour on motion; re-probing every cap on
|
|
223485
|
+
* every wake would spend the (~14s) window on data we already have. */
|
|
223486
|
+
static WAKE_WARM_MAX_AGE_MS = 10 * 6e4;
|
|
223487
|
+
registerCapWarmer(capName, warm) {
|
|
223488
|
+
this.capWarmers.set(capName, warm);
|
|
223489
|
+
}
|
|
223490
|
+
/**
|
|
223491
|
+
* Refresh the device-config cap caches during a wake window.
|
|
223492
|
+
*
|
|
223493
|
+
* SERIALIZED, like every other wake-path refresh — concurrent Baichuan
|
|
223494
|
+
* traffic on the single BCUDP stream starves the heavy reads. Each cap
|
|
223495
|
+
* is skipped when its slice is still fresh, and each failure is
|
|
223496
|
+
* swallowed: the window is short, partial progress accumulates across
|
|
223497
|
+
* wakes because the slices persist.
|
|
223498
|
+
*/
|
|
223499
|
+
async warmCapSlicesOnWake() {
|
|
223500
|
+
const now = Date.now();
|
|
223501
|
+
let warmed = 0;
|
|
223502
|
+
let skipped = 0;
|
|
223503
|
+
for (const [capName, warm] of this.capWarmers) {
|
|
223504
|
+
const slice = this.runtimeState.getCapState(capName);
|
|
223505
|
+
const fetchedAt = typeof slice?.lastFetchedAt === "number" ? slice.lastFetchedAt : 0;
|
|
223506
|
+
if (fetchedAt > 0 && now - fetchedAt < ReolinkCamera.WAKE_WARM_MAX_AGE_MS) {
|
|
223507
|
+
skipped += 1;
|
|
223508
|
+
continue;
|
|
223509
|
+
}
|
|
223510
|
+
try {
|
|
223511
|
+
await warm();
|
|
223512
|
+
warmed += 1;
|
|
223513
|
+
} catch (err) {
|
|
223514
|
+
this.ctx.logger.debug("wake warm-up: cap refresh failed — retrying on next wake", {
|
|
223515
|
+
tags: { deviceId: this.id },
|
|
223516
|
+
meta: {
|
|
223517
|
+
capName,
|
|
223518
|
+
error: err instanceof Error ? err.message : String(err)
|
|
223519
|
+
}
|
|
223520
|
+
});
|
|
223521
|
+
}
|
|
223522
|
+
}
|
|
223523
|
+
this.ctx.logger.info("wake warm-up: cap caches refreshed", {
|
|
223524
|
+
tags: { deviceId: this.id },
|
|
223525
|
+
meta: {
|
|
223526
|
+
warmed,
|
|
223527
|
+
skipped,
|
|
223528
|
+
total: this.capWarmers.size
|
|
223529
|
+
}
|
|
223530
|
+
});
|
|
223531
|
+
}
|
|
223532
|
+
/**
|
|
223533
|
+
* Ask the snapshot wrapper for a frame while the camera is up, so its
|
|
223534
|
+
* cache holds something recent to serve for the whole sleep window.
|
|
223535
|
+
*
|
|
223536
|
+
* The wrapper already does the right thing on a miss — `resolveOutcome`
|
|
223537
|
+
* falls back to the stale frame rather than blanking the UI — but only
|
|
223538
|
+
* if a frame was ever captured. With reads gated, nothing captures one
|
|
223539
|
+
* any more: the tile stayed empty for as long as the camera slept.
|
|
223540
|
+
* `force: true` bypasses the wrapper's freshness gate; our own
|
|
223541
|
+
* proactive gate lets it through because `sleeping` is false here.
|
|
223542
|
+
* Mirrors the reference's `updateBatteryAndSnapshot`.
|
|
223543
|
+
*/
|
|
223544
|
+
async warmSnapshotOnWake() {
|
|
223545
|
+
try {
|
|
223546
|
+
await this.ctx.api.snapshot.getSnapshot.query({
|
|
223547
|
+
deviceId: this.id,
|
|
223548
|
+
force: true
|
|
223549
|
+
});
|
|
223550
|
+
this.ctx.logger.debug("wake warm-up: snapshot cache refreshed", { tags: { deviceId: this.id } });
|
|
223551
|
+
} catch (err) {
|
|
223552
|
+
this.ctx.logger.debug("wake warm-up: snapshot refresh failed", {
|
|
223553
|
+
tags: { deviceId: this.id },
|
|
223554
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
223555
|
+
});
|
|
223556
|
+
}
|
|
223557
|
+
}
|
|
223558
|
+
/**
|
|
223444
223559
|
* Wrap a cap's `refreshFromCamera` so the READ side (the bridge's
|
|
223445
223560
|
* stale-check behind `getStatus`) never wakes a sleeping battery cam.
|
|
223446
223561
|
* The bridge then projects whatever the slice last held — which is
|
|
@@ -223545,6 +223660,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
223545
223660
|
this.ctx.eventBus.emit(createEvent(EventCategory.StreamParamsChanged, this.eventSource(), { deviceId: this.id }));
|
|
223546
223661
|
}
|
|
223547
223662
|
};
|
|
223663
|
+
this.registerCapWarmer(CAP_NAME, async () => {
|
|
223664
|
+
await provider.getOptions({ deviceId: this.id });
|
|
223665
|
+
await refreshFromCamera();
|
|
223666
|
+
});
|
|
223548
223667
|
this.ctx.registerNativeCap(streamParamsCapability, provider);
|
|
223549
223668
|
this.ctx.logger.info("Reolink stream-params cap registered", { tags: { deviceId: this.id } });
|
|
223550
223669
|
}
|
|
@@ -223735,6 +223854,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
223735
223854
|
await refreshFromCamera();
|
|
223736
223855
|
}
|
|
223737
223856
|
};
|
|
223857
|
+
this.registerCapWarmer(CAP_NAME, async () => {
|
|
223858
|
+
await provider.getOptions({ deviceId: this.id });
|
|
223859
|
+
await refreshFromCamera();
|
|
223860
|
+
});
|
|
223738
223861
|
this.ctx.registerNativeCap(motionZonesCapability, provider);
|
|
223739
223862
|
this.ctx.logger.info("Reolink motion-zones cap registered", { tags: { deviceId: this.id } });
|
|
223740
223863
|
}
|
|
@@ -223870,6 +223993,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
223870
223993
|
});
|
|
223871
223994
|
}
|
|
223872
223995
|
};
|
|
223996
|
+
this.registerCapWarmer(CAP_NAME, async () => {
|
|
223997
|
+
await provider.getOptions({ deviceId: this.id });
|
|
223998
|
+
await refreshFromCamera();
|
|
223999
|
+
});
|
|
223873
224000
|
this.ctx.registerNativeCap(privacyMaskCapability, provider);
|
|
223874
224001
|
this.ctx.logger.info("Reolink privacy-mask cap registered (read + enable + zone write)", { tags: { deviceId: this.id } });
|
|
223875
224002
|
}
|
|
@@ -224005,6 +224132,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
224005
224132
|
await refreshFromCamera();
|
|
224006
224133
|
}
|
|
224007
224134
|
};
|
|
224135
|
+
this.registerCapWarmer(CAP_NAME, async () => {
|
|
224136
|
+
await provider.getOptions({ deviceId: this.id });
|
|
224137
|
+
await refreshFromCamera();
|
|
224138
|
+
});
|
|
224008
224139
|
this.ctx.registerNativeCap(dayNightCapability, provider);
|
|
224009
224140
|
this.ctx.logger.info("Reolink day-night cap registered", { tags: { deviceId: this.id } });
|
|
224010
224141
|
}
|
|
@@ -224142,6 +224273,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
224142
224273
|
await refreshFromCamera();
|
|
224143
224274
|
}
|
|
224144
224275
|
};
|
|
224276
|
+
this.registerCapWarmer(CAP_NAME, async () => {
|
|
224277
|
+
await provider.getOptions({ deviceId: this.id });
|
|
224278
|
+
await refreshFromCamera();
|
|
224279
|
+
});
|
|
224145
224280
|
this.ctx.registerNativeCap(imageSettingsCapability, provider);
|
|
224146
224281
|
this.ctx.logger.info("Reolink image-settings cap registered", { tags: { deviceId: this.id } });
|
|
224147
224282
|
}
|
|
@@ -224411,6 +224546,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
224411
224546
|
await refreshFromCamera();
|
|
224412
224547
|
}
|
|
224413
224548
|
};
|
|
224549
|
+
this.registerCapWarmer(CAP_NAME, refreshFromCamera);
|
|
224414
224550
|
this.ctx.registerNativeCap(ptzAutotrackCapability, provider);
|
|
224415
224551
|
this.ctx.logger.info("Reolink ptz-autotrack cap registered", { tags: { deviceId: this.id } });
|
|
224416
224552
|
}
|
|
@@ -224418,148 +224554,154 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
224418
224554
|
if (this.ptzRegistered) return;
|
|
224419
224555
|
if (!this.getProbeFlags().hasPtz) return;
|
|
224420
224556
|
this.ptzRegistered = true;
|
|
224421
|
-
|
|
224422
|
-
|
|
224423
|
-
|
|
224424
|
-
|
|
224425
|
-
|
|
224426
|
-
|
|
224427
|
-
|
|
224428
|
-
|
|
224429
|
-
|
|
224430
|
-
|
|
224431
|
-
|
|
224432
|
-
|
|
224433
|
-
const channel = this.getChannel();
|
|
224434
|
-
try {
|
|
224435
|
-
await api.ptz(channel, {
|
|
224436
|
-
action: "stop",
|
|
224437
|
-
command: "Up"
|
|
224438
|
-
});
|
|
224439
|
-
} catch {}
|
|
224440
|
-
},
|
|
224441
|
-
getPresets: async ({ deviceId }) => {
|
|
224442
|
-
if (deviceId !== this.id) return [];
|
|
224443
|
-
try {
|
|
224557
|
+
{
|
|
224558
|
+
const ptzProvider = {
|
|
224559
|
+
move: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
224560
|
+
if (deviceId !== this.id) return;
|
|
224561
|
+
await this.runPtz(pan, tilt, zoom, speed, false);
|
|
224562
|
+
},
|
|
224563
|
+
continuousMove: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
224564
|
+
if (deviceId !== this.id) return;
|
|
224565
|
+
await this.runPtz(pan, tilt, zoom, speed, true);
|
|
224566
|
+
},
|
|
224567
|
+
stop: async ({ deviceId }) => {
|
|
224568
|
+
if (deviceId !== this.id) return;
|
|
224444
224569
|
const api = await this.ensureApi();
|
|
224445
224570
|
const channel = this.getChannel();
|
|
224446
|
-
|
|
224447
|
-
|
|
224448
|
-
|
|
224449
|
-
|
|
224450
|
-
|
|
224451
|
-
|
|
224452
|
-
}
|
|
224453
|
-
|
|
224454
|
-
|
|
224455
|
-
|
|
224456
|
-
|
|
224457
|
-
|
|
224458
|
-
|
|
224459
|
-
|
|
224460
|
-
|
|
224461
|
-
|
|
224462
|
-
|
|
224463
|
-
|
|
224464
|
-
const api = await this.ensureApi();
|
|
224465
|
-
const channel = this.getChannel();
|
|
224466
|
-
const id = Number(presetId);
|
|
224467
|
-
if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
|
|
224468
|
-
await api.setPtzPreset(channel, id, name);
|
|
224469
|
-
},
|
|
224470
|
-
deletePreset: async ({ deviceId, presetId }) => {
|
|
224471
|
-
if (deviceId !== this.id) return;
|
|
224472
|
-
const api = await this.ensureApi();
|
|
224473
|
-
const channel = this.getChannel();
|
|
224474
|
-
const id = Number(presetId);
|
|
224475
|
-
if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
|
|
224476
|
-
await api.deletePtzPreset(channel, id);
|
|
224477
|
-
},
|
|
224478
|
-
getOptions: async ({ deviceId }) => {
|
|
224479
|
-
if (deviceId !== this.id) return {
|
|
224480
|
-
hasPan: false,
|
|
224481
|
-
hasTilt: false,
|
|
224482
|
-
hasZoom: false,
|
|
224483
|
-
supportsPresets: false,
|
|
224484
|
-
hasAutofocus: false
|
|
224485
|
-
};
|
|
224486
|
-
const hasAutofocus = this.config.get("deviceCache")?.autoFocusSnapshot?.supported === true;
|
|
224487
|
-
return this.resolveCapOptions({
|
|
224488
|
-
capName: "ptz",
|
|
224489
|
-
schema: PtzOptionsSchema,
|
|
224490
|
-
probe: async () => {
|
|
224491
|
-
const { capabilities } = await (await this.ensureApi()).getDeviceCapabilities(this.getChannel());
|
|
224492
|
-
return {
|
|
224493
|
-
hasPan: capabilities.hasPan,
|
|
224494
|
-
hasTilt: capabilities.hasTilt,
|
|
224495
|
-
hasZoom: capabilities.hasZoom,
|
|
224496
|
-
supportsPresets: capabilities.hasPresets,
|
|
224497
|
-
hasAutofocus
|
|
224498
|
-
};
|
|
224499
|
-
},
|
|
224500
|
-
fallback: () => {
|
|
224501
|
-
const hasPtz = this.getProbeFlags().hasPtz === true;
|
|
224502
|
-
return {
|
|
224503
|
-
hasPan: hasPtz,
|
|
224504
|
-
hasTilt: hasPtz,
|
|
224505
|
-
hasZoom: hasPtz,
|
|
224506
|
-
supportsPresets: hasPtz,
|
|
224507
|
-
hasAutofocus
|
|
224508
|
-
};
|
|
224571
|
+
try {
|
|
224572
|
+
await api.ptz(channel, {
|
|
224573
|
+
action: "stop",
|
|
224574
|
+
command: "Up"
|
|
224575
|
+
});
|
|
224576
|
+
} catch {}
|
|
224577
|
+
},
|
|
224578
|
+
getPresets: async ({ deviceId }) => {
|
|
224579
|
+
if (deviceId !== this.id) return [];
|
|
224580
|
+
try {
|
|
224581
|
+
const api = await this.ensureApi();
|
|
224582
|
+
const channel = this.getChannel();
|
|
224583
|
+
return (await api.getPtzPresets(channel)).map((p) => ({
|
|
224584
|
+
id: String(p.id),
|
|
224585
|
+
name: p.name
|
|
224586
|
+
}));
|
|
224587
|
+
} catch {
|
|
224588
|
+
return [];
|
|
224509
224589
|
}
|
|
224510
|
-
}
|
|
224511
|
-
|
|
224512
|
-
|
|
224513
|
-
if (deviceId !== this.id) return;
|
|
224514
|
-
try {
|
|
224590
|
+
},
|
|
224591
|
+
goToPreset: async ({ deviceId, presetId }) => {
|
|
224592
|
+
if (deviceId !== this.id) return;
|
|
224515
224593
|
const api = await this.ensureApi();
|
|
224516
224594
|
const channel = this.getChannel();
|
|
224517
|
-
|
|
224518
|
-
|
|
224519
|
-
|
|
224520
|
-
|
|
224521
|
-
|
|
224522
|
-
|
|
224523
|
-
|
|
224524
|
-
|
|
224525
|
-
|
|
224526
|
-
|
|
224527
|
-
|
|
224528
|
-
|
|
224529
|
-
|
|
224530
|
-
|
|
224531
|
-
|
|
224532
|
-
|
|
224533
|
-
|
|
224534
|
-
|
|
224535
|
-
|
|
224536
|
-
|
|
224537
|
-
|
|
224538
|
-
|
|
224539
|
-
|
|
224540
|
-
|
|
224541
|
-
|
|
224542
|
-
|
|
224543
|
-
|
|
224544
|
-
|
|
224545
|
-
|
|
224546
|
-
|
|
224547
|
-
|
|
224548
|
-
|
|
224549
|
-
|
|
224550
|
-
|
|
224551
|
-
|
|
224552
|
-
|
|
224553
|
-
|
|
224554
|
-
|
|
224555
|
-
|
|
224556
|
-
|
|
224557
|
-
|
|
224595
|
+
const id = Number(presetId);
|
|
224596
|
+
if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
|
|
224597
|
+
await api.moveToPtzPreset(channel, id);
|
|
224598
|
+
},
|
|
224599
|
+
savePreset: async ({ deviceId, presetId, name }) => {
|
|
224600
|
+
if (deviceId !== this.id) return;
|
|
224601
|
+
const api = await this.ensureApi();
|
|
224602
|
+
const channel = this.getChannel();
|
|
224603
|
+
const id = Number(presetId);
|
|
224604
|
+
if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
|
|
224605
|
+
await api.setPtzPreset(channel, id, name);
|
|
224606
|
+
},
|
|
224607
|
+
deletePreset: async ({ deviceId, presetId }) => {
|
|
224608
|
+
if (deviceId !== this.id) return;
|
|
224609
|
+
const api = await this.ensureApi();
|
|
224610
|
+
const channel = this.getChannel();
|
|
224611
|
+
const id = Number(presetId);
|
|
224612
|
+
if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
|
|
224613
|
+
await api.deletePtzPreset(channel, id);
|
|
224614
|
+
},
|
|
224615
|
+
getOptions: async ({ deviceId }) => {
|
|
224616
|
+
if (deviceId !== this.id) return {
|
|
224617
|
+
hasPan: false,
|
|
224618
|
+
hasTilt: false,
|
|
224619
|
+
hasZoom: false,
|
|
224620
|
+
supportsPresets: false,
|
|
224621
|
+
hasAutofocus: false
|
|
224622
|
+
};
|
|
224623
|
+
const hasAutofocus = this.config.get("deviceCache")?.autoFocusSnapshot?.supported === true;
|
|
224624
|
+
return this.resolveCapOptions({
|
|
224625
|
+
capName: "ptz",
|
|
224626
|
+
schema: PtzOptionsSchema,
|
|
224627
|
+
probe: async () => {
|
|
224628
|
+
const { capabilities } = await (await this.ensureApi()).getDeviceCapabilities(this.getChannel());
|
|
224629
|
+
return {
|
|
224630
|
+
hasPan: capabilities.hasPan,
|
|
224631
|
+
hasTilt: capabilities.hasTilt,
|
|
224632
|
+
hasZoom: capabilities.hasZoom,
|
|
224633
|
+
supportsPresets: capabilities.hasPresets,
|
|
224634
|
+
hasAutofocus
|
|
224635
|
+
};
|
|
224636
|
+
},
|
|
224637
|
+
fallback: () => {
|
|
224638
|
+
const hasPtz = this.getProbeFlags().hasPtz === true;
|
|
224639
|
+
return {
|
|
224640
|
+
hasPan: hasPtz,
|
|
224641
|
+
hasTilt: hasPtz,
|
|
224642
|
+
hasZoom: hasPtz,
|
|
224643
|
+
supportsPresets: hasPtz,
|
|
224644
|
+
hasAutofocus
|
|
224645
|
+
};
|
|
224558
224646
|
}
|
|
224559
|
-
}
|
|
224560
|
-
}
|
|
224561
|
-
|
|
224562
|
-
|
|
224647
|
+
});
|
|
224648
|
+
},
|
|
224649
|
+
goHome: async ({ deviceId }) => {
|
|
224650
|
+
if (deviceId !== this.id) return;
|
|
224651
|
+
try {
|
|
224652
|
+
const api = await this.ensureApi();
|
|
224653
|
+
const channel = this.getChannel();
|
|
224654
|
+
await api.moveToPtzPreset(channel, 0);
|
|
224655
|
+
} catch {}
|
|
224656
|
+
},
|
|
224657
|
+
getPosition: async ({ deviceId }) => {
|
|
224658
|
+
if (deviceId !== this.id) return {
|
|
224659
|
+
pan: 0,
|
|
224660
|
+
tilt: 0,
|
|
224661
|
+
zoom: 0
|
|
224662
|
+
};
|
|
224663
|
+
return {
|
|
224664
|
+
pan: 0,
|
|
224665
|
+
tilt: 0,
|
|
224666
|
+
zoom: 0
|
|
224667
|
+
};
|
|
224668
|
+
},
|
|
224669
|
+
getStatus: async ({ deviceId }) => {
|
|
224670
|
+
if (deviceId !== this.id) return {
|
|
224671
|
+
pan: 0,
|
|
224672
|
+
tilt: 0,
|
|
224673
|
+
zoom: 0,
|
|
224674
|
+
autofocus: false
|
|
224675
|
+
};
|
|
224676
|
+
return {
|
|
224677
|
+
pan: 0,
|
|
224678
|
+
tilt: 0,
|
|
224679
|
+
zoom: 0,
|
|
224680
|
+
autofocus: (this.config.get("deviceCache")?.autoFocusSnapshot)?.enabled === true
|
|
224681
|
+
};
|
|
224682
|
+
},
|
|
224683
|
+
setAutofocus: async ({ deviceId, enabled }) => {
|
|
224684
|
+
if (deviceId !== this.id) return;
|
|
224685
|
+
const api = await this.ensureApi();
|
|
224686
|
+
const channel = this.getChannel();
|
|
224687
|
+
await api.setAutoFocus(channel, enabled ? 0 : 1);
|
|
224688
|
+
try {
|
|
224689
|
+
const a = (await api.getAutoFocus(channel, { timeoutMs: 1500 }))?.body?.AutoFocus;
|
|
224690
|
+
if (a) await this.config.setAll({ deviceCache: {
|
|
224691
|
+
...this.config.get("deviceCache"),
|
|
224692
|
+
autoFocusSnapshot: {
|
|
224693
|
+
enabled: typeof a.disable === "number" ? a.disable === 0 : null,
|
|
224694
|
+
supported: true
|
|
224695
|
+
}
|
|
224696
|
+
} });
|
|
224697
|
+
} catch {}
|
|
224698
|
+
}
|
|
224699
|
+
};
|
|
224700
|
+
this.registerCapWarmer("ptz", async () => {
|
|
224701
|
+
await ptzProvider.getOptions({ deviceId: this.id });
|
|
224702
|
+
});
|
|
224703
|
+
this.ctx.registerNativeCap(ptzCapability, ptzProvider);
|
|
224704
|
+
}
|
|
224563
224705
|
}
|
|
224564
224706
|
/**
|
|
224565
224707
|
* Translate normalized (-1..1) pan/tilt/zoom to one or more discrete
|
|
@@ -225262,6 +225404,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
225262
225404
|
if (this.batteryUpdateInFlight) return this.batteryUpdateInFlight;
|
|
225263
225405
|
const cycle = (async () => {
|
|
225264
225406
|
try {
|
|
225407
|
+
if (!this.allowProactiveCameraAccess("battery-update")) {
|
|
225408
|
+
this.ctx.logger.debug("battery-update: cam sleeping — skipping cycle without connecting", { tags: { deviceId: this.id } });
|
|
225409
|
+
return;
|
|
225410
|
+
}
|
|
225265
225411
|
let api;
|
|
225266
225412
|
try {
|
|
225267
225413
|
api = await this.ensureApi();
|
|
@@ -225360,6 +225506,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
225360
225506
|
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
225361
225507
|
});
|
|
225362
225508
|
}
|
|
225509
|
+
await this.warmCapSlicesOnWake();
|
|
225510
|
+
await this.warmSnapshotOnWake();
|
|
225363
225511
|
await this.alignAuxDevicesState("wake");
|
|
225364
225512
|
await this.refreshParentSettingsSnapshot();
|
|
225365
225513
|
}
|
|
@@ -227113,6 +227261,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
227113
227261
|
}
|
|
227114
227262
|
if (this.api) return this.api;
|
|
227115
227263
|
if (this.loginPromise) return this.loginPromise;
|
|
227264
|
+
if (this.isBattery && this.sleeping) {
|
|
227265
|
+
const frames = (/* @__PURE__ */ new Error("wake-attribution")).stack?.split("\n").slice(2, 7).join(" | ");
|
|
227266
|
+
this.ctx.logger.info("battery cam login while sleeping — this wakes the camera", {
|
|
227267
|
+
tags: { deviceId: this.id },
|
|
227268
|
+
meta: { caller: frames ?? "unavailable" }
|
|
227269
|
+
});
|
|
227270
|
+
}
|
|
227116
227271
|
const host = this.config.get("host");
|
|
227117
227272
|
const port = this.config.get("port");
|
|
227118
227273
|
const username = this.config.get("username");
|
package/dist/addon.mjs
CHANGED
|
@@ -222600,6 +222600,33 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
222600
222600
|
markWakeIssued() {
|
|
222601
222601
|
this.lastProactiveWakeAt = Date.now();
|
|
222602
222602
|
}
|
|
222603
|
+
/**
|
|
222604
|
+
* Gate for a PROACTIVE camera read, checked BEFORE `ensureApi()`.
|
|
222605
|
+
*
|
|
222606
|
+
* The login itself is the wake. On a sleeping UDP/battery camera the
|
|
222607
|
+
* lib's discovery + handshake nudges the firmware awake (same reason
|
|
222608
|
+
* `refreshParentSettingsSnapshot` refuses to call `ensureApi` while
|
|
222609
|
+
* asleep), so gating only the explicit `wakeUp()` call is useless —
|
|
222610
|
+
* by the time we reach it the camera is already up. Production logs
|
|
222611
|
+
* showed exactly that: a background read produced a full
|
|
222612
|
+
* `Connecting to Reolink` → BCUDP discovery → `battery sleep state
|
|
222613
|
+
* committed` (awake) → whole refresh cascade, on a camera that had
|
|
222614
|
+
* been asleep for six minutes.
|
|
222615
|
+
*
|
|
222616
|
+
* Returns `false` when the caller must serve cache / bail out without
|
|
222617
|
+
* touching the socket. Stamps the cooldown when it does let a wake
|
|
222618
|
+
* through, so "at most one proactive wake per
|
|
222619
|
+
* `PROACTIVE_WAKE_COOLDOWN_MS`" holds across ALL proactive callers
|
|
222620
|
+
* rather than per-caller.
|
|
222621
|
+
*
|
|
222622
|
+
* Demand-driven paths never call this — see `canProactivelyWake`.
|
|
222623
|
+
*/
|
|
222624
|
+
allowProactiveCameraAccess(reason) {
|
|
222625
|
+
if (!this.isBattery || !this.sleeping) return true;
|
|
222626
|
+
if (!this.canProactivelyWake(reason)) return false;
|
|
222627
|
+
this.markWakeIssued();
|
|
222628
|
+
return true;
|
|
222629
|
+
}
|
|
222603
222630
|
updateBatteryCache(info) {
|
|
222604
222631
|
this.setCapSlice(batteryCapability, this.mapBatteryInfo(info));
|
|
222605
222632
|
}
|
|
@@ -222845,6 +222872,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
222845
222872
|
}
|
|
222846
222873
|
async fetchSnapshotWithSingleFlight() {
|
|
222847
222874
|
if (this.snapshotInFlight) return this.snapshotInFlight;
|
|
222875
|
+
if (!this.allowProactiveCameraAccess("snapshot")) {
|
|
222876
|
+
this.ctx.logger.debug("snapshot: skipped — sleeping battery cam inside wake cooldown", { tags: { deviceId: this.id } });
|
|
222877
|
+
return null;
|
|
222878
|
+
}
|
|
222848
222879
|
const promise = (async () => {
|
|
222849
222880
|
let api;
|
|
222850
222881
|
try {
|
|
@@ -222862,23 +222893,16 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
222862
222893
|
} catch {
|
|
222863
222894
|
return true;
|
|
222864
222895
|
}
|
|
222865
|
-
})()) {
|
|
222866
|
-
|
|
222867
|
-
|
|
222868
|
-
|
|
222869
|
-
}
|
|
222870
|
-
|
|
222871
|
-
try {
|
|
222872
|
-
|
|
222873
|
-
|
|
222874
|
-
|
|
222875
|
-
});
|
|
222876
|
-
} catch (err) {
|
|
222877
|
-
this.ctx.logger.debug("snapshot: pre-wake failed (will still try getSnapshot)", {
|
|
222878
|
-
tags: { deviceId: this.id },
|
|
222879
|
-
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
222880
|
-
});
|
|
222881
|
-
}
|
|
222896
|
+
})()) try {
|
|
222897
|
+
await api.wakeUp(this.getChannel(), {
|
|
222898
|
+
waitAfterWakeMs: 1500,
|
|
222899
|
+
attempts: 2
|
|
222900
|
+
});
|
|
222901
|
+
} catch (err) {
|
|
222902
|
+
this.ctx.logger.debug("snapshot: pre-wake failed (will still try getSnapshot)", {
|
|
222903
|
+
tags: { deviceId: this.id },
|
|
222904
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
222905
|
+
});
|
|
222882
222906
|
}
|
|
222883
222907
|
const tryOnce = async (timeoutMs) => {
|
|
222884
222908
|
const buf = await api.getSnapshot(this.getChannel(), {
|
|
@@ -223421,6 +223445,97 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
223421
223445
|
}
|
|
223422
223446
|
}
|
|
223423
223447
|
/**
|
|
223448
|
+
* Per-cap "warm the cache while the camera is up" closures, registered
|
|
223449
|
+
* by each device-config cap alongside its provider and run by
|
|
223450
|
+
* `onWakeTransition`.
|
|
223451
|
+
*
|
|
223452
|
+
* This is the OTHER half of the read discipline. Gating reads stops us
|
|
223453
|
+
* waking the camera, but on its own it leaves the caches empty — and an
|
|
223454
|
+
* empty cache is what the operator actually sees: the State panel reads
|
|
223455
|
+
* the runtime-state slices (`deviceState.getAllSnapshots`), so a
|
|
223456
|
+
* sleeping camera whose slices were never written shows null for
|
|
223457
|
+
* everything. Before the gate, the admin UI's 2.5s aggregate poll kept
|
|
223458
|
+
* those slices warm by waking the camera every time — the very bug we
|
|
223459
|
+
* fixed. So the refresh has to move to the moment the camera is
|
|
223460
|
+
* ALREADY up, which is exactly the wake transition.
|
|
223461
|
+
*/
|
|
223462
|
+
capWarmers = /* @__PURE__ */ new Map();
|
|
223463
|
+
/** How fresh a slice must be for the wake warm-up to skip it. A battery
|
|
223464
|
+
* cam can wake many times an hour on motion; re-probing every cap on
|
|
223465
|
+
* every wake would spend the (~14s) window on data we already have. */
|
|
223466
|
+
static WAKE_WARM_MAX_AGE_MS = 10 * 6e4;
|
|
223467
|
+
registerCapWarmer(capName, warm) {
|
|
223468
|
+
this.capWarmers.set(capName, warm);
|
|
223469
|
+
}
|
|
223470
|
+
/**
|
|
223471
|
+
* Refresh the device-config cap caches during a wake window.
|
|
223472
|
+
*
|
|
223473
|
+
* SERIALIZED, like every other wake-path refresh — concurrent Baichuan
|
|
223474
|
+
* traffic on the single BCUDP stream starves the heavy reads. Each cap
|
|
223475
|
+
* is skipped when its slice is still fresh, and each failure is
|
|
223476
|
+
* swallowed: the window is short, partial progress accumulates across
|
|
223477
|
+
* wakes because the slices persist.
|
|
223478
|
+
*/
|
|
223479
|
+
async warmCapSlicesOnWake() {
|
|
223480
|
+
const now = Date.now();
|
|
223481
|
+
let warmed = 0;
|
|
223482
|
+
let skipped = 0;
|
|
223483
|
+
for (const [capName, warm] of this.capWarmers) {
|
|
223484
|
+
const slice = this.runtimeState.getCapState(capName);
|
|
223485
|
+
const fetchedAt = typeof slice?.lastFetchedAt === "number" ? slice.lastFetchedAt : 0;
|
|
223486
|
+
if (fetchedAt > 0 && now - fetchedAt < ReolinkCamera.WAKE_WARM_MAX_AGE_MS) {
|
|
223487
|
+
skipped += 1;
|
|
223488
|
+
continue;
|
|
223489
|
+
}
|
|
223490
|
+
try {
|
|
223491
|
+
await warm();
|
|
223492
|
+
warmed += 1;
|
|
223493
|
+
} catch (err) {
|
|
223494
|
+
this.ctx.logger.debug("wake warm-up: cap refresh failed — retrying on next wake", {
|
|
223495
|
+
tags: { deviceId: this.id },
|
|
223496
|
+
meta: {
|
|
223497
|
+
capName,
|
|
223498
|
+
error: err instanceof Error ? err.message : String(err)
|
|
223499
|
+
}
|
|
223500
|
+
});
|
|
223501
|
+
}
|
|
223502
|
+
}
|
|
223503
|
+
this.ctx.logger.info("wake warm-up: cap caches refreshed", {
|
|
223504
|
+
tags: { deviceId: this.id },
|
|
223505
|
+
meta: {
|
|
223506
|
+
warmed,
|
|
223507
|
+
skipped,
|
|
223508
|
+
total: this.capWarmers.size
|
|
223509
|
+
}
|
|
223510
|
+
});
|
|
223511
|
+
}
|
|
223512
|
+
/**
|
|
223513
|
+
* Ask the snapshot wrapper for a frame while the camera is up, so its
|
|
223514
|
+
* cache holds something recent to serve for the whole sleep window.
|
|
223515
|
+
*
|
|
223516
|
+
* The wrapper already does the right thing on a miss — `resolveOutcome`
|
|
223517
|
+
* falls back to the stale frame rather than blanking the UI — but only
|
|
223518
|
+
* if a frame was ever captured. With reads gated, nothing captures one
|
|
223519
|
+
* any more: the tile stayed empty for as long as the camera slept.
|
|
223520
|
+
* `force: true` bypasses the wrapper's freshness gate; our own
|
|
223521
|
+
* proactive gate lets it through because `sleeping` is false here.
|
|
223522
|
+
* Mirrors the reference's `updateBatteryAndSnapshot`.
|
|
223523
|
+
*/
|
|
223524
|
+
async warmSnapshotOnWake() {
|
|
223525
|
+
try {
|
|
223526
|
+
await this.ctx.api.snapshot.getSnapshot.query({
|
|
223527
|
+
deviceId: this.id,
|
|
223528
|
+
force: true
|
|
223529
|
+
});
|
|
223530
|
+
this.ctx.logger.debug("wake warm-up: snapshot cache refreshed", { tags: { deviceId: this.id } });
|
|
223531
|
+
} catch (err) {
|
|
223532
|
+
this.ctx.logger.debug("wake warm-up: snapshot refresh failed", {
|
|
223533
|
+
tags: { deviceId: this.id },
|
|
223534
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
223535
|
+
});
|
|
223536
|
+
}
|
|
223537
|
+
}
|
|
223538
|
+
/**
|
|
223424
223539
|
* Wrap a cap's `refreshFromCamera` so the READ side (the bridge's
|
|
223425
223540
|
* stale-check behind `getStatus`) never wakes a sleeping battery cam.
|
|
223426
223541
|
* The bridge then projects whatever the slice last held — which is
|
|
@@ -223525,6 +223640,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
223525
223640
|
this.ctx.eventBus.emit(createEvent(EventCategory.StreamParamsChanged, this.eventSource(), { deviceId: this.id }));
|
|
223526
223641
|
}
|
|
223527
223642
|
};
|
|
223643
|
+
this.registerCapWarmer(CAP_NAME, async () => {
|
|
223644
|
+
await provider.getOptions({ deviceId: this.id });
|
|
223645
|
+
await refreshFromCamera();
|
|
223646
|
+
});
|
|
223528
223647
|
this.ctx.registerNativeCap(streamParamsCapability, provider);
|
|
223529
223648
|
this.ctx.logger.info("Reolink stream-params cap registered", { tags: { deviceId: this.id } });
|
|
223530
223649
|
}
|
|
@@ -223715,6 +223834,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
223715
223834
|
await refreshFromCamera();
|
|
223716
223835
|
}
|
|
223717
223836
|
};
|
|
223837
|
+
this.registerCapWarmer(CAP_NAME, async () => {
|
|
223838
|
+
await provider.getOptions({ deviceId: this.id });
|
|
223839
|
+
await refreshFromCamera();
|
|
223840
|
+
});
|
|
223718
223841
|
this.ctx.registerNativeCap(motionZonesCapability, provider);
|
|
223719
223842
|
this.ctx.logger.info("Reolink motion-zones cap registered", { tags: { deviceId: this.id } });
|
|
223720
223843
|
}
|
|
@@ -223850,6 +223973,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
223850
223973
|
});
|
|
223851
223974
|
}
|
|
223852
223975
|
};
|
|
223976
|
+
this.registerCapWarmer(CAP_NAME, async () => {
|
|
223977
|
+
await provider.getOptions({ deviceId: this.id });
|
|
223978
|
+
await refreshFromCamera();
|
|
223979
|
+
});
|
|
223853
223980
|
this.ctx.registerNativeCap(privacyMaskCapability, provider);
|
|
223854
223981
|
this.ctx.logger.info("Reolink privacy-mask cap registered (read + enable + zone write)", { tags: { deviceId: this.id } });
|
|
223855
223982
|
}
|
|
@@ -223985,6 +224112,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
223985
224112
|
await refreshFromCamera();
|
|
223986
224113
|
}
|
|
223987
224114
|
};
|
|
224115
|
+
this.registerCapWarmer(CAP_NAME, async () => {
|
|
224116
|
+
await provider.getOptions({ deviceId: this.id });
|
|
224117
|
+
await refreshFromCamera();
|
|
224118
|
+
});
|
|
223988
224119
|
this.ctx.registerNativeCap(dayNightCapability, provider);
|
|
223989
224120
|
this.ctx.logger.info("Reolink day-night cap registered", { tags: { deviceId: this.id } });
|
|
223990
224121
|
}
|
|
@@ -224122,6 +224253,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
224122
224253
|
await refreshFromCamera();
|
|
224123
224254
|
}
|
|
224124
224255
|
};
|
|
224256
|
+
this.registerCapWarmer(CAP_NAME, async () => {
|
|
224257
|
+
await provider.getOptions({ deviceId: this.id });
|
|
224258
|
+
await refreshFromCamera();
|
|
224259
|
+
});
|
|
224125
224260
|
this.ctx.registerNativeCap(imageSettingsCapability, provider);
|
|
224126
224261
|
this.ctx.logger.info("Reolink image-settings cap registered", { tags: { deviceId: this.id } });
|
|
224127
224262
|
}
|
|
@@ -224391,6 +224526,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
224391
224526
|
await refreshFromCamera();
|
|
224392
224527
|
}
|
|
224393
224528
|
};
|
|
224529
|
+
this.registerCapWarmer(CAP_NAME, refreshFromCamera);
|
|
224394
224530
|
this.ctx.registerNativeCap(ptzAutotrackCapability, provider);
|
|
224395
224531
|
this.ctx.logger.info("Reolink ptz-autotrack cap registered", { tags: { deviceId: this.id } });
|
|
224396
224532
|
}
|
|
@@ -224398,148 +224534,154 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
224398
224534
|
if (this.ptzRegistered) return;
|
|
224399
224535
|
if (!this.getProbeFlags().hasPtz) return;
|
|
224400
224536
|
this.ptzRegistered = true;
|
|
224401
|
-
|
|
224402
|
-
|
|
224403
|
-
|
|
224404
|
-
|
|
224405
|
-
|
|
224406
|
-
|
|
224407
|
-
|
|
224408
|
-
|
|
224409
|
-
|
|
224410
|
-
|
|
224411
|
-
|
|
224412
|
-
|
|
224413
|
-
const channel = this.getChannel();
|
|
224414
|
-
try {
|
|
224415
|
-
await api.ptz(channel, {
|
|
224416
|
-
action: "stop",
|
|
224417
|
-
command: "Up"
|
|
224418
|
-
});
|
|
224419
|
-
} catch {}
|
|
224420
|
-
},
|
|
224421
|
-
getPresets: async ({ deviceId }) => {
|
|
224422
|
-
if (deviceId !== this.id) return [];
|
|
224423
|
-
try {
|
|
224537
|
+
{
|
|
224538
|
+
const ptzProvider = {
|
|
224539
|
+
move: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
224540
|
+
if (deviceId !== this.id) return;
|
|
224541
|
+
await this.runPtz(pan, tilt, zoom, speed, false);
|
|
224542
|
+
},
|
|
224543
|
+
continuousMove: async ({ deviceId, pan, tilt, zoom, speed }) => {
|
|
224544
|
+
if (deviceId !== this.id) return;
|
|
224545
|
+
await this.runPtz(pan, tilt, zoom, speed, true);
|
|
224546
|
+
},
|
|
224547
|
+
stop: async ({ deviceId }) => {
|
|
224548
|
+
if (deviceId !== this.id) return;
|
|
224424
224549
|
const api = await this.ensureApi();
|
|
224425
224550
|
const channel = this.getChannel();
|
|
224426
|
-
|
|
224427
|
-
|
|
224428
|
-
|
|
224429
|
-
|
|
224430
|
-
|
|
224431
|
-
|
|
224432
|
-
}
|
|
224433
|
-
|
|
224434
|
-
|
|
224435
|
-
|
|
224436
|
-
|
|
224437
|
-
|
|
224438
|
-
|
|
224439
|
-
|
|
224440
|
-
|
|
224441
|
-
|
|
224442
|
-
|
|
224443
|
-
|
|
224444
|
-
const api = await this.ensureApi();
|
|
224445
|
-
const channel = this.getChannel();
|
|
224446
|
-
const id = Number(presetId);
|
|
224447
|
-
if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
|
|
224448
|
-
await api.setPtzPreset(channel, id, name);
|
|
224449
|
-
},
|
|
224450
|
-
deletePreset: async ({ deviceId, presetId }) => {
|
|
224451
|
-
if (deviceId !== this.id) return;
|
|
224452
|
-
const api = await this.ensureApi();
|
|
224453
|
-
const channel = this.getChannel();
|
|
224454
|
-
const id = Number(presetId);
|
|
224455
|
-
if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
|
|
224456
|
-
await api.deletePtzPreset(channel, id);
|
|
224457
|
-
},
|
|
224458
|
-
getOptions: async ({ deviceId }) => {
|
|
224459
|
-
if (deviceId !== this.id) return {
|
|
224460
|
-
hasPan: false,
|
|
224461
|
-
hasTilt: false,
|
|
224462
|
-
hasZoom: false,
|
|
224463
|
-
supportsPresets: false,
|
|
224464
|
-
hasAutofocus: false
|
|
224465
|
-
};
|
|
224466
|
-
const hasAutofocus = this.config.get("deviceCache")?.autoFocusSnapshot?.supported === true;
|
|
224467
|
-
return this.resolveCapOptions({
|
|
224468
|
-
capName: "ptz",
|
|
224469
|
-
schema: PtzOptionsSchema,
|
|
224470
|
-
probe: async () => {
|
|
224471
|
-
const { capabilities } = await (await this.ensureApi()).getDeviceCapabilities(this.getChannel());
|
|
224472
|
-
return {
|
|
224473
|
-
hasPan: capabilities.hasPan,
|
|
224474
|
-
hasTilt: capabilities.hasTilt,
|
|
224475
|
-
hasZoom: capabilities.hasZoom,
|
|
224476
|
-
supportsPresets: capabilities.hasPresets,
|
|
224477
|
-
hasAutofocus
|
|
224478
|
-
};
|
|
224479
|
-
},
|
|
224480
|
-
fallback: () => {
|
|
224481
|
-
const hasPtz = this.getProbeFlags().hasPtz === true;
|
|
224482
|
-
return {
|
|
224483
|
-
hasPan: hasPtz,
|
|
224484
|
-
hasTilt: hasPtz,
|
|
224485
|
-
hasZoom: hasPtz,
|
|
224486
|
-
supportsPresets: hasPtz,
|
|
224487
|
-
hasAutofocus
|
|
224488
|
-
};
|
|
224551
|
+
try {
|
|
224552
|
+
await api.ptz(channel, {
|
|
224553
|
+
action: "stop",
|
|
224554
|
+
command: "Up"
|
|
224555
|
+
});
|
|
224556
|
+
} catch {}
|
|
224557
|
+
},
|
|
224558
|
+
getPresets: async ({ deviceId }) => {
|
|
224559
|
+
if (deviceId !== this.id) return [];
|
|
224560
|
+
try {
|
|
224561
|
+
const api = await this.ensureApi();
|
|
224562
|
+
const channel = this.getChannel();
|
|
224563
|
+
return (await api.getPtzPresets(channel)).map((p) => ({
|
|
224564
|
+
id: String(p.id),
|
|
224565
|
+
name: p.name
|
|
224566
|
+
}));
|
|
224567
|
+
} catch {
|
|
224568
|
+
return [];
|
|
224489
224569
|
}
|
|
224490
|
-
}
|
|
224491
|
-
|
|
224492
|
-
|
|
224493
|
-
if (deviceId !== this.id) return;
|
|
224494
|
-
try {
|
|
224570
|
+
},
|
|
224571
|
+
goToPreset: async ({ deviceId, presetId }) => {
|
|
224572
|
+
if (deviceId !== this.id) return;
|
|
224495
224573
|
const api = await this.ensureApi();
|
|
224496
224574
|
const channel = this.getChannel();
|
|
224497
|
-
|
|
224498
|
-
|
|
224499
|
-
|
|
224500
|
-
|
|
224501
|
-
|
|
224502
|
-
|
|
224503
|
-
|
|
224504
|
-
|
|
224505
|
-
|
|
224506
|
-
|
|
224507
|
-
|
|
224508
|
-
|
|
224509
|
-
|
|
224510
|
-
|
|
224511
|
-
|
|
224512
|
-
|
|
224513
|
-
|
|
224514
|
-
|
|
224515
|
-
|
|
224516
|
-
|
|
224517
|
-
|
|
224518
|
-
|
|
224519
|
-
|
|
224520
|
-
|
|
224521
|
-
|
|
224522
|
-
|
|
224523
|
-
|
|
224524
|
-
|
|
224525
|
-
|
|
224526
|
-
|
|
224527
|
-
|
|
224528
|
-
|
|
224529
|
-
|
|
224530
|
-
|
|
224531
|
-
|
|
224532
|
-
|
|
224533
|
-
|
|
224534
|
-
|
|
224535
|
-
|
|
224536
|
-
|
|
224537
|
-
|
|
224575
|
+
const id = Number(presetId);
|
|
224576
|
+
if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
|
|
224577
|
+
await api.moveToPtzPreset(channel, id);
|
|
224578
|
+
},
|
|
224579
|
+
savePreset: async ({ deviceId, presetId, name }) => {
|
|
224580
|
+
if (deviceId !== this.id) return;
|
|
224581
|
+
const api = await this.ensureApi();
|
|
224582
|
+
const channel = this.getChannel();
|
|
224583
|
+
const id = Number(presetId);
|
|
224584
|
+
if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
|
|
224585
|
+
await api.setPtzPreset(channel, id, name);
|
|
224586
|
+
},
|
|
224587
|
+
deletePreset: async ({ deviceId, presetId }) => {
|
|
224588
|
+
if (deviceId !== this.id) return;
|
|
224589
|
+
const api = await this.ensureApi();
|
|
224590
|
+
const channel = this.getChannel();
|
|
224591
|
+
const id = Number(presetId);
|
|
224592
|
+
if (!Number.isFinite(id)) throw new Error(`Invalid presetId: ${presetId}`);
|
|
224593
|
+
await api.deletePtzPreset(channel, id);
|
|
224594
|
+
},
|
|
224595
|
+
getOptions: async ({ deviceId }) => {
|
|
224596
|
+
if (deviceId !== this.id) return {
|
|
224597
|
+
hasPan: false,
|
|
224598
|
+
hasTilt: false,
|
|
224599
|
+
hasZoom: false,
|
|
224600
|
+
supportsPresets: false,
|
|
224601
|
+
hasAutofocus: false
|
|
224602
|
+
};
|
|
224603
|
+
const hasAutofocus = this.config.get("deviceCache")?.autoFocusSnapshot?.supported === true;
|
|
224604
|
+
return this.resolveCapOptions({
|
|
224605
|
+
capName: "ptz",
|
|
224606
|
+
schema: PtzOptionsSchema,
|
|
224607
|
+
probe: async () => {
|
|
224608
|
+
const { capabilities } = await (await this.ensureApi()).getDeviceCapabilities(this.getChannel());
|
|
224609
|
+
return {
|
|
224610
|
+
hasPan: capabilities.hasPan,
|
|
224611
|
+
hasTilt: capabilities.hasTilt,
|
|
224612
|
+
hasZoom: capabilities.hasZoom,
|
|
224613
|
+
supportsPresets: capabilities.hasPresets,
|
|
224614
|
+
hasAutofocus
|
|
224615
|
+
};
|
|
224616
|
+
},
|
|
224617
|
+
fallback: () => {
|
|
224618
|
+
const hasPtz = this.getProbeFlags().hasPtz === true;
|
|
224619
|
+
return {
|
|
224620
|
+
hasPan: hasPtz,
|
|
224621
|
+
hasTilt: hasPtz,
|
|
224622
|
+
hasZoom: hasPtz,
|
|
224623
|
+
supportsPresets: hasPtz,
|
|
224624
|
+
hasAutofocus
|
|
224625
|
+
};
|
|
224538
224626
|
}
|
|
224539
|
-
}
|
|
224540
|
-
}
|
|
224541
|
-
|
|
224542
|
-
|
|
224627
|
+
});
|
|
224628
|
+
},
|
|
224629
|
+
goHome: async ({ deviceId }) => {
|
|
224630
|
+
if (deviceId !== this.id) return;
|
|
224631
|
+
try {
|
|
224632
|
+
const api = await this.ensureApi();
|
|
224633
|
+
const channel = this.getChannel();
|
|
224634
|
+
await api.moveToPtzPreset(channel, 0);
|
|
224635
|
+
} catch {}
|
|
224636
|
+
},
|
|
224637
|
+
getPosition: async ({ deviceId }) => {
|
|
224638
|
+
if (deviceId !== this.id) return {
|
|
224639
|
+
pan: 0,
|
|
224640
|
+
tilt: 0,
|
|
224641
|
+
zoom: 0
|
|
224642
|
+
};
|
|
224643
|
+
return {
|
|
224644
|
+
pan: 0,
|
|
224645
|
+
tilt: 0,
|
|
224646
|
+
zoom: 0
|
|
224647
|
+
};
|
|
224648
|
+
},
|
|
224649
|
+
getStatus: async ({ deviceId }) => {
|
|
224650
|
+
if (deviceId !== this.id) return {
|
|
224651
|
+
pan: 0,
|
|
224652
|
+
tilt: 0,
|
|
224653
|
+
zoom: 0,
|
|
224654
|
+
autofocus: false
|
|
224655
|
+
};
|
|
224656
|
+
return {
|
|
224657
|
+
pan: 0,
|
|
224658
|
+
tilt: 0,
|
|
224659
|
+
zoom: 0,
|
|
224660
|
+
autofocus: (this.config.get("deviceCache")?.autoFocusSnapshot)?.enabled === true
|
|
224661
|
+
};
|
|
224662
|
+
},
|
|
224663
|
+
setAutofocus: async ({ deviceId, enabled }) => {
|
|
224664
|
+
if (deviceId !== this.id) return;
|
|
224665
|
+
const api = await this.ensureApi();
|
|
224666
|
+
const channel = this.getChannel();
|
|
224667
|
+
await api.setAutoFocus(channel, enabled ? 0 : 1);
|
|
224668
|
+
try {
|
|
224669
|
+
const a = (await api.getAutoFocus(channel, { timeoutMs: 1500 }))?.body?.AutoFocus;
|
|
224670
|
+
if (a) await this.config.setAll({ deviceCache: {
|
|
224671
|
+
...this.config.get("deviceCache"),
|
|
224672
|
+
autoFocusSnapshot: {
|
|
224673
|
+
enabled: typeof a.disable === "number" ? a.disable === 0 : null,
|
|
224674
|
+
supported: true
|
|
224675
|
+
}
|
|
224676
|
+
} });
|
|
224677
|
+
} catch {}
|
|
224678
|
+
}
|
|
224679
|
+
};
|
|
224680
|
+
this.registerCapWarmer("ptz", async () => {
|
|
224681
|
+
await ptzProvider.getOptions({ deviceId: this.id });
|
|
224682
|
+
});
|
|
224683
|
+
this.ctx.registerNativeCap(ptzCapability, ptzProvider);
|
|
224684
|
+
}
|
|
224543
224685
|
}
|
|
224544
224686
|
/**
|
|
224545
224687
|
* Translate normalized (-1..1) pan/tilt/zoom to one or more discrete
|
|
@@ -225242,6 +225384,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
225242
225384
|
if (this.batteryUpdateInFlight) return this.batteryUpdateInFlight;
|
|
225243
225385
|
const cycle = (async () => {
|
|
225244
225386
|
try {
|
|
225387
|
+
if (!this.allowProactiveCameraAccess("battery-update")) {
|
|
225388
|
+
this.ctx.logger.debug("battery-update: cam sleeping — skipping cycle without connecting", { tags: { deviceId: this.id } });
|
|
225389
|
+
return;
|
|
225390
|
+
}
|
|
225245
225391
|
let api;
|
|
225246
225392
|
try {
|
|
225247
225393
|
api = await this.ensureApi();
|
|
@@ -225340,6 +225486,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
225340
225486
|
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
225341
225487
|
});
|
|
225342
225488
|
}
|
|
225489
|
+
await this.warmCapSlicesOnWake();
|
|
225490
|
+
await this.warmSnapshotOnWake();
|
|
225343
225491
|
await this.alignAuxDevicesState("wake");
|
|
225344
225492
|
await this.refreshParentSettingsSnapshot();
|
|
225345
225493
|
}
|
|
@@ -227093,6 +227241,13 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
227093
227241
|
}
|
|
227094
227242
|
if (this.api) return this.api;
|
|
227095
227243
|
if (this.loginPromise) return this.loginPromise;
|
|
227244
|
+
if (this.isBattery && this.sleeping) {
|
|
227245
|
+
const frames = (/* @__PURE__ */ new Error("wake-attribution")).stack?.split("\n").slice(2, 7).join(" | ");
|
|
227246
|
+
this.ctx.logger.info("battery cam login while sleeping — this wakes the camera", {
|
|
227247
|
+
tags: { deviceId: this.id },
|
|
227248
|
+
meta: { caller: frames ?? "unavailable" }
|
|
227249
|
+
});
|
|
227250
|
+
}
|
|
227096
227251
|
const host = this.config.get("host");
|
|
227097
227252
|
const port = this.config.get("port");
|
|
227098
227253
|
const username = this.config.get("username");
|