@camstack/addon-provider-reolink 1.2.183 → 1.2.184
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 +447 -146
- package/dist/addon.mjs +446 -145
- package/package.json +1 -1
package/dist/addon.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import * as crypto$7 from "crypto";
|
|
|
10
10
|
import crypto$1, { createHash as createHash$1, randomUUID } from "crypto";
|
|
11
11
|
import { execFile, spawn } from "child_process";
|
|
12
12
|
import { createServer } from "node:http";
|
|
13
|
+
import "node:zlib";
|
|
13
14
|
import { mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
14
15
|
import { join as join$1 } from "node:path";
|
|
15
16
|
import { PassThrough } from "stream";
|
|
@@ -27,7 +28,7 @@ import { URL as URL$1 } from "url";
|
|
|
27
28
|
import { spawn as spawn$1 } from "node:child_process";
|
|
28
29
|
import os, { tmpdir } from "node:os";
|
|
29
30
|
import { createReadStream } from "node:fs";
|
|
30
|
-
//#region ../types/dist/event-category-
|
|
31
|
+
//#region ../types/dist/event-category-C4hVnRo8.mjs
|
|
31
32
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
32
33
|
EventCategory["SystemBoot"] = "system.boot";
|
|
33
34
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -246,7 +247,17 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
246
247
|
EventCategory["RecordingStopped"] = "recording.stopped";
|
|
247
248
|
EventCategory["RecordingError"] = "recording.error";
|
|
248
249
|
EventCategory["RecordingHealthDegraded"] = "recording.health.degraded";
|
|
250
|
+
/** Recording enabled but no segment finalized for the NC stall window. */
|
|
251
|
+
EventCategory["RecordingHealthStalled"] = "recording.health.stalled";
|
|
252
|
+
/** Recovery from {@link RecordingHealthStalled}. */
|
|
253
|
+
EventCategory["RecordingHealthOk"] = "recording.health.ok";
|
|
249
254
|
EventCategory["RecordingStorageCritical"] = "recording.storage.critical";
|
|
255
|
+
/** Free space on a storage location crossed the warn bar. */
|
|
256
|
+
EventCategory["StorageLocationLow"] = "storage.location.low";
|
|
257
|
+
/** Free space at/under the critical bar, or capacity probe failed (`null`). */
|
|
258
|
+
EventCategory["StorageLocationCritical"] = "storage.location.critical";
|
|
259
|
+
/** Recovery from low/critical on a storage location. */
|
|
260
|
+
EventCategory["StorageLocationOk"] = "storage.location.ok";
|
|
250
261
|
EventCategory["RecordingSegmentWritten"] = "recording.segment.written";
|
|
251
262
|
EventCategory["RecordingPolicyFallback"] = "recording.policy.fallback";
|
|
252
263
|
EventCategory["RecordingRetentionCompleted"] = "recording.retention.completed";
|
|
@@ -5405,7 +5416,7 @@ var ZodIssueCode = {
|
|
|
5405
5416
|
var ZodFirstPartyTypeKind;
|
|
5406
5417
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5407
5418
|
//#endregion
|
|
5408
|
-
//#region ../types/dist/sleep-
|
|
5419
|
+
//#region ../types/dist/sleep-vWTnHq9G.mjs
|
|
5409
5420
|
/**
|
|
5410
5421
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5411
5422
|
* window to float samples (D455).
|
|
@@ -8010,6 +8021,21 @@ var ProfileRtspEntrySchema = object({
|
|
|
8010
8021
|
codec: string().optional(),
|
|
8011
8022
|
resolution: CamStreamResolutionSchema.optional()
|
|
8012
8023
|
});
|
|
8024
|
+
/**
|
|
8025
|
+
* The model weight formats a catalog entry can ship. Schema-free on purpose:
|
|
8026
|
+
* `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
|
|
8027
|
+
* (the model downloader in `@camstack/system`) needs only the list and must
|
|
8028
|
+
* reach it through `@camstack/types/addon` without building the cap schema
|
|
8029
|
+
* graph (D28).
|
|
8030
|
+
*/
|
|
8031
|
+
var MODEL_FORMATS = [
|
|
8032
|
+
"onnx",
|
|
8033
|
+
"coreml",
|
|
8034
|
+
"openvino",
|
|
8035
|
+
"tflite",
|
|
8036
|
+
"pt",
|
|
8037
|
+
"gguf"
|
|
8038
|
+
];
|
|
8013
8039
|
object({
|
|
8014
8040
|
field: string().min(1),
|
|
8015
8041
|
on: literal("advance")
|
|
@@ -8608,7 +8634,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
|
|
|
8608
8634
|
return DeviceRole;
|
|
8609
8635
|
}({});
|
|
8610
8636
|
//#endregion
|
|
8611
|
-
//#region ../types/dist/composition-
|
|
8637
|
+
//#region ../types/dist/composition-C-WpVYTX.mjs
|
|
8612
8638
|
/**
|
|
8613
8639
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
8614
8640
|
*
|
|
@@ -8660,7 +8686,7 @@ var BASE = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFM
|
|
|
8660
8686
|
function portable(target) {
|
|
8661
8687
|
return `${BASE}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
|
|
8662
8688
|
}
|
|
8663
|
-
portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac64")
|
|
8689
|
+
portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac64"), `${BASE}${FFMPEG_RELEASE}`;
|
|
8664
8690
|
/**
|
|
8665
8691
|
* THE table "which boolean of this device type counts as HIGH", and the
|
|
8666
8692
|
* pure evaluator of its EDGE.
|
|
@@ -12404,14 +12430,6 @@ var MASK_SUPPLY_STEP_IDS = ["segmentation"];
|
|
|
12404
12430
|
...CROP_SUPPLY_STEP_IDS,
|
|
12405
12431
|
...MASK_SUPPLY_STEP_IDS
|
|
12406
12432
|
];
|
|
12407
|
-
var MODEL_FORMATS = [
|
|
12408
|
-
"onnx",
|
|
12409
|
-
"coreml",
|
|
12410
|
-
"openvino",
|
|
12411
|
-
"tflite",
|
|
12412
|
-
"pt",
|
|
12413
|
-
"gguf"
|
|
12414
|
-
];
|
|
12415
12433
|
/**
|
|
12416
12434
|
* Multi-file format payload.
|
|
12417
12435
|
*
|
|
@@ -14235,7 +14253,14 @@ var BrokerProviderInfoSchema = object({
|
|
|
14235
14253
|
/** Broker kinds this provider can create, with a display label. */
|
|
14236
14254
|
kinds: array(object({
|
|
14237
14255
|
kind: string(),
|
|
14238
|
-
label: string()
|
|
14256
|
+
label: string(),
|
|
14257
|
+
/**
|
|
14258
|
+
* Relative hub path to start an interactive account link (OAuth popup).
|
|
14259
|
+
* May include `{{brokerId}}`; admin-ui substitutes and appends `?popup=1`.
|
|
14260
|
+
*/
|
|
14261
|
+
interactiveConnectPath: string().min(1).optional(),
|
|
14262
|
+
/** Relative hub path to disconnect the linked account (same popup pattern). */
|
|
14263
|
+
interactiveDisconnectPath: string().min(1).optional()
|
|
14239
14264
|
}))
|
|
14240
14265
|
});
|
|
14241
14266
|
var ListInputSchema = object({
|
|
@@ -21329,6 +21354,12 @@ var NcSystemEventKindSchema = _enum([
|
|
|
21329
21354
|
"node-updating",
|
|
21330
21355
|
"node-inference-unavailable",
|
|
21331
21356
|
"detection-blind",
|
|
21357
|
+
"recording-stalled",
|
|
21358
|
+
"recording-healthy",
|
|
21359
|
+
"recording-degraded",
|
|
21360
|
+
"storage-location-low",
|
|
21361
|
+
"storage-location-critical",
|
|
21362
|
+
"storage-location-ok",
|
|
21332
21363
|
"addon-crash-loop",
|
|
21333
21364
|
"addon-update-available",
|
|
21334
21365
|
"server-update-available",
|
|
@@ -33428,6 +33459,17 @@ var BatteryStatusSchema = object({
|
|
|
33428
33459
|
*/
|
|
33429
33460
|
lastContactAt: number().optional(),
|
|
33430
33461
|
/**
|
|
33462
|
+
* Ms epoch of the last COMPLETE scheduled refresh — every fact the provider
|
|
33463
|
+
* holds for the device (level, charging, link, device info, state slices) and
|
|
33464
|
+
* its cached snapshot, read together. Absent = never since this slice was
|
|
33465
|
+
* born. Unlike `lastUpdated` (any write, even a push of one field) this is
|
|
33466
|
+
* the staleness an operator can trust: the provider owns the device fact and
|
|
33467
|
+
* so owns the window on its age (D224, D847). Not a clock in the
|
|
33468
|
+
* runtime-state-durability sense: it moves once per window and must survive
|
|
33469
|
+
* a restart, or every restart would owe the camera a wake.
|
|
33470
|
+
*/
|
|
33471
|
+
lastFullRefreshAt: number().optional(),
|
|
33472
|
+
/**
|
|
33431
33473
|
* True when the source is a BINARY low-battery indicator (HA
|
|
33432
33474
|
* `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
|
|
33433
33475
|
* charge level — `percentage` is then a coarse stand-in (100 = normal,
|
|
@@ -46462,17 +46504,6 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
46462
46504
|
return toDeviceSummary(device, this.addonId);
|
|
46463
46505
|
}
|
|
46464
46506
|
};
|
|
46465
|
-
/**
|
|
46466
|
-
* Default silence budget before a sleeping battery camera is called gone.
|
|
46467
|
-
*
|
|
46468
|
-
* Sized off the mechanism that produces contact, not off taste: a battery cam
|
|
46469
|
-
* on this deployment surfaces a firmware push (battery level, sleep/wake, or a
|
|
46470
|
-
* motion email) on the order of hours even with no visitors, and the snapshot
|
|
46471
|
-
* wrapper's own battery window is 1 hour. Six hours is therefore several
|
|
46472
|
-
* missed opportunities, not one — so a single quiet night does not raise a
|
|
46473
|
-
* fault, and a genuinely flat camera is named well inside a day.
|
|
46474
|
-
*/
|
|
46475
|
-
var BATTERY_UNREACHABLE_AFTER_MS = 360 * 6e4;
|
|
46476
46507
|
DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
|
|
46477
46508
|
new Set(Object.values(DeviceType));
|
|
46478
46509
|
Object.freeze({
|
|
@@ -58480,6 +58511,9 @@ new Map(Object.entries({
|
|
|
58480
58511
|
"stream-online": "device-events",
|
|
58481
58512
|
"stream-offline": "device-events",
|
|
58482
58513
|
"detection-blind": "device-events",
|
|
58514
|
+
"recording-stalled": "device-events",
|
|
58515
|
+
"recording-healthy": "device-events",
|
|
58516
|
+
"recording-degraded": "device-events",
|
|
58483
58517
|
"node-online": "system",
|
|
58484
58518
|
"node-offline": "system",
|
|
58485
58519
|
"node-restarting": "system",
|
|
@@ -58493,6 +58527,9 @@ new Map(Object.entries({
|
|
|
58493
58527
|
"wrapper-update-available": "system",
|
|
58494
58528
|
"backup-completed": "system",
|
|
58495
58529
|
"export-completed": "system",
|
|
58530
|
+
"storage-location-low": "system",
|
|
58531
|
+
"storage-location-critical": "system",
|
|
58532
|
+
"storage-location-ok": "system",
|
|
58496
58533
|
"alarm-triggered": "alarm",
|
|
58497
58534
|
"alarm-armed": "alarm",
|
|
58498
58535
|
"alarm-disarmed": "alarm",
|
|
@@ -58554,6 +58591,16 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
58554
58591
|
label: "Detecting nothing",
|
|
58555
58592
|
kinds: ["detection-blind"]
|
|
58556
58593
|
},
|
|
58594
|
+
{
|
|
58595
|
+
id: "recording",
|
|
58596
|
+
family: "device-events",
|
|
58597
|
+
label: "Recording",
|
|
58598
|
+
kinds: [
|
|
58599
|
+
"recording-stalled",
|
|
58600
|
+
"recording-healthy",
|
|
58601
|
+
"recording-degraded"
|
|
58602
|
+
]
|
|
58603
|
+
},
|
|
58557
58604
|
{
|
|
58558
58605
|
id: "nodes",
|
|
58559
58606
|
family: "system",
|
|
@@ -58594,6 +58641,16 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
58594
58641
|
label: "Backups & exports",
|
|
58595
58642
|
kinds: ["backup-completed", "export-completed"]
|
|
58596
58643
|
},
|
|
58644
|
+
{
|
|
58645
|
+
id: "storage",
|
|
58646
|
+
family: "system",
|
|
58647
|
+
label: "Storage locations",
|
|
58648
|
+
kinds: [
|
|
58649
|
+
"storage-location-low",
|
|
58650
|
+
"storage-location-critical",
|
|
58651
|
+
"storage-location-ok"
|
|
58652
|
+
]
|
|
58653
|
+
},
|
|
58597
58654
|
{
|
|
58598
58655
|
id: "alarm",
|
|
58599
58656
|
family: "alarm",
|
|
@@ -58676,6 +58733,9 @@ var DEVICE_EVENTS = [
|
|
|
58676
58733
|
"stream-online",
|
|
58677
58734
|
"stream-offline",
|
|
58678
58735
|
"detection-blind",
|
|
58736
|
+
"recording-stalled",
|
|
58737
|
+
"recording-healthy",
|
|
58738
|
+
"recording-degraded",
|
|
58679
58739
|
...ALARM
|
|
58680
58740
|
];
|
|
58681
58741
|
/** Every kind a summary may speak about — the two summarisable families. */
|
|
@@ -58843,6 +58903,11 @@ var STATE_SUMMARY_VARS = [
|
|
|
58843
58903
|
[...RULE_TEXT],
|
|
58844
58904
|
[...RULE_TEXT],
|
|
58845
58905
|
[...RULE_TEXT],
|
|
58906
|
+
[...RULE_TEXT],
|
|
58907
|
+
[...RULE_TEXT],
|
|
58908
|
+
[...RULE_TEXT],
|
|
58909
|
+
[...RULE_TEXT],
|
|
58910
|
+
[...RULE_TEXT],
|
|
58846
58911
|
([...RULE_TEXT], [...UPDATES]),
|
|
58847
58912
|
([...RULE_TEXT], [...PACKAGE_UPDATES]),
|
|
58848
58913
|
([...RULE_TEXT], [...PACKAGE_UPDATES]),
|
|
@@ -71514,7 +71579,7 @@ var require_snapshot_utils = /* @__PURE__ */ __commonJSMin(((exports, module) =>
|
|
|
71514
71579
|
//#region ../../node_modules/undici/lib/mock/snapshot-recorder.js
|
|
71515
71580
|
var require_snapshot_recorder = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
71516
71581
|
var { writeFile: writeFile$1, readFile: readFile$1, mkdir: mkdir$2 } = __require("node:fs/promises");
|
|
71517
|
-
var { dirname: dirname$
|
|
71582
|
+
var { dirname: dirname$2, resolve: resolve$1 } = __require("node:path");
|
|
71518
71583
|
var { setTimeout: setTimeout$1, clearTimeout: clearTimeout$1 } = __require("node:timers");
|
|
71519
71584
|
var { InvalidArgumentError, UndiciError } = require_errors$1();
|
|
71520
71585
|
var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
|
|
@@ -71779,7 +71844,7 @@ var require_snapshot_recorder = /* @__PURE__ */ __commonJSMin(((exports, module)
|
|
|
71779
71844
|
const path = filePath || this.#snapshotPath;
|
|
71780
71845
|
if (!path) throw new InvalidArgumentError("Snapshot path is required");
|
|
71781
71846
|
try {
|
|
71782
|
-
const data = await readFile$1(resolve(path), "utf8");
|
|
71847
|
+
const data = await readFile$1(resolve$1(path), "utf8");
|
|
71783
71848
|
const parsed = JSON.parse(data);
|
|
71784
71849
|
if (Array.isArray(parsed)) {
|
|
71785
71850
|
this.#snapshots.clear();
|
|
@@ -71799,8 +71864,8 @@ var require_snapshot_recorder = /* @__PURE__ */ __commonJSMin(((exports, module)
|
|
|
71799
71864
|
async saveSnapshots(filePath) {
|
|
71800
71865
|
const path = filePath || this.#snapshotPath;
|
|
71801
71866
|
if (!path) throw new InvalidArgumentError("Snapshot path is required");
|
|
71802
|
-
const resolvedPath = resolve(path);
|
|
71803
|
-
await mkdir$2(dirname$
|
|
71867
|
+
const resolvedPath = resolve$1(path);
|
|
71868
|
+
await mkdir$2(dirname$2(resolvedPath), { recursive: true });
|
|
71804
71869
|
const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({
|
|
71805
71870
|
hash,
|
|
71806
71871
|
snapshot
|
|
@@ -160437,6 +160502,7 @@ function buildInitialStatus2(config, enabled) {
|
|
|
160437
160502
|
lastErrorMessage: void 0
|
|
160438
160503
|
};
|
|
160439
160504
|
}
|
|
160505
|
+
process.platform, process.env["SystemRoot"];
|
|
160440
160506
|
/**
|
|
160441
160507
|
* The spawn a one-shot ffmpeg site uses: the spec's seam when one is injected,
|
|
160442
160508
|
* else the node's PINNED binary (D540), else `null` — and the site refuses by
|
|
@@ -161009,6 +161075,13 @@ var reolinkCameraSchema = object({
|
|
|
161009
161075
|
*/
|
|
161010
161076
|
clipCatalogForcedWake: boolean().optional(),
|
|
161011
161077
|
/**
|
|
161078
|
+
* Battery cameras only: the window, in minutes, in which the camera refreshes
|
|
161079
|
+
* ALL its metadata and its cached snapshot exactly once (D847). Absent means
|
|
161080
|
+
* the default (60); `0` turns the scheduled refresh off for this camera; a
|
|
161081
|
+
* value is clamped to 15..240 (below the 6 h presence budget).
|
|
161082
|
+
*/
|
|
161083
|
+
batteryRefreshMinutes: number().int().min(0).max(240).optional(),
|
|
161084
|
+
/**
|
|
161012
161085
|
* Per-AI-class sensitivity overrides — pushed via
|
|
161013
161086
|
* `setAiDetection(aiType, sensitivity)` (cmdId=343). Reolink
|
|
161014
161087
|
* range 0..100 — same scale the reolink_aio CGI exposes
|
|
@@ -167266,14 +167339,266 @@ function deriveChargingSource(adapterStatus, chargeStatus) {
|
|
|
167266
167339
|
function reportsPowerSource(info) {
|
|
167267
167340
|
return info.adapterStatus !== void 0 || info.chargeStatus !== void 0;
|
|
167268
167341
|
}
|
|
167342
|
+
/** The settings key, per device. */
|
|
167343
|
+
var BATTERY_REFRESH_MINUTES_KEY = "batteryRefreshMinutes";
|
|
167344
|
+
/** How often the in-memory check runs. Costs nothing: no I/O unless it fires. */
|
|
167345
|
+
var BATTERY_REFRESH_TICK_MS = 6e4;
|
|
167346
|
+
/** The least time between two attempts in one window. */
|
|
167347
|
+
var RETRY_BACKOFF_MS = 10 * 6e4;
|
|
167348
|
+
var MAX_WAKE_LEAD_MS = 5 * 6e4;
|
|
167349
|
+
var STAGGER_SLOTS = 60;
|
|
167350
|
+
/** The operator's window in ms, or `0` for off. Out-of-range values clamp. */
|
|
167351
|
+
function resolveBatteryRefreshIntervalMs(configured) {
|
|
167352
|
+
if (typeof configured !== "number" || !Number.isFinite(configured)) return 60 * 6e4;
|
|
167353
|
+
if (configured <= 0) return 0;
|
|
167354
|
+
return Math.min(240, Math.max(15, Math.round(configured))) * 6e4;
|
|
167355
|
+
}
|
|
167356
|
+
/** Deterministic per-device offset so a fleet's windows do not align. */
|
|
167357
|
+
function staggerOffsetMs(deviceId, intervalMs) {
|
|
167358
|
+
const slot = Math.abs(Math.trunc(deviceId)) % STAGGER_SLOTS;
|
|
167359
|
+
return Math.floor(intervalMs / STAGGER_SLOTS * slot);
|
|
167360
|
+
}
|
|
167361
|
+
/** Which window a wall-clock instant falls in. */
|
|
167362
|
+
function windowIndex(at, intervalMs, offsetMs) {
|
|
167363
|
+
return Math.floor((at - offsetMs) / intervalMs);
|
|
167364
|
+
}
|
|
167365
|
+
/** How long before a window's end the wake is allowed to fire. */
|
|
167366
|
+
function wakeLeadMs(intervalMs) {
|
|
167367
|
+
return Math.min(MAX_WAKE_LEAD_MS, Math.floor(intervalMs / 10));
|
|
167368
|
+
}
|
|
167369
|
+
function decideBatteryRefresh(input) {
|
|
167370
|
+
if (input.intervalMs <= 0) return {
|
|
167371
|
+
kind: "skip",
|
|
167372
|
+
reason: "off"
|
|
167373
|
+
};
|
|
167374
|
+
const current = windowIndex(input.nowMs, input.intervalMs, input.offsetMs);
|
|
167375
|
+
if ((typeof input.lastFullRefreshAt === "number" && input.lastFullRefreshAt > 0 ? windowIndex(input.lastFullRefreshAt, input.intervalMs, input.offsetMs) : Number.NEGATIVE_INFINITY) >= current) return {
|
|
167376
|
+
kind: "skip",
|
|
167377
|
+
reason: "done"
|
|
167378
|
+
};
|
|
167379
|
+
if (input.attemptsInWindow >= 2) return {
|
|
167380
|
+
kind: "skip",
|
|
167381
|
+
reason: "attempts-exhausted"
|
|
167382
|
+
};
|
|
167383
|
+
if (input.lastAttemptAt !== void 0 && input.attemptsInWindow > 0 && input.nowMs - input.lastAttemptAt < 6e5) return {
|
|
167384
|
+
kind: "skip",
|
|
167385
|
+
reason: "backoff"
|
|
167386
|
+
};
|
|
167387
|
+
if (input.awake) return { kind: "refresh-awake" };
|
|
167388
|
+
const windowEnd = input.offsetMs + (current + 1) * input.intervalMs;
|
|
167389
|
+
if (input.nowMs < windowEnd - wakeLeadMs(input.intervalMs)) return {
|
|
167390
|
+
kind: "skip",
|
|
167391
|
+
reason: "asleep-before-deadline"
|
|
167392
|
+
};
|
|
167393
|
+
return { kind: "wake-then-refresh" };
|
|
167394
|
+
}
|
|
167395
|
+
var BatteryHourlyRefresh = class {
|
|
167396
|
+
deps;
|
|
167397
|
+
inFlight = null;
|
|
167398
|
+
attemptWindow = Number.NEGATIVE_INFINITY;
|
|
167399
|
+
attempts = 0;
|
|
167400
|
+
lastAttemptAt;
|
|
167401
|
+
constructor(deps) {
|
|
167402
|
+
this.deps = deps;
|
|
167403
|
+
}
|
|
167404
|
+
/**
|
|
167405
|
+
* The periodic check. Resolves `true` when a refresh landed in this call.
|
|
167406
|
+
* Never throws.
|
|
167407
|
+
*/
|
|
167408
|
+
async tick() {
|
|
167409
|
+
return this.consider("tick");
|
|
167410
|
+
}
|
|
167411
|
+
/**
|
|
167412
|
+
* The camera was just observed coming up (event, sleep poll, hub summary, a
|
|
167413
|
+
* confirmed wake). Resolves `true` when this call refreshed everything, so
|
|
167414
|
+
* the caller can skip the parts of its own fan-out the refresh already did.
|
|
167415
|
+
*/
|
|
167416
|
+
async noteAwake() {
|
|
167417
|
+
return this.consider("wake-transition");
|
|
167418
|
+
}
|
|
167419
|
+
async consider(trigger) {
|
|
167420
|
+
if (this.inFlight !== null) return this.inFlight;
|
|
167421
|
+
const nowMs = this.deps.now();
|
|
167422
|
+
const intervalMs = this.deps.intervalMs();
|
|
167423
|
+
const offsetMs = intervalMs > 0 ? staggerOffsetMs(this.deps.deviceId, intervalMs) : 0;
|
|
167424
|
+
const window = intervalMs > 0 ? windowIndex(nowMs, intervalMs, offsetMs) : 0;
|
|
167425
|
+
if (window !== this.attemptWindow) {
|
|
167426
|
+
this.attemptWindow = window;
|
|
167427
|
+
this.attempts = 0;
|
|
167428
|
+
this.lastAttemptAt = void 0;
|
|
167429
|
+
}
|
|
167430
|
+
const decision = decideBatteryRefresh({
|
|
167431
|
+
nowMs,
|
|
167432
|
+
intervalMs,
|
|
167433
|
+
offsetMs,
|
|
167434
|
+
lastFullRefreshAt: this.deps.lastFullRefreshAt(),
|
|
167435
|
+
awake: trigger === "wake-transition" || this.deps.isAwake(),
|
|
167436
|
+
attemptsInWindow: this.attempts,
|
|
167437
|
+
lastAttemptAt: this.lastAttemptAt
|
|
167438
|
+
});
|
|
167439
|
+
if (decision.kind === "skip") {
|
|
167440
|
+
this.logSkip(decision.reason, trigger);
|
|
167441
|
+
return false;
|
|
167442
|
+
}
|
|
167443
|
+
this.attempts += 1;
|
|
167444
|
+
this.lastAttemptAt = nowMs;
|
|
167445
|
+
const run = this.run(decision.kind, trigger).finally(() => {
|
|
167446
|
+
this.inFlight = null;
|
|
167447
|
+
});
|
|
167448
|
+
this.inFlight = run;
|
|
167449
|
+
return run;
|
|
167450
|
+
}
|
|
167451
|
+
async run(kind, trigger) {
|
|
167452
|
+
const { logger, deviceId } = this.deps;
|
|
167453
|
+
try {
|
|
167454
|
+
if (kind === "wake-then-refresh") {
|
|
167455
|
+
logger.info("battery hourly refresh: not awake in the window — waking once", {
|
|
167456
|
+
tags: { deviceId },
|
|
167457
|
+
meta: { attempt: this.attempts }
|
|
167458
|
+
});
|
|
167459
|
+
if (!await this.deps.wake()) {
|
|
167460
|
+
logger.warn("battery hourly refresh: the wake did not bring the camera up", {
|
|
167461
|
+
tags: { deviceId },
|
|
167462
|
+
meta: {
|
|
167463
|
+
attempt: this.attempts,
|
|
167464
|
+
retryAfterMs: RETRY_BACKOFF_MS
|
|
167465
|
+
}
|
|
167466
|
+
});
|
|
167467
|
+
return false;
|
|
167468
|
+
}
|
|
167469
|
+
if (this.windowSatisfied()) return true;
|
|
167470
|
+
} else logger.info("battery hourly refresh: camera is awake — refreshing now, no wake", {
|
|
167471
|
+
tags: { deviceId },
|
|
167472
|
+
meta: {
|
|
167473
|
+
trigger,
|
|
167474
|
+
attempt: this.attempts
|
|
167475
|
+
}
|
|
167476
|
+
});
|
|
167477
|
+
const outcome = await this.deps.refreshAll();
|
|
167478
|
+
if (outcome.failed.length > 0) {
|
|
167479
|
+
logger.warn("battery hourly refresh incomplete — not stamped, retried at the next chance", {
|
|
167480
|
+
tags: { deviceId },
|
|
167481
|
+
meta: {
|
|
167482
|
+
failed: outcome.failed,
|
|
167483
|
+
attempt: this.attempts,
|
|
167484
|
+
retryAfterMs: RETRY_BACKOFF_MS
|
|
167485
|
+
}
|
|
167486
|
+
});
|
|
167487
|
+
return false;
|
|
167488
|
+
}
|
|
167489
|
+
const at = this.deps.now();
|
|
167490
|
+
this.deps.stamp(at);
|
|
167491
|
+
logger.info("battery hourly refresh complete", {
|
|
167492
|
+
tags: { deviceId },
|
|
167493
|
+
meta: {
|
|
167494
|
+
kind,
|
|
167495
|
+
trigger,
|
|
167496
|
+
at
|
|
167497
|
+
}
|
|
167498
|
+
});
|
|
167499
|
+
return true;
|
|
167500
|
+
} catch (err) {
|
|
167501
|
+
logger.warn("battery hourly refresh failed — retried at the next chance", {
|
|
167502
|
+
tags: { deviceId },
|
|
167503
|
+
meta: {
|
|
167504
|
+
error: err instanceof Error ? err.message : String(err),
|
|
167505
|
+
attempt: this.attempts,
|
|
167506
|
+
retryAfterMs: RETRY_BACKOFF_MS
|
|
167507
|
+
}
|
|
167508
|
+
});
|
|
167509
|
+
return false;
|
|
167510
|
+
}
|
|
167511
|
+
}
|
|
167512
|
+
windowSatisfied() {
|
|
167513
|
+
const intervalMs = this.deps.intervalMs();
|
|
167514
|
+
if (intervalMs <= 0) return false;
|
|
167515
|
+
const offsetMs = staggerOffsetMs(this.deps.deviceId, intervalMs);
|
|
167516
|
+
const last = this.deps.lastFullRefreshAt();
|
|
167517
|
+
if (typeof last !== "number" || last <= 0) return false;
|
|
167518
|
+
return windowIndex(last, intervalMs, offsetMs) >= windowIndex(this.deps.now(), intervalMs, offsetMs);
|
|
167519
|
+
}
|
|
167520
|
+
logSkip(reason, trigger) {
|
|
167521
|
+
if (reason === "backoff" || reason === "attempts-exhausted") {
|
|
167522
|
+
this.deps.logger.info("battery hourly refresh skipped", {
|
|
167523
|
+
tags: { deviceId: this.deps.deviceId },
|
|
167524
|
+
meta: {
|
|
167525
|
+
reason,
|
|
167526
|
+
trigger,
|
|
167527
|
+
attempts: this.attempts
|
|
167528
|
+
}
|
|
167529
|
+
});
|
|
167530
|
+
return;
|
|
167531
|
+
}
|
|
167532
|
+
this.deps.logger.debug("battery hourly refresh not due", {
|
|
167533
|
+
tags: { deviceId: this.deps.deviceId },
|
|
167534
|
+
meta: {
|
|
167535
|
+
reason,
|
|
167536
|
+
trigger
|
|
167537
|
+
}
|
|
167538
|
+
});
|
|
167539
|
+
}
|
|
167540
|
+
};
|
|
167541
|
+
//#endregion
|
|
167542
|
+
//#region src/battery-refresh-settings.ts
|
|
167543
|
+
var BATTERY_REFRESH_SECTION_ID = "battery-refresh";
|
|
167544
|
+
var BATTERY_REFRESH_MINUTES_DESCRIPTION = `Once per window this camera refreshes everything CamStack knows about it — battery level and charging, signal, device info, every state panel and the cached snapshot. If the camera is awake at some point in the window (a motion event, a live view, anything that woke it) it is refreshed then and not woken again; if the whole window passes with the camera asleep, it is woken ONCE near the end of the window, refreshed, and left to go back to sleep. A camera that does not answer is retried at most twice per window. 15–240 minutes, default 60; 0 turns the scheduled refresh off (the battery level then ages until the camera next wakes by itself).`;
|
|
167545
|
+
/** Battery cameras only: a mains camera keeps its facts fresh on its own socket. */
|
|
167546
|
+
function buildBatteryRefreshDeviceSection(input) {
|
|
167547
|
+
if (!input.isBattery) return [];
|
|
167548
|
+
return [{
|
|
167549
|
+
id: BATTERY_REFRESH_SECTION_ID,
|
|
167550
|
+
tab: "general",
|
|
167551
|
+
title: "Scheduled refresh",
|
|
167552
|
+
description: "How often this battery camera is guaranteed to have been read, so a battery level never goes stale unseen.",
|
|
167553
|
+
columns: 1,
|
|
167554
|
+
fields: [{
|
|
167555
|
+
type: "number",
|
|
167556
|
+
key: BATTERY_REFRESH_MINUTES_KEY,
|
|
167557
|
+
label: "Refresh window (min)",
|
|
167558
|
+
description: BATTERY_REFRESH_MINUTES_DESCRIPTION,
|
|
167559
|
+
default: 60,
|
|
167560
|
+
min: 0,
|
|
167561
|
+
max: 240,
|
|
167562
|
+
step: 5
|
|
167563
|
+
}]
|
|
167564
|
+
}];
|
|
167565
|
+
}
|
|
167269
167566
|
//#endregion
|
|
167270
|
-
//#region src/battery-
|
|
167271
|
-
|
|
167272
|
-
|
|
167273
|
-
|
|
167274
|
-
const
|
|
167275
|
-
|
|
167276
|
-
|
|
167567
|
+
//#region src/battery-snapshot-refresh.ts
|
|
167568
|
+
/** The wrapper may run on another node; a stale frame is hours old, skew is seconds. */
|
|
167569
|
+
var CLOCK_SKEW_TOLERANCE_MS = 6e4;
|
|
167570
|
+
async function refreshSnapshotCacheOnce(input) {
|
|
167571
|
+
const { deviceId, api, logger } = input;
|
|
167572
|
+
const startedAt = input.now();
|
|
167573
|
+
if (await api.getSnapshot({
|
|
167574
|
+
deviceId,
|
|
167575
|
+
force: true
|
|
167576
|
+
}) === null) {
|
|
167577
|
+
logger.warn("battery hourly refresh: the snapshot came back empty", { tags: { deviceId } });
|
|
167578
|
+
return false;
|
|
167579
|
+
}
|
|
167580
|
+
let rows;
|
|
167581
|
+
try {
|
|
167582
|
+
rows = await api.getSnapshotOverview({ deviceIds: [deviceId] });
|
|
167583
|
+
} catch (err) {
|
|
167584
|
+
logger.debug("battery hourly refresh: snapshot overview unreadable — counting the image", {
|
|
167585
|
+
tags: { deviceId },
|
|
167586
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
167587
|
+
});
|
|
167588
|
+
return true;
|
|
167589
|
+
}
|
|
167590
|
+
const capturedAt = rows.find((row) => row.deviceId === deviceId)?.lastCapturedAt ?? null;
|
|
167591
|
+
if (capturedAt === null || capturedAt < startedAt - CLOCK_SKEW_TOLERANCE_MS) {
|
|
167592
|
+
logger.warn("battery hourly refresh: the snapshot cache did not move (a stale frame was served)", {
|
|
167593
|
+
tags: { deviceId },
|
|
167594
|
+
meta: {
|
|
167595
|
+
capturedAt,
|
|
167596
|
+
startedAt
|
|
167597
|
+
}
|
|
167598
|
+
});
|
|
167599
|
+
return false;
|
|
167600
|
+
}
|
|
167601
|
+
return true;
|
|
167277
167602
|
}
|
|
167278
167603
|
//#endregion
|
|
167279
167604
|
//#region src/log-channels.ts
|
|
@@ -171276,7 +171601,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
171276
171601
|
* passively checks sleep state, and short-circuits if sleeping. */
|
|
171277
171602
|
batteryUpdateTimer = null;
|
|
171278
171603
|
/** Single-flight gate so two interval fires can't overlap. */
|
|
171279
|
-
batteryUpdateInFlight = null;
|
|
171280
171604
|
/** Aux accessory references registered by spawned children
|
|
171281
171605
|
* (siren / floodlight / pir). The parent's centralised align
|
|
171282
171606
|
* method iterates these to refresh their slices on a single
|
|
@@ -171614,7 +171938,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
171614
171938
|
* snapshot also drives the dispatch's known-good baseline.
|
|
171615
171939
|
*/
|
|
171616
171940
|
async refreshParentSettingsSnapshot(slices, reason = "background") {
|
|
171617
|
-
if (this.isBattery && this.sleeping && reason !== "post-write") {
|
|
171941
|
+
if (this.isBattery && this.sleeping && reason !== "post-write" && reason !== "scheduled") {
|
|
171618
171942
|
this.ctx.logger.info("refreshParentSettingsSnapshot skipped — battery cam is sleeping", { tags: {
|
|
171619
171943
|
deviceId: this.id,
|
|
171620
171944
|
reason
|
|
@@ -172560,7 +172884,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
172560
172884
|
tags: { deviceId: this.id },
|
|
172561
172885
|
meta: { reason }
|
|
172562
172886
|
});
|
|
172563
|
-
return;
|
|
172887
|
+
return false;
|
|
172564
172888
|
}
|
|
172565
172889
|
let api;
|
|
172566
172890
|
try {
|
|
@@ -172570,17 +172894,19 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
172570
172894
|
tags: { deviceId: this.id },
|
|
172571
172895
|
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
172572
172896
|
});
|
|
172573
|
-
return;
|
|
172897
|
+
return false;
|
|
172574
172898
|
}
|
|
172575
172899
|
try {
|
|
172576
172900
|
const info = await api.getBatteryInfo(this.getChannel());
|
|
172577
172901
|
this.updateBatteryCache(info);
|
|
172578
172902
|
await this.refreshNetworkLinkFromApi(api, "battery");
|
|
172903
|
+
return true;
|
|
172579
172904
|
} catch (err) {
|
|
172580
172905
|
this.ctx.logger.debug("battery refresh failed", {
|
|
172581
172906
|
tags: { deviceId: this.id },
|
|
172582
172907
|
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
172583
172908
|
});
|
|
172909
|
+
return false;
|
|
172584
172910
|
}
|
|
172585
172911
|
}
|
|
172586
172912
|
/**
|
|
@@ -173826,6 +174152,18 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
173826
174152
|
* proactive gate lets it through because `sleeping` is false here.
|
|
173827
174153
|
* Mirrors the reference's `updateBatteryAndSnapshot`.
|
|
173828
174154
|
*/
|
|
174155
|
+
/** The scheduled refresh's snapshot step: true only when the cache really moved. */
|
|
174156
|
+
refreshSnapshotCache() {
|
|
174157
|
+
return refreshSnapshotCacheOnce({
|
|
174158
|
+
deviceId: this.id,
|
|
174159
|
+
logger: this.ctx.logger,
|
|
174160
|
+
now: () => Date.now(),
|
|
174161
|
+
api: {
|
|
174162
|
+
getSnapshot: (input) => this.ctx.api.snapshot.getSnapshot.query(input),
|
|
174163
|
+
getSnapshotOverview: (input) => this.ctx.api.snapshot.getSnapshotOverview.query({ deviceIds: [...input.deviceIds] })
|
|
174164
|
+
}
|
|
174165
|
+
});
|
|
174166
|
+
}
|
|
173829
174167
|
async warmSnapshotOnWake() {
|
|
173830
174168
|
try {
|
|
173831
174169
|
await this.ctx.api.snapshot.getSnapshot.query({
|
|
@@ -176261,119 +176599,84 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
176261
176599
|
this.alignAuxTimer = null;
|
|
176262
176600
|
}
|
|
176263
176601
|
}
|
|
176264
|
-
|
|
176265
|
-
|
|
176266
|
-
|
|
176267
|
-
|
|
176268
|
-
|
|
176602
|
+
batteryRefresh = null;
|
|
176603
|
+
getBatteryRefresh() {
|
|
176604
|
+
if (this.batteryRefresh === null) this.batteryRefresh = new BatteryHourlyRefresh({
|
|
176605
|
+
deviceId: this.id,
|
|
176606
|
+
logger: this.ctx.logger,
|
|
176607
|
+
now: () => Date.now(),
|
|
176608
|
+
isAwake: () => !this.sleeping,
|
|
176609
|
+
intervalMs: () => resolveBatteryRefreshIntervalMs(this.config.get("batteryRefreshMinutes")),
|
|
176610
|
+
lastFullRefreshAt: () => this.state.battery.lastFullRefreshAt,
|
|
176611
|
+
wake: async () => {
|
|
176612
|
+
if (!this.canProactivelyWake("scheduled-refresh")) return false;
|
|
176613
|
+
this.markWakeIssued();
|
|
176614
|
+
return this.wakeIfSleeping();
|
|
176615
|
+
},
|
|
176616
|
+
refreshAll: () => this.refreshEverythingNow(),
|
|
176617
|
+
stamp: (at) => {
|
|
176618
|
+
this.state.battery.lastFullRefreshAt = at;
|
|
176619
|
+
}
|
|
176620
|
+
});
|
|
176621
|
+
return this.batteryRefresh;
|
|
176622
|
+
}
|
|
176623
|
+
/**
|
|
176624
|
+
* Every fact and the snapshot, read while the camera is up. SERIALIZED (one
|
|
176625
|
+
* Baichuan round-trip at a time on the single BCUDP stream), and each step
|
|
176626
|
+
* names itself in `failed` instead of throwing, so one dead read neither
|
|
176627
|
+
* hides the others nor stamps the window as done.
|
|
176628
|
+
*/
|
|
176629
|
+
async refreshEverythingNow() {
|
|
176630
|
+
const failed = [];
|
|
176631
|
+
const step = async (name, run) => {
|
|
176269
176632
|
try {
|
|
176270
|
-
if (
|
|
176271
|
-
|
|
176272
|
-
|
|
176273
|
-
|
|
176274
|
-
sleeping: true,
|
|
176275
|
-
hubChild: this.isHubChild(),
|
|
176276
|
-
unreachableAfterMs: 216e5,
|
|
176277
|
-
tickMs: ReolinkCamera.BATTERY_UPDATE_INTERVAL_MS
|
|
176278
|
-
})) {
|
|
176279
|
-
await this.wakeForContactRefresh();
|
|
176280
|
-
return;
|
|
176281
|
-
}
|
|
176282
|
-
this.ctx.logger.debug("battery-update: cam sleeping — skipping cycle without connecting", { tags: { deviceId: this.id } });
|
|
176283
|
-
return;
|
|
176284
|
-
}
|
|
176285
|
-
let api;
|
|
176286
|
-
try {
|
|
176287
|
-
api = await this.ensureApi();
|
|
176288
|
-
} catch (err) {
|
|
176289
|
-
this.ctx.logger.debug("battery-update: ensureApi failed — skipping cycle", {
|
|
176290
|
-
tags: { deviceId: this.id },
|
|
176291
|
-
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
176292
|
-
});
|
|
176293
|
-
return;
|
|
176294
|
-
}
|
|
176295
|
-
let sleeping = false;
|
|
176296
|
-
try {
|
|
176297
|
-
sleeping = api.getSleepStatus({ channel: this.getChannel() }).state === "sleeping";
|
|
176298
|
-
} catch (err) {
|
|
176299
|
-
this.ctx.logger.debug("battery-update: getSleepStatus threw — skipping cycle", {
|
|
176300
|
-
tags: { deviceId: this.id },
|
|
176301
|
-
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
176302
|
-
});
|
|
176303
|
-
return;
|
|
176304
|
-
}
|
|
176305
|
-
if (sleeping) {
|
|
176306
|
-
this.ctx.logger.debug("battery-update: cam sleeping — skipping periodic refresh", { tags: { deviceId: this.id } });
|
|
176307
|
-
return;
|
|
176308
|
-
}
|
|
176309
|
-
this.ctx.logger.info("battery-update: cam awake — running periodic refresh", {
|
|
176633
|
+
if (await run() === false) failed.push(name);
|
|
176634
|
+
} catch (err) {
|
|
176635
|
+
failed.push(name);
|
|
176636
|
+
this.ctx.logger.debug("battery hourly refresh: step threw", {
|
|
176310
176637
|
tags: { deviceId: this.id },
|
|
176311
176638
|
meta: {
|
|
176312
|
-
|
|
176313
|
-
|
|
176639
|
+
step: name,
|
|
176640
|
+
error: err instanceof Error ? err.message : String(err)
|
|
176314
176641
|
}
|
|
176315
176642
|
});
|
|
176316
|
-
await this.refreshBatteryFromApi("periodic").catch(() => {});
|
|
176317
|
-
await this.alignAuxDevicesState("periodic").catch(() => {});
|
|
176318
|
-
await this.refreshParentSettingsSnapshot().catch(() => {});
|
|
176319
|
-
} finally {
|
|
176320
|
-
this.batteryUpdateInFlight = null;
|
|
176321
176643
|
}
|
|
176322
|
-
}
|
|
176323
|
-
this.
|
|
176324
|
-
|
|
176644
|
+
};
|
|
176645
|
+
await step("battery", () => this.refreshBatteryFromApi("demand"));
|
|
176646
|
+
await step("device-info", async () => {
|
|
176647
|
+
await this.populateMetadataFromFirmware(await this.ensureApi());
|
|
176648
|
+
});
|
|
176649
|
+
await step("cap-slices", async () => {
|
|
176650
|
+
await this.warmCapSlicesOnWake({ force: true });
|
|
176651
|
+
});
|
|
176652
|
+
await step("aux-accessories", () => this.alignAuxDevicesState("wake"));
|
|
176653
|
+
await step("settings-snapshot", () => this.refreshParentSettingsSnapshot(void 0, "scheduled"));
|
|
176654
|
+
await step("snapshot", () => this.refreshSnapshotCache());
|
|
176655
|
+
return { failed };
|
|
176325
176656
|
}
|
|
176326
176657
|
/**
|
|
176327
|
-
*
|
|
176328
|
-
*
|
|
176329
|
-
* Bounded by `canProactivelyWake` (the shared cooldown) and by the fact
|
|
176330
|
-
* that a wake that answers resets the silence. A wake that fails is left
|
|
176331
|
-
* alone: the silence then reaches the budget and `deriveBatteryPresence`
|
|
176332
|
-
* says `unreachable` — for the first time on evidence.
|
|
176658
|
+
* Run one scheduled-refresh check. Never throws; costs nothing unless a
|
|
176659
|
+
* refresh is due (see {@link decideBatteryRefresh}).
|
|
176333
176660
|
*/
|
|
176334
|
-
async
|
|
176335
|
-
if (!this.
|
|
176336
|
-
this.
|
|
176337
|
-
const lastContactAt = this.state.battery.lastContactAt ?? 0;
|
|
176338
|
-
const silenceMs = Date.now() - lastContactAt;
|
|
176339
|
-
this.ctx.logger.info("battery contact expiring — waking once to refresh every slice (D355)", {
|
|
176340
|
-
tags: { deviceId: this.id },
|
|
176341
|
-
meta: {
|
|
176342
|
-
silenceMs,
|
|
176343
|
-
budgetMs: BATTERY_UNREACHABLE_AFTER_MS
|
|
176344
|
-
}
|
|
176345
|
-
});
|
|
176346
|
-
try {
|
|
176347
|
-
await this.ensureApi();
|
|
176348
|
-
} catch (err) {
|
|
176349
|
-
this.ctx.logger.warn("battery contact wake FAILED — silence continues and the camera will read unreachable at the budget", {
|
|
176350
|
-
tags: { deviceId: this.id },
|
|
176351
|
-
meta: {
|
|
176352
|
-
silenceMs,
|
|
176353
|
-
error: err instanceof Error ? err.message : String(err)
|
|
176354
|
-
}
|
|
176355
|
-
});
|
|
176356
|
-
return;
|
|
176357
|
-
}
|
|
176358
|
-
await this.refreshBatteryFromApi("demand").catch(() => {});
|
|
176359
|
-
await this.warmCapSlicesOnWake().catch(() => {});
|
|
176360
|
-
await this.alignAuxDevicesState("wake").catch(() => {});
|
|
176361
|
-
await this.warmSnapshotOnWake().catch(() => {});
|
|
176661
|
+
async runBatteryUpdate() {
|
|
176662
|
+
if (!this.isBattery) return;
|
|
176663
|
+
await this.getBatteryRefresh().tick();
|
|
176362
176664
|
}
|
|
176363
|
-
/** Arm the
|
|
176364
|
-
*
|
|
176365
|
-
* events). Idempotent. */
|
|
176665
|
+
/** Arm the scheduled-refresh check. No-op for wired cams (their state is
|
|
176666
|
+
* kept fresh by the 10 s aux-align poll and push events). Idempotent. */
|
|
176366
176667
|
startBatteryUpdatePolling() {
|
|
176367
176668
|
if (this.batteryUpdateTimer) return;
|
|
176368
176669
|
if (!this.isBattery) return;
|
|
176369
176670
|
this.batteryUpdateTimer = setInterval(() => {
|
|
176370
176671
|
this.runBatteryUpdate().catch(() => {});
|
|
176371
|
-
},
|
|
176672
|
+
}, BATTERY_REFRESH_TICK_MS);
|
|
176372
176673
|
if (typeof this.batteryUpdateTimer.unref === "function") this.batteryUpdateTimer.unref();
|
|
176373
|
-
this.ctx.logger.info("battery-update:
|
|
176674
|
+
this.ctx.logger.info("battery-update: scheduled full refresh armed", {
|
|
176374
176675
|
tags: { deviceId: this.id },
|
|
176375
176676
|
meta: {
|
|
176376
|
-
|
|
176677
|
+
tickMs: BATTERY_REFRESH_TICK_MS,
|
|
176678
|
+
intervalMs: resolveBatteryRefreshIntervalMs(this.config.get("batteryRefreshMinutes")),
|
|
176679
|
+
lastFullRefreshAt: this.state.battery.lastFullRefreshAt ?? null,
|
|
176377
176680
|
isHubChild: this.isHubChild()
|
|
176378
176681
|
}
|
|
176379
176682
|
});
|
|
@@ -176469,22 +176772,18 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
176469
176772
|
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
176470
176773
|
});
|
|
176471
176774
|
}
|
|
176472
|
-
await this.
|
|
176473
|
-
|
|
176474
|
-
|
|
176475
|
-
|
|
176775
|
+
if (!(this.isBattery ? await this.getBatteryRefresh().noteAwake().catch(() => false) : false)) {
|
|
176776
|
+
await this.warmCapSlicesOnWake();
|
|
176777
|
+
await this.warmSnapshotOnWake();
|
|
176778
|
+
await this.alignAuxDevicesState("wake");
|
|
176779
|
+
await this.refreshParentSettingsSnapshot();
|
|
176780
|
+
}
|
|
176476
176781
|
await this.refreshClipCatalogOnWake();
|
|
176477
176782
|
}
|
|
176478
176783
|
static PING_INTERVAL_MS = 3e4;
|
|
176479
176784
|
static PING_MAX_FAILURES = 3;
|
|
176480
176785
|
static EVENT_CHECK_INTERVAL_MS = 6e4;
|
|
176481
176786
|
static EVENT_STALE_THRESHOLD_MS = 10 * 6e4;
|
|
176482
|
-
/** Battery-cam periodic refresh cadence. The timer fires on this
|
|
176483
|
-
* interval, passively checks sleep state, and only runs the
|
|
176484
|
-
* refresh when the cam is already awake — so it never forces a
|
|
176485
|
-
* wake. Mirrors the reference's `batteryUpdateIntervalMinutes`
|
|
176486
|
-
* default (60 minutes). */
|
|
176487
|
-
static BATTERY_UPDATE_INTERVAL_MS = 60 * 6e4;
|
|
176488
176787
|
/**
|
|
176489
176788
|
* Arm the connection-liveness ping watchdog and the event-watchdog.
|
|
176490
176789
|
* Battery cams skip the ping watchdog — they keep `setIdleDisconnect=true`
|
|
@@ -177668,7 +177967,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
177668
177967
|
...buildClipRefreshDeviceSection({
|
|
177669
177968
|
isHubChild: this.isHubChild(),
|
|
177670
177969
|
isBattery: this.isBattery
|
|
177671
|
-
})
|
|
177970
|
+
}),
|
|
177971
|
+
...buildBatteryRefreshDeviceSection({ isBattery: this.isBattery })
|
|
177672
177972
|
]
|
|
177673
177973
|
};
|
|
177674
177974
|
const emailSnap = cache?.emailConfigSnapshot;
|
|
@@ -177689,6 +177989,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
177689
177989
|
emailScheduleEnabled: emailSnap?.taskEnabled ?? false,
|
|
177690
177990
|
clipCatalogRefreshMinutes: this.config.get("clipCatalogRefreshMinutes"),
|
|
177691
177991
|
clipCatalogForcedWake: this.config.get("clipCatalogForcedWake") ?? false,
|
|
177992
|
+
batteryRefreshMinutes: this.config.get("batteryRefreshMinutes"),
|
|
177692
177993
|
debugGeneral: this.config.get("debugGeneral") ?? false,
|
|
177693
177994
|
debugSocketLogs: this.config.get("debugSocketLogs") ?? [],
|
|
177694
177995
|
motionEnabled: this.config.get("motionEnabled") ?? motionSnap.enabled ?? true,
|