@camstack/addon-provider-reolink 1.2.86 → 1.2.88
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/DiagnosticsTools-QJ3CRYGA-9NV95vRN.mjs +2 -0
- package/dist/addon.js +353 -35
- package/dist/addon.mjs +355 -38
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/DiagnosticsTools-QJ3CRYGA-CcYIIfQN.mjs +0 -2
package/dist/addon.js
CHANGED
|
@@ -13532,6 +13532,24 @@ var deviceProviderCapability = {
|
|
|
13532
13532
|
})
|
|
13533
13533
|
}
|
|
13534
13534
|
};
|
|
13535
|
+
/**
|
|
13536
|
+
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
13537
|
+
* live registry access, and all management operations into a single tRPC surface.
|
|
13538
|
+
*
|
|
13539
|
+
* Replaces:
|
|
13540
|
+
* - `device-persistence` capability (persistence methods absorbed here)
|
|
13541
|
+
* - `device-management.router.ts` (deleted in Phase 2)
|
|
13542
|
+
* - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
|
|
13543
|
+
*
|
|
13544
|
+
* All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
|
|
13545
|
+
* fork into separate processes but never run on remote cluster agents. Therefore:
|
|
13546
|
+
* - No nodeId routing needed — this is a pure hub singleton.
|
|
13547
|
+
* - The hub's DeviceRegistry is the single source of truth for all live devices.
|
|
13548
|
+
* - No shadow registry or cross-node aggregation required.
|
|
13549
|
+
*
|
|
13550
|
+
* Forked workers register devices back to the hub via `ctx.devices`
|
|
13551
|
+
* (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
|
|
13552
|
+
*/
|
|
13535
13553
|
/** One child-placement directive on a container's `childLayout`. Structurally
|
|
13536
13554
|
* identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
|
|
13537
13555
|
* shape for the same field. The child is identified by its re-sync-stable
|
|
@@ -13630,7 +13648,12 @@ var ConfigEntrySchema = object({
|
|
|
13630
13648
|
value: unknown(),
|
|
13631
13649
|
description: string().optional()
|
|
13632
13650
|
});
|
|
13633
|
-
|
|
13651
|
+
/**
|
|
13652
|
+
* Only `manual` since D356: the operator picks, nothing links itself. The
|
|
13653
|
+
* field survives on the wire because a viewer already OTA'd parses it —
|
|
13654
|
+
* drop it once no shipped client reads `mode`.
|
|
13655
|
+
*/
|
|
13656
|
+
var LinkedDevicesModeSchema = _enum(["manual"]);
|
|
13634
13657
|
/** One resolved linked device — the compact projection consumers need. */
|
|
13635
13658
|
var LinkedDeviceSchema = object({
|
|
13636
13659
|
deviceId: number(),
|
|
@@ -19298,6 +19321,9 @@ var RetrainStatusSchema = _enum([
|
|
|
19298
19321
|
*
|
|
19299
19322
|
* `debug` does NOT pin; it is attention, not durability.
|
|
19300
19323
|
*
|
|
19324
|
+
* `debugNote` is the operator's own words about WHAT should be checked on this
|
|
19325
|
+
* track, and it lives and dies with `debug` — see {@link MAX_TRACK_DEBUG_NOTE_LEN}.
|
|
19326
|
+
*
|
|
19301
19327
|
* `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
|
|
19302
19328
|
* A favourited track is skipped by retention the same way `staging` is, but
|
|
19303
19329
|
* it does not enter `none|staging|trained` and has no staging budget.
|
|
@@ -19308,6 +19334,21 @@ var TrackFlagFields = {
|
|
|
19308
19334
|
markForTrain: boolean().optional(),
|
|
19309
19335
|
/** Operator marked this track for diagnostic attention. */
|
|
19310
19336
|
debug: boolean().optional(),
|
|
19337
|
+
/**
|
|
19338
|
+
* What the operator wants CHECKED on this track, in their own words.
|
|
19339
|
+
*
|
|
19340
|
+
* The flag alone says "look at this" and nothing about what for; a debug set
|
|
19341
|
+
* reviewed hours later is a list of tracks with no question attached. Bound
|
|
19342
|
+
* to `debug` on the WRITE side — the body clears it when `debug` goes off,
|
|
19343
|
+
* and refuses it on a track whose debug is off — so a note can never outlive
|
|
19344
|
+
* the attention it belongs to.
|
|
19345
|
+
*
|
|
19346
|
+
* `''` is a real value ("marked, nothing to add"); the ABSENT key means
|
|
19347
|
+
* "leave whatever is stored alone", which is what lets a surface turn debug
|
|
19348
|
+
* on without a note (a cancelled prompt) and what lets it edit the note
|
|
19349
|
+
* without touching the flag.
|
|
19350
|
+
*/
|
|
19351
|
+
debugNote: string().max(500).optional(),
|
|
19311
19352
|
/** Operator favourited this track. Pins it against pruning. */
|
|
19312
19353
|
favourited: boolean().optional()
|
|
19313
19354
|
};
|
|
@@ -19334,6 +19375,12 @@ var TrackFlagsSchema = object({
|
|
|
19334
19375
|
trackId: string(),
|
|
19335
19376
|
markForTrain: boolean(),
|
|
19336
19377
|
debug: boolean(),
|
|
19378
|
+
/** The note as it STANDS after the write — never absent here (a track with no
|
|
19379
|
+
* note reports `''`), for the same reason the booleans are required: a
|
|
19380
|
+
* surface that has just written must be able to render the note it now owns
|
|
19381
|
+
* without a re-fetch, and an absent key would read as "unchanged" on a
|
|
19382
|
+
* screen that has no previous value to keep. */
|
|
19383
|
+
debugNote: string(),
|
|
19337
19384
|
favourited: boolean(),
|
|
19338
19385
|
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
19339
19386
|
* a track row) because this shape is only ever produced by the write body,
|
|
@@ -34180,6 +34227,17 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
34180
34227
|
return toDeviceSummary(device, this.addonId);
|
|
34181
34228
|
}
|
|
34182
34229
|
};
|
|
34230
|
+
/**
|
|
34231
|
+
* Default silence budget before a sleeping battery camera is called gone.
|
|
34232
|
+
*
|
|
34233
|
+
* Sized off the mechanism that produces contact, not off taste: a battery cam
|
|
34234
|
+
* on this deployment surfaces a firmware push (battery level, sleep/wake, or a
|
|
34235
|
+
* motion email) on the order of hours even with no visitors, and the snapshot
|
|
34236
|
+
* wrapper's own battery window is 1 hour. Six hours is therefore several
|
|
34237
|
+
* missed opportunities, not one — so a single quiet night does not raise a
|
|
34238
|
+
* fault, and a genuinely flat camera is named well inside a day.
|
|
34239
|
+
*/
|
|
34240
|
+
var BATTERY_UNREACHABLE_AFTER_MS = 360 * 6e4;
|
|
34183
34241
|
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;
|
|
34184
34242
|
new Set(Object.values(DeviceType));
|
|
34185
34243
|
DeviceFeature.BatteryOperated;
|
|
@@ -231298,7 +231356,8 @@ var RawReadSliceSchema = _enum([
|
|
|
231298
231356
|
"osd",
|
|
231299
231357
|
"led",
|
|
231300
231358
|
"pir",
|
|
231301
|
-
"autoReboot"
|
|
231359
|
+
"autoReboot",
|
|
231360
|
+
"battery"
|
|
231302
231361
|
]);
|
|
231303
231362
|
/**
|
|
231304
231363
|
* The allow-list. `Record<RawReadSlice, …>` keeps the catalog and the enum
|
|
@@ -231309,17 +231368,26 @@ var RawReadSliceSchema = _enum([
|
|
|
231309
231368
|
var RAW_READ_CATALOG = {
|
|
231310
231369
|
image: {
|
|
231311
231370
|
command: "getVideoInput",
|
|
231312
|
-
|
|
231371
|
+
believed: {
|
|
231372
|
+
kind: "deviceCache",
|
|
231373
|
+
snapshotKey: "imageSnapshot"
|
|
231374
|
+
},
|
|
231313
231375
|
invoke: (api, channel) => api.getVideoInput(channel)
|
|
231314
231376
|
},
|
|
231315
231377
|
motion: {
|
|
231316
231378
|
command: "getMotionAlarm",
|
|
231317
|
-
|
|
231379
|
+
believed: {
|
|
231380
|
+
kind: "deviceCache",
|
|
231381
|
+
snapshotKey: "motionSnapshot"
|
|
231382
|
+
},
|
|
231318
231383
|
invoke: (api, channel) => api.getMotionAlarm(channel)
|
|
231319
231384
|
},
|
|
231320
231385
|
ai: {
|
|
231321
231386
|
command: "getAiDetectTypes + getAiDetectionFull (per type)",
|
|
231322
|
-
|
|
231387
|
+
believed: {
|
|
231388
|
+
kind: "deviceCache",
|
|
231389
|
+
snapshotKey: "aiSensitivitySnapshot"
|
|
231390
|
+
},
|
|
231323
231391
|
invoke: async (api, channel) => {
|
|
231324
231392
|
const detectTypes = await api.getAiDetectTypes(channel, { timeoutMs: 1500 });
|
|
231325
231393
|
const perType = {};
|
|
@@ -231336,71 +231404,204 @@ var RAW_READ_CATALOG = {
|
|
|
231336
231404
|
},
|
|
231337
231405
|
enc: {
|
|
231338
231406
|
command: "getEnc",
|
|
231339
|
-
|
|
231407
|
+
believed: {
|
|
231408
|
+
kind: "deviceCache",
|
|
231409
|
+
snapshotKey: "encSnapshot"
|
|
231410
|
+
},
|
|
231340
231411
|
invoke: (api, channel) => api.getEnc(channel)
|
|
231341
231412
|
},
|
|
231342
231413
|
encOptions: {
|
|
231343
231414
|
command: "getEncOptions",
|
|
231344
|
-
|
|
231415
|
+
believed: {
|
|
231416
|
+
kind: "deviceCache",
|
|
231417
|
+
snapshotKey: "encOptionsSnapshot"
|
|
231418
|
+
},
|
|
231345
231419
|
invoke: (api, channel) => api.getEncOptions(channel)
|
|
231346
231420
|
},
|
|
231347
231421
|
mask: {
|
|
231348
231422
|
command: "getMask",
|
|
231349
|
-
|
|
231423
|
+
believed: {
|
|
231424
|
+
kind: "deviceCache",
|
|
231425
|
+
snapshotKey: "maskSnapshot"
|
|
231426
|
+
},
|
|
231350
231427
|
invoke: (api, channel) => api.getMask(channel)
|
|
231351
231428
|
},
|
|
231352
231429
|
audioNoise: {
|
|
231353
231430
|
command: "getAudioNoise",
|
|
231354
|
-
|
|
231431
|
+
believed: {
|
|
231432
|
+
kind: "deviceCache",
|
|
231433
|
+
snapshotKey: "audioNoiseSnapshot"
|
|
231434
|
+
},
|
|
231355
231435
|
invoke: (api, channel) => api.getAudioNoise(channel)
|
|
231356
231436
|
},
|
|
231357
231437
|
autofocus: {
|
|
231358
231438
|
command: "getAutoFocus",
|
|
231359
|
-
|
|
231439
|
+
believed: {
|
|
231440
|
+
kind: "deviceCache",
|
|
231441
|
+
snapshotKey: "autoFocusSnapshot"
|
|
231442
|
+
},
|
|
231360
231443
|
invoke: (api, channel) => api.getAutoFocus(channel, { timeoutMs: 1500 })
|
|
231361
231444
|
},
|
|
231362
231445
|
netPort: {
|
|
231363
231446
|
command: "getNetPort",
|
|
231364
|
-
|
|
231447
|
+
believed: {
|
|
231448
|
+
kind: "deviceCache",
|
|
231449
|
+
snapshotKey: "netPortSnapshot"
|
|
231450
|
+
},
|
|
231365
231451
|
invoke: (api) => api.getNetPort()
|
|
231366
231452
|
},
|
|
231367
231453
|
ntp: {
|
|
231368
231454
|
command: "getNtp",
|
|
231369
|
-
|
|
231455
|
+
believed: {
|
|
231456
|
+
kind: "deviceCache",
|
|
231457
|
+
snapshotKey: "ntpSnapshot"
|
|
231458
|
+
},
|
|
231370
231459
|
invoke: (api) => api.getNtp()
|
|
231371
231460
|
},
|
|
231372
231461
|
systemGeneral: {
|
|
231373
231462
|
command: "getSystemGeneral",
|
|
231374
|
-
|
|
231463
|
+
believed: {
|
|
231464
|
+
kind: "deviceCache",
|
|
231465
|
+
snapshotKey: "systemGeneralSnapshot"
|
|
231466
|
+
},
|
|
231375
231467
|
invoke: (api) => api.getSystemGeneral()
|
|
231376
231468
|
},
|
|
231377
231469
|
osd: {
|
|
231378
231470
|
command: "getOsd",
|
|
231379
|
-
|
|
231471
|
+
believed: {
|
|
231472
|
+
kind: "deviceCache",
|
|
231473
|
+
snapshotKey: "osdSnapshot"
|
|
231474
|
+
},
|
|
231380
231475
|
invoke: (api, channel) => api.getOsd(channel)
|
|
231381
231476
|
},
|
|
231382
231477
|
led: {
|
|
231383
231478
|
command: "getIrLights",
|
|
231384
|
-
|
|
231479
|
+
believed: {
|
|
231480
|
+
kind: "deviceCache",
|
|
231481
|
+
snapshotKey: "ledSnapshot"
|
|
231482
|
+
},
|
|
231385
231483
|
invoke: (api, channel) => api.getIrLights(channel)
|
|
231386
231484
|
},
|
|
231387
231485
|
pir: {
|
|
231388
231486
|
command: "getPirInfo",
|
|
231389
|
-
|
|
231487
|
+
believed: {
|
|
231488
|
+
kind: "deviceCache",
|
|
231489
|
+
snapshotKey: "pirSnapshot"
|
|
231490
|
+
},
|
|
231390
231491
|
invoke: (api, channel) => api.getPirInfo(channel)
|
|
231391
231492
|
},
|
|
231392
231493
|
autoReboot: {
|
|
231393
231494
|
command: "getAutoReboot",
|
|
231394
|
-
|
|
231495
|
+
believed: {
|
|
231496
|
+
kind: "deviceCache",
|
|
231497
|
+
snapshotKey: "autoRebootSnapshot"
|
|
231498
|
+
},
|
|
231395
231499
|
invoke: (api) => api.getAutoReboot()
|
|
231500
|
+
},
|
|
231501
|
+
/**
|
|
231502
|
+
* Battery — one slice, TWO firmware reads, chosen by how the camera is
|
|
231503
|
+
* attached. They are not variants of one call:
|
|
231504
|
+
*
|
|
231505
|
+
* - **standalone**: `getBatteryInfo(channel)` — the camera's own
|
|
231506
|
+
* answer, one `BatteryInfo`;
|
|
231507
|
+
* - **hub child**: `getAllChannelsBatteryInfo()` — a HUB-level CGI
|
|
231508
|
+
* keyed by channel, projected to the requested one here so the slice
|
|
231509
|
+
* stays per-camera like the other fifteen (a "cluster-wide" second
|
|
231510
|
+
* slice shape would have to be understood by every caller).
|
|
231511
|
+
*
|
|
231512
|
+
* Both vocabularies carry `adapterStatus` next to `chargeStatus`, which
|
|
231513
|
+
* is why this slice exists: the provider keeps only the latter, and
|
|
231514
|
+
* whether `adapterStatus` MOVES was previously observable exactly once
|
|
231515
|
+
* per hub process (`ReolinkHub.batteryShapeLogged`). The answer is
|
|
231516
|
+
* returned as the firmware gave it, under a `readVia` that names the path
|
|
231517
|
+
* — NOT normalised into a shared shape. The paths do not carry the same
|
|
231518
|
+
* fields (`voltage`/`lowPower` on the camera read, `channelsReported` and
|
|
231519
|
+
* the channel-status row on the hub read); a field the firmware did not
|
|
231520
|
+
* send must be ABSENT, because a default here would be indistinguishable
|
|
231521
|
+
* from a reading (D315).
|
|
231522
|
+
*
|
|
231523
|
+
* A camera that cannot answer at all throws, and the read comes back
|
|
231524
|
+
* `read-failed` carrying the firmware's reason — "we could not read it"
|
|
231525
|
+
* must never be served as "it has no battery".
|
|
231526
|
+
*/
|
|
231527
|
+
battery: {
|
|
231528
|
+
command: "getBatteryInfo (standalone) / getAllChannelsBatteryInfo projected (hub child)",
|
|
231529
|
+
believed: {
|
|
231530
|
+
kind: "capRuntimeState",
|
|
231531
|
+
capName: "battery"
|
|
231532
|
+
},
|
|
231533
|
+
servedByParentHub: true,
|
|
231534
|
+
invoke: async (api, channel, ctx) => {
|
|
231535
|
+
if (!ctx.hubChild) return {
|
|
231536
|
+
readVia: "camera",
|
|
231537
|
+
command: "getBatteryInfo",
|
|
231538
|
+
channel,
|
|
231539
|
+
batteryInfo: await api.getBatteryInfo(channel)
|
|
231540
|
+
};
|
|
231541
|
+
const byChannel = (await api.getAllChannelsBatteryInfo()).batteryInfoData;
|
|
231542
|
+
const channelsReported = Object.keys(byChannel).map(Number).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
|
|
231543
|
+
const info = byChannel[channel];
|
|
231544
|
+
if (info === void 0) throw new Error(`getAllChannelsBatteryInfo answered for channels [${channelsReported.join(", ")}] but not channel ${channel} — this camera's battery was NOT reported. That is an unanswered read, not "no battery".`);
|
|
231545
|
+
const [cgiBattery, channelStatus] = info.entries;
|
|
231546
|
+
return {
|
|
231547
|
+
readVia: "parentHub",
|
|
231548
|
+
command: "getAllChannelsBatteryInfo",
|
|
231549
|
+
channel,
|
|
231550
|
+
channelsReported,
|
|
231551
|
+
batteryInfo: {
|
|
231552
|
+
...info,
|
|
231553
|
+
entries: [cgiBattery ?? null, redactChannelIdentity(channelStatus)]
|
|
231554
|
+
}
|
|
231555
|
+
};
|
|
231556
|
+
}
|
|
231396
231557
|
}
|
|
231397
231558
|
};
|
|
231559
|
+
/**
|
|
231560
|
+
* The hub's channel-status row (`entries[1]`) carries the camera's Reolink
|
|
231561
|
+
* P2P `uid` — a routable cloud identifier for the device. A charge-state
|
|
231562
|
+
* read has no reason to hand it out, so it is dropped here rather than
|
|
231563
|
+
* relied on not to be looked at. The drop is STATED (`uidRedacted`): a
|
|
231564
|
+
* removed field and a firmware that never sent one must not look alike.
|
|
231565
|
+
* Everything else in the row (channel, name, online, sleep, typeInfo) is
|
|
231566
|
+
* kept verbatim — it is the context that makes the battery row readable.
|
|
231567
|
+
*/
|
|
231568
|
+
function redactChannelIdentity(entry) {
|
|
231569
|
+
if (entry === void 0) return null;
|
|
231570
|
+
const { uid, ...rest } = entry;
|
|
231571
|
+
return {
|
|
231572
|
+
...rest,
|
|
231573
|
+
uidRedacted: uid !== void 0
|
|
231574
|
+
};
|
|
231575
|
+
}
|
|
231576
|
+
/**
|
|
231577
|
+
* Age of a `capRuntimeState` belief. The cap slice carries `lastUpdated`
|
|
231578
|
+
* rather than a freshness-map stamp, and its `empty` seed is `0` — which is
|
|
231579
|
+
* NOT an observation, so it resolves to `never`. A slice that was never
|
|
231580
|
+
* written at all is `never` too: on a battery reading, "we hold no value"
|
|
231581
|
+
* and "we measured 0%" are an alarm apart (see `BatteryStatusSchema`).
|
|
231582
|
+
*/
|
|
231583
|
+
function capStateAge(state, now) {
|
|
231584
|
+
if (state === void 0) return { state: "never" };
|
|
231585
|
+
const lastUpdated = state.lastUpdated;
|
|
231586
|
+
if (typeof lastUpdated !== "number" || lastUpdated <= 0) return { state: "never" };
|
|
231587
|
+
return {
|
|
231588
|
+
state: "known",
|
|
231589
|
+
fetchedAt: lastUpdated,
|
|
231590
|
+
ageMs: Math.max(0, now - lastUpdated)
|
|
231591
|
+
};
|
|
231592
|
+
}
|
|
231398
231593
|
/** What CamStack currently believes about the slice, with its own age. */
|
|
231399
231594
|
var BelievedStateSchema = object({
|
|
231400
|
-
/** The persisted projection
|
|
231595
|
+
/** The persisted projection / cap slice, verbatim. */
|
|
231401
231596
|
snapshot: unknown(),
|
|
231402
|
-
/**
|
|
231403
|
-
|
|
231597
|
+
/** Where that belief lives — mirrors {@link BelievedSource}. */
|
|
231598
|
+
source: discriminatedUnion("kind", [object({
|
|
231599
|
+
kind: literal("deviceCache"),
|
|
231600
|
+
snapshotKey: string()
|
|
231601
|
+
}), object({
|
|
231602
|
+
kind: literal("capRuntimeState"),
|
|
231603
|
+
capName: literal("battery")
|
|
231604
|
+
})]),
|
|
231404
231605
|
/** Tri-state freshness — `never` / `unknown` (legacy, no stamp) / `known`. */
|
|
231405
231606
|
age: union([
|
|
231406
231607
|
object({ state: literal("never") }),
|
|
@@ -231439,11 +231640,17 @@ var RawReadInputSchema = object({
|
|
|
231439
231640
|
deviceId: number().int().nonnegative(),
|
|
231440
231641
|
slice: RawReadSliceSchema
|
|
231441
231642
|
});
|
|
231442
|
-
function believedState(
|
|
231443
|
-
const
|
|
231643
|
+
function believedState(deps, entry) {
|
|
231644
|
+
const source = entry.believed;
|
|
231645
|
+
if (source.kind === "capRuntimeState") return {
|
|
231646
|
+
snapshot: toPlainJson(deps.batteryState),
|
|
231647
|
+
source,
|
|
231648
|
+
age: capStateAge(deps.batteryState, deps.now)
|
|
231649
|
+
};
|
|
231650
|
+
const age = resolveSnapshotAge(deps.cache, source.snapshotKey, deps.now);
|
|
231444
231651
|
return {
|
|
231445
|
-
snapshot: toPlainJson(cache === void 0 ? void 0 : Reflect.get(cache,
|
|
231446
|
-
|
|
231652
|
+
snapshot: toPlainJson(deps.cache === void 0 ? void 0 : Reflect.get(deps.cache, source.snapshotKey)),
|
|
231653
|
+
source,
|
|
231447
231654
|
age
|
|
231448
231655
|
};
|
|
231449
231656
|
}
|
|
@@ -231463,8 +231670,9 @@ function toPlainJson(value) {
|
|
|
231463
231670
|
*/
|
|
231464
231671
|
async function performRawRead(slice, deps) {
|
|
231465
231672
|
const entry = RAW_READ_CATALOG[slice];
|
|
231466
|
-
const believed = believedState(deps
|
|
231467
|
-
|
|
231673
|
+
const believed = believedState(deps, entry);
|
|
231674
|
+
const servedWithoutTouchingCamera = deps.hubChild && entry.servedByParentHub === true;
|
|
231675
|
+
if (deps.sleeping && !servedWithoutTouchingCamera) {
|
|
231468
231676
|
deps.logger.info("reolink raw read refused — battery cam is sleeping", {
|
|
231469
231677
|
tags: { deviceId: deps.deviceId },
|
|
231470
231678
|
meta: { slice }
|
|
@@ -231474,7 +231682,7 @@ async function performRawRead(slice, deps) {
|
|
|
231474
231682
|
deviceId: deps.deviceId,
|
|
231475
231683
|
slice,
|
|
231476
231684
|
reason: "sleeping",
|
|
231477
|
-
message: "Battery camera is asleep — a raw read would wake it, so it is refused. The believed state below is what CamStack currently serves.",
|
|
231685
|
+
message: deps.hubChild ? `Battery camera is asleep and the "${slice}" read is addressed to the camera channel, which would wake it — refused. The believed state below is what CamStack currently serves. (The "battery" slice is answered by the parent hub and stays readable while the camera sleeps.)` : "Battery camera is asleep — a raw read would wake it, so it is refused. The believed state below is what CamStack currently serves.",
|
|
231478
231686
|
believed
|
|
231479
231687
|
};
|
|
231480
231688
|
}
|
|
@@ -231500,12 +231708,14 @@ async function performRawRead(slice, deps) {
|
|
|
231500
231708
|
};
|
|
231501
231709
|
}
|
|
231502
231710
|
try {
|
|
231503
|
-
const payload = await entry.invoke(api, deps.channel);
|
|
231711
|
+
const payload = await entry.invoke(api, deps.channel, { hubChild: deps.hubChild });
|
|
231504
231712
|
deps.logger.info("reolink raw read served", {
|
|
231505
231713
|
tags: { deviceId: deps.deviceId },
|
|
231506
231714
|
meta: {
|
|
231507
231715
|
slice,
|
|
231508
|
-
command: entry.command
|
|
231716
|
+
command: entry.command,
|
|
231717
|
+
viaParentHub: servedWithoutTouchingCamera,
|
|
231718
|
+
cameraWasSleeping: deps.sleeping
|
|
231509
231719
|
}
|
|
231510
231720
|
});
|
|
231511
231721
|
return {
|
|
@@ -231564,6 +231774,67 @@ var reolinkDebugActions = defineCustomActions({ debugRawRead: customAction(RawRe
|
|
|
231564
231774
|
auth: "admin"
|
|
231565
231775
|
}) });
|
|
231566
231776
|
//#endregion
|
|
231777
|
+
//#region src/battery-charging.ts
|
|
231778
|
+
/**
|
|
231779
|
+
* Da `adapterStatus` + `chargeStatus` alla sorgente di alimentazione.
|
|
231780
|
+
*
|
|
231781
|
+
* I due campi rispondono a DUE domande diverse, e confonderle è il bug che
|
|
231782
|
+
* questo modulo esiste per chiudere:
|
|
231783
|
+
*
|
|
231784
|
+
* - `adapterStatus` — l'alimentazione è COLLEGATA? Il firmware manda 0/1/2
|
|
231785
|
+
* (nessuna · adattatore DC · pannello solare) e il confine dell'hub lo
|
|
231786
|
+
* traduce in `'none' | 'dc' | 'solarPanel'` (`mapHubChargeStatus` e
|
|
231787
|
+
* `mapHubAdapterStatus` in `reolink-hub.ts`), perché il vocabolario del
|
|
231788
|
+
* percorso standalone è già a stringhe.
|
|
231789
|
+
* - `chargeStatus` — la CELLA cosa sta facendo? 0 ferma · 1 in carica ·
|
|
231790
|
+
* 2 carica completa.
|
|
231791
|
+
*
|
|
231792
|
+
* La vecchia derivazione consultava l'adattatore SOLO per cercarci 'solar', e
|
|
231793
|
+
* un DC collegato cadeva nel ripiego `'none'`. Due Argus 3E lo nascondevano per
|
|
231794
|
+
* caso: piene e collegate rispondono `chargeStatus: 2`, che il ramo `isCharging`
|
|
231795
|
+
* mappava su `'dc'` per la ragione sbagliata. La Argus MagiCam (batteryVersion
|
|
231796
|
+
* 1, firmware 2026) collegata in permanenza risponde `chargeStatus: 0`, e per
|
|
231797
|
+
* tutta la sua vita è stata riportata NON in carica.
|
|
231798
|
+
*
|
|
231799
|
+
* Che `adapterStatus` sia un segnale vero e non una costante è MISURATO: il
|
|
231800
|
+
* 2026-09-05 l'operatore ha staccato il device 3628 e il payload grezzo è
|
|
231801
|
+
* passato da `adapterStatus: 1` a `0`, con `chargeStatus: 0` da entrambe le
|
|
231802
|
+
* parti — cioè `chargeStatus` da solo NON distingue i due stati.
|
|
231803
|
+
*
|
|
231804
|
+
* ⚠️ Questa funzione non sa ancora dire "non lo so": `BatteryStatus['charging']`
|
|
231805
|
+
* non ha un valore `unknown`, quindi due campi illeggibili producono `'none'`,
|
|
231806
|
+
* che è un valore NEGATIVO al posto di un non-noto. È il difetto D315 che
|
|
231807
|
+
* `percentage` ha già chiuso (è `nullable`) e questo campo no; chiuderlo tocca
|
|
231808
|
+
* `battery.cap.ts`, cioè la closure del server, e vive nel proprio lavoro.
|
|
231809
|
+
*/
|
|
231810
|
+
function deriveChargingSource(adapterStatus, chargeStatus) {
|
|
231811
|
+
const adapter = (adapterStatus ?? "").toLowerCase();
|
|
231812
|
+
const charge = (chargeStatus ?? "").toLowerCase();
|
|
231813
|
+
if (adapter.includes("solar")) return "solar";
|
|
231814
|
+
if (adapter === "dc") return "dc";
|
|
231815
|
+
if (charge === "charging" || charge === "chargecomplete") return "dc";
|
|
231816
|
+
return "none";
|
|
231817
|
+
}
|
|
231818
|
+
/**
|
|
231819
|
+
* Did this reading SAY anything about the power source? A battery push may
|
|
231820
|
+
* carry only the level (every `BatteryInfo` field is optional and nodelink
|
|
231821
|
+
* dispatches as soon as one is present); a caller that derives `charging`
|
|
231822
|
+
* from such a push turns "nothing was said" into 'none' and overwrites a known
|
|
231823
|
+
* 'dc'. When this is false the caller keeps its previous belief.
|
|
231824
|
+
*/
|
|
231825
|
+
function reportsPowerSource(info) {
|
|
231826
|
+
return info.adapterStatus !== void 0 || info.chargeStatus !== void 0;
|
|
231827
|
+
}
|
|
231828
|
+
//#endregion
|
|
231829
|
+
//#region src/battery-contact-expiry.ts
|
|
231830
|
+
function shouldWakeForContact(input) {
|
|
231831
|
+
if (input.hubChild) return false;
|
|
231832
|
+
if (!input.sleeping) return false;
|
|
231833
|
+
const last = input.lastContactAt;
|
|
231834
|
+
if (typeof last !== "number" || last <= 0) return false;
|
|
231835
|
+
return input.nowMs - last + input.tickMs >= input.unreachableAfterMs;
|
|
231836
|
+
}
|
|
231837
|
+
//#endregion
|
|
231567
231838
|
//#region src/log-channels.ts
|
|
231568
231839
|
/**
|
|
231569
231840
|
* The diagnostic log CHANNELS `provider-reolink` declares.
|
|
@@ -236339,8 +236610,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
236339
236610
|
deviceId: this.id,
|
|
236340
236611
|
channel: this.getChannel(),
|
|
236341
236612
|
sleeping: this.isBattery && this.sleeping,
|
|
236613
|
+
hubChild: this.isHubChild(),
|
|
236342
236614
|
getApi: () => this.ensureApi(),
|
|
236343
236615
|
cache: this.config.get("deviceCache"),
|
|
236616
|
+
batteryState: this.runtimeState.getCapState("battery"),
|
|
236344
236617
|
logger: this.ctx.logger,
|
|
236345
236618
|
now: Date.now()
|
|
236346
236619
|
});
|
|
@@ -236671,13 +236944,9 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
236671
236944
|
* tracker is more reliable.
|
|
236672
236945
|
*/
|
|
236673
236946
|
mapBatteryInfo(info) {
|
|
236674
|
-
const percentage = typeof info.batteryPercent === "number" ? Math.max(0, Math.min(100, Math.round(info.batteryPercent))) : this.state.battery.percentage ?? null;
|
|
236675
|
-
const adapter = (info.adapterStatus ?? "").toLowerCase();
|
|
236676
|
-
const charge = (info.chargeStatus ?? "").toLowerCase();
|
|
236677
|
-
const isCharging = charge === "charging" || charge === "chargecomplete";
|
|
236678
236947
|
return {
|
|
236679
|
-
percentage,
|
|
236680
|
-
charging:
|
|
236948
|
+
percentage: typeof info.batteryPercent === "number" ? Math.max(0, Math.min(100, Math.round(info.batteryPercent))) : this.state.battery.percentage ?? null,
|
|
236949
|
+
charging: reportsPowerSource(info) ? deriveChargingSource(info.adapterStatus, info.chargeStatus) : this.state.battery.charging ?? "none",
|
|
236681
236950
|
sleeping: info.sleeping === true || this.sleeping,
|
|
236682
236951
|
lastUpdated: Date.now()
|
|
236683
236952
|
};
|
|
@@ -240226,6 +240495,17 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
240226
240495
|
const cycle = (async () => {
|
|
240227
240496
|
try {
|
|
240228
240497
|
if (this.isBattery && this.sleeping) {
|
|
240498
|
+
if (shouldWakeForContact({
|
|
240499
|
+
nowMs: Date.now(),
|
|
240500
|
+
lastContactAt: this.state.battery.lastContactAt,
|
|
240501
|
+
sleeping: true,
|
|
240502
|
+
hubChild: this.isHubChild(),
|
|
240503
|
+
unreachableAfterMs: 216e5,
|
|
240504
|
+
tickMs: ReolinkCamera.BATTERY_UPDATE_INTERVAL_MS
|
|
240505
|
+
})) {
|
|
240506
|
+
await this.wakeForContactRefresh();
|
|
240507
|
+
return;
|
|
240508
|
+
}
|
|
240229
240509
|
this.ctx.logger.debug("battery-update: cam sleeping — skipping cycle without connecting", { tags: { deviceId: this.id } });
|
|
240230
240510
|
return;
|
|
240231
240511
|
}
|
|
@@ -240270,6 +240550,43 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
240270
240550
|
this.batteryUpdateInFlight = cycle;
|
|
240271
240551
|
return cycle;
|
|
240272
240552
|
}
|
|
240553
|
+
/**
|
|
240554
|
+
* D355: wake a standalone battery camera ONCE because its last passive
|
|
240555
|
+
* contact is about to expire, and refresh every slice while it is up.
|
|
240556
|
+
* Bounded by `canProactivelyWake` (the shared cooldown) and by the fact
|
|
240557
|
+
* that a wake that answers resets the silence. A wake that fails is left
|
|
240558
|
+
* alone: the silence then reaches the budget and `deriveBatteryPresence`
|
|
240559
|
+
* says `unreachable` — for the first time on evidence.
|
|
240560
|
+
*/
|
|
240561
|
+
async wakeForContactRefresh() {
|
|
240562
|
+
if (!this.canProactivelyWake("contact-expiry")) return;
|
|
240563
|
+
this.markWakeIssued();
|
|
240564
|
+
const lastContactAt = this.state.battery.lastContactAt ?? 0;
|
|
240565
|
+
const silenceMs = Date.now() - lastContactAt;
|
|
240566
|
+
this.ctx.logger.info("battery contact expiring — waking once to refresh every slice (D355)", {
|
|
240567
|
+
tags: { deviceId: this.id },
|
|
240568
|
+
meta: {
|
|
240569
|
+
silenceMs,
|
|
240570
|
+
budgetMs: BATTERY_UNREACHABLE_AFTER_MS
|
|
240571
|
+
}
|
|
240572
|
+
});
|
|
240573
|
+
try {
|
|
240574
|
+
await this.ensureApi();
|
|
240575
|
+
} catch (err) {
|
|
240576
|
+
this.ctx.logger.warn("battery contact wake FAILED — silence continues and the camera will read unreachable at the budget", {
|
|
240577
|
+
tags: { deviceId: this.id },
|
|
240578
|
+
meta: {
|
|
240579
|
+
silenceMs,
|
|
240580
|
+
error: err instanceof Error ? err.message : String(err)
|
|
240581
|
+
}
|
|
240582
|
+
});
|
|
240583
|
+
return;
|
|
240584
|
+
}
|
|
240585
|
+
await this.refreshBatteryFromApi("demand").catch(() => {});
|
|
240586
|
+
await this.warmCapSlicesOnWake().catch(() => {});
|
|
240587
|
+
await this.alignAuxDevicesState("wake").catch(() => {});
|
|
240588
|
+
await this.warmSnapshotOnWake().catch(() => {});
|
|
240589
|
+
}
|
|
240273
240590
|
/** Arm the periodic battery-cam refresh timer. No-op for wired cams
|
|
240274
240591
|
* (their state is kept fresh by the 10 s aux-align poll and push
|
|
240275
240592
|
* events). Idempotent. */
|
|
@@ -245192,6 +245509,7 @@ exports.collectMultifocalDiagnostics = collectMultifocalDiagnostics;
|
|
|
245192
245509
|
exports.collectNativeDiagnostics = collectNativeDiagnostics;
|
|
245193
245510
|
exports.collectNvrDiagnostics = collectNvrDiagnostics;
|
|
245194
245511
|
exports.createDiagnosticsBundle = createDiagnosticsBundle;
|
|
245512
|
+
exports.customActions = reolinkDebugActions;
|
|
245195
245513
|
exports.reolinkCameraSchema = reolinkCameraSchema;
|
|
245196
245514
|
exports.reolinkDebugActions = reolinkDebugActions;
|
|
245197
245515
|
exports.runAllDiagnosticsConsecutively = runAllDiagnosticsConsecutively;
|
package/dist/addon.mjs
CHANGED
|
@@ -13527,6 +13527,24 @@ var deviceProviderCapability = {
|
|
|
13527
13527
|
})
|
|
13528
13528
|
}
|
|
13529
13529
|
};
|
|
13530
|
+
/**
|
|
13531
|
+
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
13532
|
+
* live registry access, and all management operations into a single tRPC surface.
|
|
13533
|
+
*
|
|
13534
|
+
* Replaces:
|
|
13535
|
+
* - `device-persistence` capability (persistence methods absorbed here)
|
|
13536
|
+
* - `device-management.router.ts` (deleted in Phase 2)
|
|
13537
|
+
* - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
|
|
13538
|
+
*
|
|
13539
|
+
* All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
|
|
13540
|
+
* fork into separate processes but never run on remote cluster agents. Therefore:
|
|
13541
|
+
* - No nodeId routing needed — this is a pure hub singleton.
|
|
13542
|
+
* - The hub's DeviceRegistry is the single source of truth for all live devices.
|
|
13543
|
+
* - No shadow registry or cross-node aggregation required.
|
|
13544
|
+
*
|
|
13545
|
+
* Forked workers register devices back to the hub via `ctx.devices`
|
|
13546
|
+
* (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
|
|
13547
|
+
*/
|
|
13530
13548
|
/** One child-placement directive on a container's `childLayout`. Structurally
|
|
13531
13549
|
* identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
|
|
13532
13550
|
* shape for the same field. The child is identified by its re-sync-stable
|
|
@@ -13625,7 +13643,12 @@ var ConfigEntrySchema = object({
|
|
|
13625
13643
|
value: unknown(),
|
|
13626
13644
|
description: string().optional()
|
|
13627
13645
|
});
|
|
13628
|
-
|
|
13646
|
+
/**
|
|
13647
|
+
* Only `manual` since D356: the operator picks, nothing links itself. The
|
|
13648
|
+
* field survives on the wire because a viewer already OTA'd parses it —
|
|
13649
|
+
* drop it once no shipped client reads `mode`.
|
|
13650
|
+
*/
|
|
13651
|
+
var LinkedDevicesModeSchema = _enum(["manual"]);
|
|
13629
13652
|
/** One resolved linked device — the compact projection consumers need. */
|
|
13630
13653
|
var LinkedDeviceSchema = object({
|
|
13631
13654
|
deviceId: number(),
|
|
@@ -19293,6 +19316,9 @@ var RetrainStatusSchema = _enum([
|
|
|
19293
19316
|
*
|
|
19294
19317
|
* `debug` does NOT pin; it is attention, not durability.
|
|
19295
19318
|
*
|
|
19319
|
+
* `debugNote` is the operator's own words about WHAT should be checked on this
|
|
19320
|
+
* track, and it lives and dies with `debug` — see {@link MAX_TRACK_DEBUG_NOTE_LEN}.
|
|
19321
|
+
*
|
|
19296
19322
|
* `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
|
|
19297
19323
|
* A favourited track is skipped by retention the same way `staging` is, but
|
|
19298
19324
|
* it does not enter `none|staging|trained` and has no staging budget.
|
|
@@ -19303,6 +19329,21 @@ var TrackFlagFields = {
|
|
|
19303
19329
|
markForTrain: boolean().optional(),
|
|
19304
19330
|
/** Operator marked this track for diagnostic attention. */
|
|
19305
19331
|
debug: boolean().optional(),
|
|
19332
|
+
/**
|
|
19333
|
+
* What the operator wants CHECKED on this track, in their own words.
|
|
19334
|
+
*
|
|
19335
|
+
* The flag alone says "look at this" and nothing about what for; a debug set
|
|
19336
|
+
* reviewed hours later is a list of tracks with no question attached. Bound
|
|
19337
|
+
* to `debug` on the WRITE side — the body clears it when `debug` goes off,
|
|
19338
|
+
* and refuses it on a track whose debug is off — so a note can never outlive
|
|
19339
|
+
* the attention it belongs to.
|
|
19340
|
+
*
|
|
19341
|
+
* `''` is a real value ("marked, nothing to add"); the ABSENT key means
|
|
19342
|
+
* "leave whatever is stored alone", which is what lets a surface turn debug
|
|
19343
|
+
* on without a note (a cancelled prompt) and what lets it edit the note
|
|
19344
|
+
* without touching the flag.
|
|
19345
|
+
*/
|
|
19346
|
+
debugNote: string().max(500).optional(),
|
|
19306
19347
|
/** Operator favourited this track. Pins it against pruning. */
|
|
19307
19348
|
favourited: boolean().optional()
|
|
19308
19349
|
};
|
|
@@ -19329,6 +19370,12 @@ var TrackFlagsSchema = object({
|
|
|
19329
19370
|
trackId: string(),
|
|
19330
19371
|
markForTrain: boolean(),
|
|
19331
19372
|
debug: boolean(),
|
|
19373
|
+
/** The note as it STANDS after the write — never absent here (a track with no
|
|
19374
|
+
* note reports `''`), for the same reason the booleans are required: a
|
|
19375
|
+
* surface that has just written must be able to render the note it now owns
|
|
19376
|
+
* without a re-fetch, and an absent key would read as "unchanged" on a
|
|
19377
|
+
* screen that has no previous value to keep. */
|
|
19378
|
+
debugNote: string(),
|
|
19332
19379
|
favourited: boolean(),
|
|
19333
19380
|
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
19334
19381
|
* a track row) because this shape is only ever produced by the write body,
|
|
@@ -34175,6 +34222,17 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
34175
34222
|
return toDeviceSummary(device, this.addonId);
|
|
34176
34223
|
}
|
|
34177
34224
|
};
|
|
34225
|
+
/**
|
|
34226
|
+
* Default silence budget before a sleeping battery camera is called gone.
|
|
34227
|
+
*
|
|
34228
|
+
* Sized off the mechanism that produces contact, not off taste: a battery cam
|
|
34229
|
+
* on this deployment surfaces a firmware push (battery level, sleep/wake, or a
|
|
34230
|
+
* motion email) on the order of hours even with no visitors, and the snapshot
|
|
34231
|
+
* wrapper's own battery window is 1 hour. Six hours is therefore several
|
|
34232
|
+
* missed opportunities, not one — so a single quiet night does not raise a
|
|
34233
|
+
* fault, and a genuinely flat camera is named well inside a day.
|
|
34234
|
+
*/
|
|
34235
|
+
var BATTERY_UNREACHABLE_AFTER_MS = 360 * 6e4;
|
|
34178
34236
|
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;
|
|
34179
34237
|
new Set(Object.values(DeviceType));
|
|
34180
34238
|
DeviceFeature.BatteryOperated;
|
|
@@ -179513,7 +179571,7 @@ ${xml}`);
|
|
|
179513
179571
|
* @returns Test results for all stream types and profiles
|
|
179514
179572
|
*/
|
|
179515
179573
|
async testChannelStreams(channel, logger) {
|
|
179516
|
-
const { testChannelStreams } = await import("./DiagnosticsTools-QJ3CRYGA-
|
|
179574
|
+
const { testChannelStreams } = await import("./DiagnosticsTools-QJ3CRYGA-9NV95vRN.mjs");
|
|
179517
179575
|
return await testChannelStreams({
|
|
179518
179576
|
api: this,
|
|
179519
179577
|
channel: this.normalizeChannel(channel),
|
|
@@ -179529,7 +179587,7 @@ ${xml}`);
|
|
|
179529
179587
|
* @returns Complete diagnostics for all channels and streams
|
|
179530
179588
|
*/
|
|
179531
179589
|
async collectMultifocalDiagnostics(logger) {
|
|
179532
|
-
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-QJ3CRYGA-
|
|
179590
|
+
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-QJ3CRYGA-9NV95vRN.mjs");
|
|
179533
179591
|
return await collectMultifocalDiagnostics({
|
|
179534
179592
|
api: this,
|
|
179535
179593
|
logger
|
|
@@ -231278,7 +231336,8 @@ var RawReadSliceSchema = _enum([
|
|
|
231278
231336
|
"osd",
|
|
231279
231337
|
"led",
|
|
231280
231338
|
"pir",
|
|
231281
|
-
"autoReboot"
|
|
231339
|
+
"autoReboot",
|
|
231340
|
+
"battery"
|
|
231282
231341
|
]);
|
|
231283
231342
|
/**
|
|
231284
231343
|
* The allow-list. `Record<RawReadSlice, …>` keeps the catalog and the enum
|
|
@@ -231289,17 +231348,26 @@ var RawReadSliceSchema = _enum([
|
|
|
231289
231348
|
var RAW_READ_CATALOG = {
|
|
231290
231349
|
image: {
|
|
231291
231350
|
command: "getVideoInput",
|
|
231292
|
-
|
|
231351
|
+
believed: {
|
|
231352
|
+
kind: "deviceCache",
|
|
231353
|
+
snapshotKey: "imageSnapshot"
|
|
231354
|
+
},
|
|
231293
231355
|
invoke: (api, channel) => api.getVideoInput(channel)
|
|
231294
231356
|
},
|
|
231295
231357
|
motion: {
|
|
231296
231358
|
command: "getMotionAlarm",
|
|
231297
|
-
|
|
231359
|
+
believed: {
|
|
231360
|
+
kind: "deviceCache",
|
|
231361
|
+
snapshotKey: "motionSnapshot"
|
|
231362
|
+
},
|
|
231298
231363
|
invoke: (api, channel) => api.getMotionAlarm(channel)
|
|
231299
231364
|
},
|
|
231300
231365
|
ai: {
|
|
231301
231366
|
command: "getAiDetectTypes + getAiDetectionFull (per type)",
|
|
231302
|
-
|
|
231367
|
+
believed: {
|
|
231368
|
+
kind: "deviceCache",
|
|
231369
|
+
snapshotKey: "aiSensitivitySnapshot"
|
|
231370
|
+
},
|
|
231303
231371
|
invoke: async (api, channel) => {
|
|
231304
231372
|
const detectTypes = await api.getAiDetectTypes(channel, { timeoutMs: 1500 });
|
|
231305
231373
|
const perType = {};
|
|
@@ -231316,71 +231384,204 @@ var RAW_READ_CATALOG = {
|
|
|
231316
231384
|
},
|
|
231317
231385
|
enc: {
|
|
231318
231386
|
command: "getEnc",
|
|
231319
|
-
|
|
231387
|
+
believed: {
|
|
231388
|
+
kind: "deviceCache",
|
|
231389
|
+
snapshotKey: "encSnapshot"
|
|
231390
|
+
},
|
|
231320
231391
|
invoke: (api, channel) => api.getEnc(channel)
|
|
231321
231392
|
},
|
|
231322
231393
|
encOptions: {
|
|
231323
231394
|
command: "getEncOptions",
|
|
231324
|
-
|
|
231395
|
+
believed: {
|
|
231396
|
+
kind: "deviceCache",
|
|
231397
|
+
snapshotKey: "encOptionsSnapshot"
|
|
231398
|
+
},
|
|
231325
231399
|
invoke: (api, channel) => api.getEncOptions(channel)
|
|
231326
231400
|
},
|
|
231327
231401
|
mask: {
|
|
231328
231402
|
command: "getMask",
|
|
231329
|
-
|
|
231403
|
+
believed: {
|
|
231404
|
+
kind: "deviceCache",
|
|
231405
|
+
snapshotKey: "maskSnapshot"
|
|
231406
|
+
},
|
|
231330
231407
|
invoke: (api, channel) => api.getMask(channel)
|
|
231331
231408
|
},
|
|
231332
231409
|
audioNoise: {
|
|
231333
231410
|
command: "getAudioNoise",
|
|
231334
|
-
|
|
231411
|
+
believed: {
|
|
231412
|
+
kind: "deviceCache",
|
|
231413
|
+
snapshotKey: "audioNoiseSnapshot"
|
|
231414
|
+
},
|
|
231335
231415
|
invoke: (api, channel) => api.getAudioNoise(channel)
|
|
231336
231416
|
},
|
|
231337
231417
|
autofocus: {
|
|
231338
231418
|
command: "getAutoFocus",
|
|
231339
|
-
|
|
231419
|
+
believed: {
|
|
231420
|
+
kind: "deviceCache",
|
|
231421
|
+
snapshotKey: "autoFocusSnapshot"
|
|
231422
|
+
},
|
|
231340
231423
|
invoke: (api, channel) => api.getAutoFocus(channel, { timeoutMs: 1500 })
|
|
231341
231424
|
},
|
|
231342
231425
|
netPort: {
|
|
231343
231426
|
command: "getNetPort",
|
|
231344
|
-
|
|
231427
|
+
believed: {
|
|
231428
|
+
kind: "deviceCache",
|
|
231429
|
+
snapshotKey: "netPortSnapshot"
|
|
231430
|
+
},
|
|
231345
231431
|
invoke: (api) => api.getNetPort()
|
|
231346
231432
|
},
|
|
231347
231433
|
ntp: {
|
|
231348
231434
|
command: "getNtp",
|
|
231349
|
-
|
|
231435
|
+
believed: {
|
|
231436
|
+
kind: "deviceCache",
|
|
231437
|
+
snapshotKey: "ntpSnapshot"
|
|
231438
|
+
},
|
|
231350
231439
|
invoke: (api) => api.getNtp()
|
|
231351
231440
|
},
|
|
231352
231441
|
systemGeneral: {
|
|
231353
231442
|
command: "getSystemGeneral",
|
|
231354
|
-
|
|
231443
|
+
believed: {
|
|
231444
|
+
kind: "deviceCache",
|
|
231445
|
+
snapshotKey: "systemGeneralSnapshot"
|
|
231446
|
+
},
|
|
231355
231447
|
invoke: (api) => api.getSystemGeneral()
|
|
231356
231448
|
},
|
|
231357
231449
|
osd: {
|
|
231358
231450
|
command: "getOsd",
|
|
231359
|
-
|
|
231451
|
+
believed: {
|
|
231452
|
+
kind: "deviceCache",
|
|
231453
|
+
snapshotKey: "osdSnapshot"
|
|
231454
|
+
},
|
|
231360
231455
|
invoke: (api, channel) => api.getOsd(channel)
|
|
231361
231456
|
},
|
|
231362
231457
|
led: {
|
|
231363
231458
|
command: "getIrLights",
|
|
231364
|
-
|
|
231459
|
+
believed: {
|
|
231460
|
+
kind: "deviceCache",
|
|
231461
|
+
snapshotKey: "ledSnapshot"
|
|
231462
|
+
},
|
|
231365
231463
|
invoke: (api, channel) => api.getIrLights(channel)
|
|
231366
231464
|
},
|
|
231367
231465
|
pir: {
|
|
231368
231466
|
command: "getPirInfo",
|
|
231369
|
-
|
|
231467
|
+
believed: {
|
|
231468
|
+
kind: "deviceCache",
|
|
231469
|
+
snapshotKey: "pirSnapshot"
|
|
231470
|
+
},
|
|
231370
231471
|
invoke: (api, channel) => api.getPirInfo(channel)
|
|
231371
231472
|
},
|
|
231372
231473
|
autoReboot: {
|
|
231373
231474
|
command: "getAutoReboot",
|
|
231374
|
-
|
|
231475
|
+
believed: {
|
|
231476
|
+
kind: "deviceCache",
|
|
231477
|
+
snapshotKey: "autoRebootSnapshot"
|
|
231478
|
+
},
|
|
231375
231479
|
invoke: (api) => api.getAutoReboot()
|
|
231480
|
+
},
|
|
231481
|
+
/**
|
|
231482
|
+
* Battery — one slice, TWO firmware reads, chosen by how the camera is
|
|
231483
|
+
* attached. They are not variants of one call:
|
|
231484
|
+
*
|
|
231485
|
+
* - **standalone**: `getBatteryInfo(channel)` — the camera's own
|
|
231486
|
+
* answer, one `BatteryInfo`;
|
|
231487
|
+
* - **hub child**: `getAllChannelsBatteryInfo()` — a HUB-level CGI
|
|
231488
|
+
* keyed by channel, projected to the requested one here so the slice
|
|
231489
|
+
* stays per-camera like the other fifteen (a "cluster-wide" second
|
|
231490
|
+
* slice shape would have to be understood by every caller).
|
|
231491
|
+
*
|
|
231492
|
+
* Both vocabularies carry `adapterStatus` next to `chargeStatus`, which
|
|
231493
|
+
* is why this slice exists: the provider keeps only the latter, and
|
|
231494
|
+
* whether `adapterStatus` MOVES was previously observable exactly once
|
|
231495
|
+
* per hub process (`ReolinkHub.batteryShapeLogged`). The answer is
|
|
231496
|
+
* returned as the firmware gave it, under a `readVia` that names the path
|
|
231497
|
+
* — NOT normalised into a shared shape. The paths do not carry the same
|
|
231498
|
+
* fields (`voltage`/`lowPower` on the camera read, `channelsReported` and
|
|
231499
|
+
* the channel-status row on the hub read); a field the firmware did not
|
|
231500
|
+
* send must be ABSENT, because a default here would be indistinguishable
|
|
231501
|
+
* from a reading (D315).
|
|
231502
|
+
*
|
|
231503
|
+
* A camera that cannot answer at all throws, and the read comes back
|
|
231504
|
+
* `read-failed` carrying the firmware's reason — "we could not read it"
|
|
231505
|
+
* must never be served as "it has no battery".
|
|
231506
|
+
*/
|
|
231507
|
+
battery: {
|
|
231508
|
+
command: "getBatteryInfo (standalone) / getAllChannelsBatteryInfo projected (hub child)",
|
|
231509
|
+
believed: {
|
|
231510
|
+
kind: "capRuntimeState",
|
|
231511
|
+
capName: "battery"
|
|
231512
|
+
},
|
|
231513
|
+
servedByParentHub: true,
|
|
231514
|
+
invoke: async (api, channel, ctx) => {
|
|
231515
|
+
if (!ctx.hubChild) return {
|
|
231516
|
+
readVia: "camera",
|
|
231517
|
+
command: "getBatteryInfo",
|
|
231518
|
+
channel,
|
|
231519
|
+
batteryInfo: await api.getBatteryInfo(channel)
|
|
231520
|
+
};
|
|
231521
|
+
const byChannel = (await api.getAllChannelsBatteryInfo()).batteryInfoData;
|
|
231522
|
+
const channelsReported = Object.keys(byChannel).map(Number).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
|
|
231523
|
+
const info = byChannel[channel];
|
|
231524
|
+
if (info === void 0) throw new Error(`getAllChannelsBatteryInfo answered for channels [${channelsReported.join(", ")}] but not channel ${channel} — this camera's battery was NOT reported. That is an unanswered read, not "no battery".`);
|
|
231525
|
+
const [cgiBattery, channelStatus] = info.entries;
|
|
231526
|
+
return {
|
|
231527
|
+
readVia: "parentHub",
|
|
231528
|
+
command: "getAllChannelsBatteryInfo",
|
|
231529
|
+
channel,
|
|
231530
|
+
channelsReported,
|
|
231531
|
+
batteryInfo: {
|
|
231532
|
+
...info,
|
|
231533
|
+
entries: [cgiBattery ?? null, redactChannelIdentity(channelStatus)]
|
|
231534
|
+
}
|
|
231535
|
+
};
|
|
231536
|
+
}
|
|
231376
231537
|
}
|
|
231377
231538
|
};
|
|
231539
|
+
/**
|
|
231540
|
+
* The hub's channel-status row (`entries[1]`) carries the camera's Reolink
|
|
231541
|
+
* P2P `uid` — a routable cloud identifier for the device. A charge-state
|
|
231542
|
+
* read has no reason to hand it out, so it is dropped here rather than
|
|
231543
|
+
* relied on not to be looked at. The drop is STATED (`uidRedacted`): a
|
|
231544
|
+
* removed field and a firmware that never sent one must not look alike.
|
|
231545
|
+
* Everything else in the row (channel, name, online, sleep, typeInfo) is
|
|
231546
|
+
* kept verbatim — it is the context that makes the battery row readable.
|
|
231547
|
+
*/
|
|
231548
|
+
function redactChannelIdentity(entry) {
|
|
231549
|
+
if (entry === void 0) return null;
|
|
231550
|
+
const { uid, ...rest } = entry;
|
|
231551
|
+
return {
|
|
231552
|
+
...rest,
|
|
231553
|
+
uidRedacted: uid !== void 0
|
|
231554
|
+
};
|
|
231555
|
+
}
|
|
231556
|
+
/**
|
|
231557
|
+
* Age of a `capRuntimeState` belief. The cap slice carries `lastUpdated`
|
|
231558
|
+
* rather than a freshness-map stamp, and its `empty` seed is `0` — which is
|
|
231559
|
+
* NOT an observation, so it resolves to `never`. A slice that was never
|
|
231560
|
+
* written at all is `never` too: on a battery reading, "we hold no value"
|
|
231561
|
+
* and "we measured 0%" are an alarm apart (see `BatteryStatusSchema`).
|
|
231562
|
+
*/
|
|
231563
|
+
function capStateAge(state, now) {
|
|
231564
|
+
if (state === void 0) return { state: "never" };
|
|
231565
|
+
const lastUpdated = state.lastUpdated;
|
|
231566
|
+
if (typeof lastUpdated !== "number" || lastUpdated <= 0) return { state: "never" };
|
|
231567
|
+
return {
|
|
231568
|
+
state: "known",
|
|
231569
|
+
fetchedAt: lastUpdated,
|
|
231570
|
+
ageMs: Math.max(0, now - lastUpdated)
|
|
231571
|
+
};
|
|
231572
|
+
}
|
|
231378
231573
|
/** What CamStack currently believes about the slice, with its own age. */
|
|
231379
231574
|
var BelievedStateSchema = object({
|
|
231380
|
-
/** The persisted projection
|
|
231575
|
+
/** The persisted projection / cap slice, verbatim. */
|
|
231381
231576
|
snapshot: unknown(),
|
|
231382
|
-
/**
|
|
231383
|
-
|
|
231577
|
+
/** Where that belief lives — mirrors {@link BelievedSource}. */
|
|
231578
|
+
source: discriminatedUnion("kind", [object({
|
|
231579
|
+
kind: literal("deviceCache"),
|
|
231580
|
+
snapshotKey: string()
|
|
231581
|
+
}), object({
|
|
231582
|
+
kind: literal("capRuntimeState"),
|
|
231583
|
+
capName: literal("battery")
|
|
231584
|
+
})]),
|
|
231384
231585
|
/** Tri-state freshness — `never` / `unknown` (legacy, no stamp) / `known`. */
|
|
231385
231586
|
age: union([
|
|
231386
231587
|
object({ state: literal("never") }),
|
|
@@ -231419,11 +231620,17 @@ var RawReadInputSchema = object({
|
|
|
231419
231620
|
deviceId: number().int().nonnegative(),
|
|
231420
231621
|
slice: RawReadSliceSchema
|
|
231421
231622
|
});
|
|
231422
|
-
function believedState(
|
|
231423
|
-
const
|
|
231623
|
+
function believedState(deps, entry) {
|
|
231624
|
+
const source = entry.believed;
|
|
231625
|
+
if (source.kind === "capRuntimeState") return {
|
|
231626
|
+
snapshot: toPlainJson(deps.batteryState),
|
|
231627
|
+
source,
|
|
231628
|
+
age: capStateAge(deps.batteryState, deps.now)
|
|
231629
|
+
};
|
|
231630
|
+
const age = resolveSnapshotAge(deps.cache, source.snapshotKey, deps.now);
|
|
231424
231631
|
return {
|
|
231425
|
-
snapshot: toPlainJson(cache === void 0 ? void 0 : Reflect.get(cache,
|
|
231426
|
-
|
|
231632
|
+
snapshot: toPlainJson(deps.cache === void 0 ? void 0 : Reflect.get(deps.cache, source.snapshotKey)),
|
|
231633
|
+
source,
|
|
231427
231634
|
age
|
|
231428
231635
|
};
|
|
231429
231636
|
}
|
|
@@ -231443,8 +231650,9 @@ function toPlainJson(value) {
|
|
|
231443
231650
|
*/
|
|
231444
231651
|
async function performRawRead(slice, deps) {
|
|
231445
231652
|
const entry = RAW_READ_CATALOG[slice];
|
|
231446
|
-
const believed = believedState(deps
|
|
231447
|
-
|
|
231653
|
+
const believed = believedState(deps, entry);
|
|
231654
|
+
const servedWithoutTouchingCamera = deps.hubChild && entry.servedByParentHub === true;
|
|
231655
|
+
if (deps.sleeping && !servedWithoutTouchingCamera) {
|
|
231448
231656
|
deps.logger.info("reolink raw read refused — battery cam is sleeping", {
|
|
231449
231657
|
tags: { deviceId: deps.deviceId },
|
|
231450
231658
|
meta: { slice }
|
|
@@ -231454,7 +231662,7 @@ async function performRawRead(slice, deps) {
|
|
|
231454
231662
|
deviceId: deps.deviceId,
|
|
231455
231663
|
slice,
|
|
231456
231664
|
reason: "sleeping",
|
|
231457
|
-
message: "Battery camera is asleep — a raw read would wake it, so it is refused. The believed state below is what CamStack currently serves.",
|
|
231665
|
+
message: deps.hubChild ? `Battery camera is asleep and the "${slice}" read is addressed to the camera channel, which would wake it — refused. The believed state below is what CamStack currently serves. (The "battery" slice is answered by the parent hub and stays readable while the camera sleeps.)` : "Battery camera is asleep — a raw read would wake it, so it is refused. The believed state below is what CamStack currently serves.",
|
|
231458
231666
|
believed
|
|
231459
231667
|
};
|
|
231460
231668
|
}
|
|
@@ -231480,12 +231688,14 @@ async function performRawRead(slice, deps) {
|
|
|
231480
231688
|
};
|
|
231481
231689
|
}
|
|
231482
231690
|
try {
|
|
231483
|
-
const payload = await entry.invoke(api, deps.channel);
|
|
231691
|
+
const payload = await entry.invoke(api, deps.channel, { hubChild: deps.hubChild });
|
|
231484
231692
|
deps.logger.info("reolink raw read served", {
|
|
231485
231693
|
tags: { deviceId: deps.deviceId },
|
|
231486
231694
|
meta: {
|
|
231487
231695
|
slice,
|
|
231488
|
-
command: entry.command
|
|
231696
|
+
command: entry.command,
|
|
231697
|
+
viaParentHub: servedWithoutTouchingCamera,
|
|
231698
|
+
cameraWasSleeping: deps.sleeping
|
|
231489
231699
|
}
|
|
231490
231700
|
});
|
|
231491
231701
|
return {
|
|
@@ -231544,6 +231754,67 @@ var reolinkDebugActions = defineCustomActions({ debugRawRead: customAction(RawRe
|
|
|
231544
231754
|
auth: "admin"
|
|
231545
231755
|
}) });
|
|
231546
231756
|
//#endregion
|
|
231757
|
+
//#region src/battery-charging.ts
|
|
231758
|
+
/**
|
|
231759
|
+
* Da `adapterStatus` + `chargeStatus` alla sorgente di alimentazione.
|
|
231760
|
+
*
|
|
231761
|
+
* I due campi rispondono a DUE domande diverse, e confonderle è il bug che
|
|
231762
|
+
* questo modulo esiste per chiudere:
|
|
231763
|
+
*
|
|
231764
|
+
* - `adapterStatus` — l'alimentazione è COLLEGATA? Il firmware manda 0/1/2
|
|
231765
|
+
* (nessuna · adattatore DC · pannello solare) e il confine dell'hub lo
|
|
231766
|
+
* traduce in `'none' | 'dc' | 'solarPanel'` (`mapHubChargeStatus` e
|
|
231767
|
+
* `mapHubAdapterStatus` in `reolink-hub.ts`), perché il vocabolario del
|
|
231768
|
+
* percorso standalone è già a stringhe.
|
|
231769
|
+
* - `chargeStatus` — la CELLA cosa sta facendo? 0 ferma · 1 in carica ·
|
|
231770
|
+
* 2 carica completa.
|
|
231771
|
+
*
|
|
231772
|
+
* La vecchia derivazione consultava l'adattatore SOLO per cercarci 'solar', e
|
|
231773
|
+
* un DC collegato cadeva nel ripiego `'none'`. Due Argus 3E lo nascondevano per
|
|
231774
|
+
* caso: piene e collegate rispondono `chargeStatus: 2`, che il ramo `isCharging`
|
|
231775
|
+
* mappava su `'dc'` per la ragione sbagliata. La Argus MagiCam (batteryVersion
|
|
231776
|
+
* 1, firmware 2026) collegata in permanenza risponde `chargeStatus: 0`, e per
|
|
231777
|
+
* tutta la sua vita è stata riportata NON in carica.
|
|
231778
|
+
*
|
|
231779
|
+
* Che `adapterStatus` sia un segnale vero e non una costante è MISURATO: il
|
|
231780
|
+
* 2026-09-05 l'operatore ha staccato il device 3628 e il payload grezzo è
|
|
231781
|
+
* passato da `adapterStatus: 1` a `0`, con `chargeStatus: 0` da entrambe le
|
|
231782
|
+
* parti — cioè `chargeStatus` da solo NON distingue i due stati.
|
|
231783
|
+
*
|
|
231784
|
+
* ⚠️ Questa funzione non sa ancora dire "non lo so": `BatteryStatus['charging']`
|
|
231785
|
+
* non ha un valore `unknown`, quindi due campi illeggibili producono `'none'`,
|
|
231786
|
+
* che è un valore NEGATIVO al posto di un non-noto. È il difetto D315 che
|
|
231787
|
+
* `percentage` ha già chiuso (è `nullable`) e questo campo no; chiuderlo tocca
|
|
231788
|
+
* `battery.cap.ts`, cioè la closure del server, e vive nel proprio lavoro.
|
|
231789
|
+
*/
|
|
231790
|
+
function deriveChargingSource(adapterStatus, chargeStatus) {
|
|
231791
|
+
const adapter = (adapterStatus ?? "").toLowerCase();
|
|
231792
|
+
const charge = (chargeStatus ?? "").toLowerCase();
|
|
231793
|
+
if (adapter.includes("solar")) return "solar";
|
|
231794
|
+
if (adapter === "dc") return "dc";
|
|
231795
|
+
if (charge === "charging" || charge === "chargecomplete") return "dc";
|
|
231796
|
+
return "none";
|
|
231797
|
+
}
|
|
231798
|
+
/**
|
|
231799
|
+
* Did this reading SAY anything about the power source? A battery push may
|
|
231800
|
+
* carry only the level (every `BatteryInfo` field is optional and nodelink
|
|
231801
|
+
* dispatches as soon as one is present); a caller that derives `charging`
|
|
231802
|
+
* from such a push turns "nothing was said" into 'none' and overwrites a known
|
|
231803
|
+
* 'dc'. When this is false the caller keeps its previous belief.
|
|
231804
|
+
*/
|
|
231805
|
+
function reportsPowerSource(info) {
|
|
231806
|
+
return info.adapterStatus !== void 0 || info.chargeStatus !== void 0;
|
|
231807
|
+
}
|
|
231808
|
+
//#endregion
|
|
231809
|
+
//#region src/battery-contact-expiry.ts
|
|
231810
|
+
function shouldWakeForContact(input) {
|
|
231811
|
+
if (input.hubChild) return false;
|
|
231812
|
+
if (!input.sleeping) return false;
|
|
231813
|
+
const last = input.lastContactAt;
|
|
231814
|
+
if (typeof last !== "number" || last <= 0) return false;
|
|
231815
|
+
return input.nowMs - last + input.tickMs >= input.unreachableAfterMs;
|
|
231816
|
+
}
|
|
231817
|
+
//#endregion
|
|
231547
231818
|
//#region src/log-channels.ts
|
|
231548
231819
|
/**
|
|
231549
231820
|
* The diagnostic log CHANNELS `provider-reolink` declares.
|
|
@@ -236319,8 +236590,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
236319
236590
|
deviceId: this.id,
|
|
236320
236591
|
channel: this.getChannel(),
|
|
236321
236592
|
sleeping: this.isBattery && this.sleeping,
|
|
236593
|
+
hubChild: this.isHubChild(),
|
|
236322
236594
|
getApi: () => this.ensureApi(),
|
|
236323
236595
|
cache: this.config.get("deviceCache"),
|
|
236596
|
+
batteryState: this.runtimeState.getCapState("battery"),
|
|
236324
236597
|
logger: this.ctx.logger,
|
|
236325
236598
|
now: Date.now()
|
|
236326
236599
|
});
|
|
@@ -236651,13 +236924,9 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
236651
236924
|
* tracker is more reliable.
|
|
236652
236925
|
*/
|
|
236653
236926
|
mapBatteryInfo(info) {
|
|
236654
|
-
const percentage = typeof info.batteryPercent === "number" ? Math.max(0, Math.min(100, Math.round(info.batteryPercent))) : this.state.battery.percentage ?? null;
|
|
236655
|
-
const adapter = (info.adapterStatus ?? "").toLowerCase();
|
|
236656
|
-
const charge = (info.chargeStatus ?? "").toLowerCase();
|
|
236657
|
-
const isCharging = charge === "charging" || charge === "chargecomplete";
|
|
236658
236927
|
return {
|
|
236659
|
-
percentage,
|
|
236660
|
-
charging:
|
|
236928
|
+
percentage: typeof info.batteryPercent === "number" ? Math.max(0, Math.min(100, Math.round(info.batteryPercent))) : this.state.battery.percentage ?? null,
|
|
236929
|
+
charging: reportsPowerSource(info) ? deriveChargingSource(info.adapterStatus, info.chargeStatus) : this.state.battery.charging ?? "none",
|
|
236661
236930
|
sleeping: info.sleeping === true || this.sleeping,
|
|
236662
236931
|
lastUpdated: Date.now()
|
|
236663
236932
|
};
|
|
@@ -240206,6 +240475,17 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
240206
240475
|
const cycle = (async () => {
|
|
240207
240476
|
try {
|
|
240208
240477
|
if (this.isBattery && this.sleeping) {
|
|
240478
|
+
if (shouldWakeForContact({
|
|
240479
|
+
nowMs: Date.now(),
|
|
240480
|
+
lastContactAt: this.state.battery.lastContactAt,
|
|
240481
|
+
sleeping: true,
|
|
240482
|
+
hubChild: this.isHubChild(),
|
|
240483
|
+
unreachableAfterMs: 216e5,
|
|
240484
|
+
tickMs: ReolinkCamera.BATTERY_UPDATE_INTERVAL_MS
|
|
240485
|
+
})) {
|
|
240486
|
+
await this.wakeForContactRefresh();
|
|
240487
|
+
return;
|
|
240488
|
+
}
|
|
240209
240489
|
this.ctx.logger.debug("battery-update: cam sleeping — skipping cycle without connecting", { tags: { deviceId: this.id } });
|
|
240210
240490
|
return;
|
|
240211
240491
|
}
|
|
@@ -240250,6 +240530,43 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
240250
240530
|
this.batteryUpdateInFlight = cycle;
|
|
240251
240531
|
return cycle;
|
|
240252
240532
|
}
|
|
240533
|
+
/**
|
|
240534
|
+
* D355: wake a standalone battery camera ONCE because its last passive
|
|
240535
|
+
* contact is about to expire, and refresh every slice while it is up.
|
|
240536
|
+
* Bounded by `canProactivelyWake` (the shared cooldown) and by the fact
|
|
240537
|
+
* that a wake that answers resets the silence. A wake that fails is left
|
|
240538
|
+
* alone: the silence then reaches the budget and `deriveBatteryPresence`
|
|
240539
|
+
* says `unreachable` — for the first time on evidence.
|
|
240540
|
+
*/
|
|
240541
|
+
async wakeForContactRefresh() {
|
|
240542
|
+
if (!this.canProactivelyWake("contact-expiry")) return;
|
|
240543
|
+
this.markWakeIssued();
|
|
240544
|
+
const lastContactAt = this.state.battery.lastContactAt ?? 0;
|
|
240545
|
+
const silenceMs = Date.now() - lastContactAt;
|
|
240546
|
+
this.ctx.logger.info("battery contact expiring — waking once to refresh every slice (D355)", {
|
|
240547
|
+
tags: { deviceId: this.id },
|
|
240548
|
+
meta: {
|
|
240549
|
+
silenceMs,
|
|
240550
|
+
budgetMs: BATTERY_UNREACHABLE_AFTER_MS
|
|
240551
|
+
}
|
|
240552
|
+
});
|
|
240553
|
+
try {
|
|
240554
|
+
await this.ensureApi();
|
|
240555
|
+
} catch (err) {
|
|
240556
|
+
this.ctx.logger.warn("battery contact wake FAILED — silence continues and the camera will read unreachable at the budget", {
|
|
240557
|
+
tags: { deviceId: this.id },
|
|
240558
|
+
meta: {
|
|
240559
|
+
silenceMs,
|
|
240560
|
+
error: err instanceof Error ? err.message : String(err)
|
|
240561
|
+
}
|
|
240562
|
+
});
|
|
240563
|
+
return;
|
|
240564
|
+
}
|
|
240565
|
+
await this.refreshBatteryFromApi("demand").catch(() => {});
|
|
240566
|
+
await this.warmCapSlicesOnWake().catch(() => {});
|
|
240567
|
+
await this.alignAuxDevicesState("wake").catch(() => {});
|
|
240568
|
+
await this.warmSnapshotOnWake().catch(() => {});
|
|
240569
|
+
}
|
|
240253
240570
|
/** Arm the periodic battery-cam refresh timer. No-op for wired cams
|
|
240254
240571
|
* (their state is kept fresh by the 10 s aux-align poll and push
|
|
240255
240572
|
* events). Idempotent. */
|
|
@@ -245165,4 +245482,4 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
245165
245482
|
}
|
|
245166
245483
|
};
|
|
245167
245484
|
//#endregion
|
|
245168
|
-
export { ReolinkProviderAddon,
|
|
245485
|
+
export { ReolinkProviderAddon, collectNativeDiagnostics as a, runAllDiagnosticsConsecutively as c, reolinkDebugActions as customActions, testChannelStreams as d, collectMultifocalDiagnostics as i, runMultifocalDiagnosticsConsecutively as l, reolinkCameraSchema as n, collectNvrDiagnostics as o, collectCgiDiagnostics as r, createDiagnosticsBundle as s, ReolinkCamera as t, sampleStreams as u };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { ReolinkProviderAddon,
|
|
1
|
+
import { ReolinkProviderAddon, customActions as reolinkDebugActions, n as reolinkCameraSchema, t as ReolinkCamera } from "./addon.mjs";
|
|
2
2
|
export { ReolinkCamera, ReolinkProviderAddon, reolinkDebugActions as customActions, reolinkCameraSchema };
|
package/package.json
CHANGED