@camstack/addon-provider-reolink 1.2.86 → 1.2.87
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 +279 -34
- package/dist/addon.mjs +281 -37
- 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
|
|
@@ -19298,6 +19316,9 @@ var RetrainStatusSchema = _enum([
|
|
|
19298
19316
|
*
|
|
19299
19317
|
* `debug` does NOT pin; it is attention, not durability.
|
|
19300
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
|
+
*
|
|
19301
19322
|
* `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
|
|
19302
19323
|
* A favourited track is skipped by retention the same way `staging` is, but
|
|
19303
19324
|
* it does not enter `none|staging|trained` and has no staging budget.
|
|
@@ -19308,6 +19329,21 @@ var TrackFlagFields = {
|
|
|
19308
19329
|
markForTrain: boolean().optional(),
|
|
19309
19330
|
/** Operator marked this track for diagnostic attention. */
|
|
19310
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(),
|
|
19311
19347
|
/** Operator favourited this track. Pins it against pruning. */
|
|
19312
19348
|
favourited: boolean().optional()
|
|
19313
19349
|
};
|
|
@@ -19334,6 +19370,12 @@ var TrackFlagsSchema = object({
|
|
|
19334
19370
|
trackId: string(),
|
|
19335
19371
|
markForTrain: boolean(),
|
|
19336
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(),
|
|
19337
19379
|
favourited: boolean(),
|
|
19338
19380
|
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
19339
19381
|
* a track row) because this shape is only ever produced by the write body,
|
|
@@ -231298,7 +231340,8 @@ var RawReadSliceSchema = _enum([
|
|
|
231298
231340
|
"osd",
|
|
231299
231341
|
"led",
|
|
231300
231342
|
"pir",
|
|
231301
|
-
"autoReboot"
|
|
231343
|
+
"autoReboot",
|
|
231344
|
+
"battery"
|
|
231302
231345
|
]);
|
|
231303
231346
|
/**
|
|
231304
231347
|
* The allow-list. `Record<RawReadSlice, …>` keeps the catalog and the enum
|
|
@@ -231309,17 +231352,26 @@ var RawReadSliceSchema = _enum([
|
|
|
231309
231352
|
var RAW_READ_CATALOG = {
|
|
231310
231353
|
image: {
|
|
231311
231354
|
command: "getVideoInput",
|
|
231312
|
-
|
|
231355
|
+
believed: {
|
|
231356
|
+
kind: "deviceCache",
|
|
231357
|
+
snapshotKey: "imageSnapshot"
|
|
231358
|
+
},
|
|
231313
231359
|
invoke: (api, channel) => api.getVideoInput(channel)
|
|
231314
231360
|
},
|
|
231315
231361
|
motion: {
|
|
231316
231362
|
command: "getMotionAlarm",
|
|
231317
|
-
|
|
231363
|
+
believed: {
|
|
231364
|
+
kind: "deviceCache",
|
|
231365
|
+
snapshotKey: "motionSnapshot"
|
|
231366
|
+
},
|
|
231318
231367
|
invoke: (api, channel) => api.getMotionAlarm(channel)
|
|
231319
231368
|
},
|
|
231320
231369
|
ai: {
|
|
231321
231370
|
command: "getAiDetectTypes + getAiDetectionFull (per type)",
|
|
231322
|
-
|
|
231371
|
+
believed: {
|
|
231372
|
+
kind: "deviceCache",
|
|
231373
|
+
snapshotKey: "aiSensitivitySnapshot"
|
|
231374
|
+
},
|
|
231323
231375
|
invoke: async (api, channel) => {
|
|
231324
231376
|
const detectTypes = await api.getAiDetectTypes(channel, { timeoutMs: 1500 });
|
|
231325
231377
|
const perType = {};
|
|
@@ -231336,71 +231388,204 @@ var RAW_READ_CATALOG = {
|
|
|
231336
231388
|
},
|
|
231337
231389
|
enc: {
|
|
231338
231390
|
command: "getEnc",
|
|
231339
|
-
|
|
231391
|
+
believed: {
|
|
231392
|
+
kind: "deviceCache",
|
|
231393
|
+
snapshotKey: "encSnapshot"
|
|
231394
|
+
},
|
|
231340
231395
|
invoke: (api, channel) => api.getEnc(channel)
|
|
231341
231396
|
},
|
|
231342
231397
|
encOptions: {
|
|
231343
231398
|
command: "getEncOptions",
|
|
231344
|
-
|
|
231399
|
+
believed: {
|
|
231400
|
+
kind: "deviceCache",
|
|
231401
|
+
snapshotKey: "encOptionsSnapshot"
|
|
231402
|
+
},
|
|
231345
231403
|
invoke: (api, channel) => api.getEncOptions(channel)
|
|
231346
231404
|
},
|
|
231347
231405
|
mask: {
|
|
231348
231406
|
command: "getMask",
|
|
231349
|
-
|
|
231407
|
+
believed: {
|
|
231408
|
+
kind: "deviceCache",
|
|
231409
|
+
snapshotKey: "maskSnapshot"
|
|
231410
|
+
},
|
|
231350
231411
|
invoke: (api, channel) => api.getMask(channel)
|
|
231351
231412
|
},
|
|
231352
231413
|
audioNoise: {
|
|
231353
231414
|
command: "getAudioNoise",
|
|
231354
|
-
|
|
231415
|
+
believed: {
|
|
231416
|
+
kind: "deviceCache",
|
|
231417
|
+
snapshotKey: "audioNoiseSnapshot"
|
|
231418
|
+
},
|
|
231355
231419
|
invoke: (api, channel) => api.getAudioNoise(channel)
|
|
231356
231420
|
},
|
|
231357
231421
|
autofocus: {
|
|
231358
231422
|
command: "getAutoFocus",
|
|
231359
|
-
|
|
231423
|
+
believed: {
|
|
231424
|
+
kind: "deviceCache",
|
|
231425
|
+
snapshotKey: "autoFocusSnapshot"
|
|
231426
|
+
},
|
|
231360
231427
|
invoke: (api, channel) => api.getAutoFocus(channel, { timeoutMs: 1500 })
|
|
231361
231428
|
},
|
|
231362
231429
|
netPort: {
|
|
231363
231430
|
command: "getNetPort",
|
|
231364
|
-
|
|
231431
|
+
believed: {
|
|
231432
|
+
kind: "deviceCache",
|
|
231433
|
+
snapshotKey: "netPortSnapshot"
|
|
231434
|
+
},
|
|
231365
231435
|
invoke: (api) => api.getNetPort()
|
|
231366
231436
|
},
|
|
231367
231437
|
ntp: {
|
|
231368
231438
|
command: "getNtp",
|
|
231369
|
-
|
|
231439
|
+
believed: {
|
|
231440
|
+
kind: "deviceCache",
|
|
231441
|
+
snapshotKey: "ntpSnapshot"
|
|
231442
|
+
},
|
|
231370
231443
|
invoke: (api) => api.getNtp()
|
|
231371
231444
|
},
|
|
231372
231445
|
systemGeneral: {
|
|
231373
231446
|
command: "getSystemGeneral",
|
|
231374
|
-
|
|
231447
|
+
believed: {
|
|
231448
|
+
kind: "deviceCache",
|
|
231449
|
+
snapshotKey: "systemGeneralSnapshot"
|
|
231450
|
+
},
|
|
231375
231451
|
invoke: (api) => api.getSystemGeneral()
|
|
231376
231452
|
},
|
|
231377
231453
|
osd: {
|
|
231378
231454
|
command: "getOsd",
|
|
231379
|
-
|
|
231455
|
+
believed: {
|
|
231456
|
+
kind: "deviceCache",
|
|
231457
|
+
snapshotKey: "osdSnapshot"
|
|
231458
|
+
},
|
|
231380
231459
|
invoke: (api, channel) => api.getOsd(channel)
|
|
231381
231460
|
},
|
|
231382
231461
|
led: {
|
|
231383
231462
|
command: "getIrLights",
|
|
231384
|
-
|
|
231463
|
+
believed: {
|
|
231464
|
+
kind: "deviceCache",
|
|
231465
|
+
snapshotKey: "ledSnapshot"
|
|
231466
|
+
},
|
|
231385
231467
|
invoke: (api, channel) => api.getIrLights(channel)
|
|
231386
231468
|
},
|
|
231387
231469
|
pir: {
|
|
231388
231470
|
command: "getPirInfo",
|
|
231389
|
-
|
|
231471
|
+
believed: {
|
|
231472
|
+
kind: "deviceCache",
|
|
231473
|
+
snapshotKey: "pirSnapshot"
|
|
231474
|
+
},
|
|
231390
231475
|
invoke: (api, channel) => api.getPirInfo(channel)
|
|
231391
231476
|
},
|
|
231392
231477
|
autoReboot: {
|
|
231393
231478
|
command: "getAutoReboot",
|
|
231394
|
-
|
|
231479
|
+
believed: {
|
|
231480
|
+
kind: "deviceCache",
|
|
231481
|
+
snapshotKey: "autoRebootSnapshot"
|
|
231482
|
+
},
|
|
231395
231483
|
invoke: (api) => api.getAutoReboot()
|
|
231484
|
+
},
|
|
231485
|
+
/**
|
|
231486
|
+
* Battery — one slice, TWO firmware reads, chosen by how the camera is
|
|
231487
|
+
* attached. They are not variants of one call:
|
|
231488
|
+
*
|
|
231489
|
+
* - **standalone**: `getBatteryInfo(channel)` — the camera's own
|
|
231490
|
+
* answer, one `BatteryInfo`;
|
|
231491
|
+
* - **hub child**: `getAllChannelsBatteryInfo()` — a HUB-level CGI
|
|
231492
|
+
* keyed by channel, projected to the requested one here so the slice
|
|
231493
|
+
* stays per-camera like the other fifteen (a "cluster-wide" second
|
|
231494
|
+
* slice shape would have to be understood by every caller).
|
|
231495
|
+
*
|
|
231496
|
+
* Both vocabularies carry `adapterStatus` next to `chargeStatus`, which
|
|
231497
|
+
* is why this slice exists: the provider keeps only the latter, and
|
|
231498
|
+
* whether `adapterStatus` MOVES was previously observable exactly once
|
|
231499
|
+
* per hub process (`ReolinkHub.batteryShapeLogged`). The answer is
|
|
231500
|
+
* returned as the firmware gave it, under a `readVia` that names the path
|
|
231501
|
+
* — NOT normalised into a shared shape. The paths do not carry the same
|
|
231502
|
+
* fields (`voltage`/`lowPower` on the camera read, `channelsReported` and
|
|
231503
|
+
* the channel-status row on the hub read); a field the firmware did not
|
|
231504
|
+
* send must be ABSENT, because a default here would be indistinguishable
|
|
231505
|
+
* from a reading (D315).
|
|
231506
|
+
*
|
|
231507
|
+
* A camera that cannot answer at all throws, and the read comes back
|
|
231508
|
+
* `read-failed` carrying the firmware's reason — "we could not read it"
|
|
231509
|
+
* must never be served as "it has no battery".
|
|
231510
|
+
*/
|
|
231511
|
+
battery: {
|
|
231512
|
+
command: "getBatteryInfo (standalone) / getAllChannelsBatteryInfo projected (hub child)",
|
|
231513
|
+
believed: {
|
|
231514
|
+
kind: "capRuntimeState",
|
|
231515
|
+
capName: "battery"
|
|
231516
|
+
},
|
|
231517
|
+
servedByParentHub: true,
|
|
231518
|
+
invoke: async (api, channel, ctx) => {
|
|
231519
|
+
if (!ctx.hubChild) return {
|
|
231520
|
+
readVia: "camera",
|
|
231521
|
+
command: "getBatteryInfo",
|
|
231522
|
+
channel,
|
|
231523
|
+
batteryInfo: await api.getBatteryInfo(channel)
|
|
231524
|
+
};
|
|
231525
|
+
const byChannel = (await api.getAllChannelsBatteryInfo()).batteryInfoData;
|
|
231526
|
+
const channelsReported = Object.keys(byChannel).map(Number).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
|
|
231527
|
+
const info = byChannel[channel];
|
|
231528
|
+
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".`);
|
|
231529
|
+
const [cgiBattery, channelStatus] = info.entries;
|
|
231530
|
+
return {
|
|
231531
|
+
readVia: "parentHub",
|
|
231532
|
+
command: "getAllChannelsBatteryInfo",
|
|
231533
|
+
channel,
|
|
231534
|
+
channelsReported,
|
|
231535
|
+
batteryInfo: {
|
|
231536
|
+
...info,
|
|
231537
|
+
entries: [cgiBattery ?? null, redactChannelIdentity(channelStatus)]
|
|
231538
|
+
}
|
|
231539
|
+
};
|
|
231540
|
+
}
|
|
231396
231541
|
}
|
|
231397
231542
|
};
|
|
231543
|
+
/**
|
|
231544
|
+
* The hub's channel-status row (`entries[1]`) carries the camera's Reolink
|
|
231545
|
+
* P2P `uid` — a routable cloud identifier for the device. A charge-state
|
|
231546
|
+
* read has no reason to hand it out, so it is dropped here rather than
|
|
231547
|
+
* relied on not to be looked at. The drop is STATED (`uidRedacted`): a
|
|
231548
|
+
* removed field and a firmware that never sent one must not look alike.
|
|
231549
|
+
* Everything else in the row (channel, name, online, sleep, typeInfo) is
|
|
231550
|
+
* kept verbatim — it is the context that makes the battery row readable.
|
|
231551
|
+
*/
|
|
231552
|
+
function redactChannelIdentity(entry) {
|
|
231553
|
+
if (entry === void 0) return null;
|
|
231554
|
+
const { uid, ...rest } = entry;
|
|
231555
|
+
return {
|
|
231556
|
+
...rest,
|
|
231557
|
+
uidRedacted: uid !== void 0
|
|
231558
|
+
};
|
|
231559
|
+
}
|
|
231560
|
+
/**
|
|
231561
|
+
* Age of a `capRuntimeState` belief. The cap slice carries `lastUpdated`
|
|
231562
|
+
* rather than a freshness-map stamp, and its `empty` seed is `0` — which is
|
|
231563
|
+
* NOT an observation, so it resolves to `never`. A slice that was never
|
|
231564
|
+
* written at all is `never` too: on a battery reading, "we hold no value"
|
|
231565
|
+
* and "we measured 0%" are an alarm apart (see `BatteryStatusSchema`).
|
|
231566
|
+
*/
|
|
231567
|
+
function capStateAge(state, now) {
|
|
231568
|
+
if (state === void 0) return { state: "never" };
|
|
231569
|
+
const lastUpdated = state.lastUpdated;
|
|
231570
|
+
if (typeof lastUpdated !== "number" || lastUpdated <= 0) return { state: "never" };
|
|
231571
|
+
return {
|
|
231572
|
+
state: "known",
|
|
231573
|
+
fetchedAt: lastUpdated,
|
|
231574
|
+
ageMs: Math.max(0, now - lastUpdated)
|
|
231575
|
+
};
|
|
231576
|
+
}
|
|
231398
231577
|
/** What CamStack currently believes about the slice, with its own age. */
|
|
231399
231578
|
var BelievedStateSchema = object({
|
|
231400
|
-
/** The persisted projection
|
|
231579
|
+
/** The persisted projection / cap slice, verbatim. */
|
|
231401
231580
|
snapshot: unknown(),
|
|
231402
|
-
/**
|
|
231403
|
-
|
|
231581
|
+
/** Where that belief lives — mirrors {@link BelievedSource}. */
|
|
231582
|
+
source: discriminatedUnion("kind", [object({
|
|
231583
|
+
kind: literal("deviceCache"),
|
|
231584
|
+
snapshotKey: string()
|
|
231585
|
+
}), object({
|
|
231586
|
+
kind: literal("capRuntimeState"),
|
|
231587
|
+
capName: literal("battery")
|
|
231588
|
+
})]),
|
|
231404
231589
|
/** Tri-state freshness — `never` / `unknown` (legacy, no stamp) / `known`. */
|
|
231405
231590
|
age: union([
|
|
231406
231591
|
object({ state: literal("never") }),
|
|
@@ -231439,11 +231624,17 @@ var RawReadInputSchema = object({
|
|
|
231439
231624
|
deviceId: number().int().nonnegative(),
|
|
231440
231625
|
slice: RawReadSliceSchema
|
|
231441
231626
|
});
|
|
231442
|
-
function believedState(
|
|
231443
|
-
const
|
|
231627
|
+
function believedState(deps, entry) {
|
|
231628
|
+
const source = entry.believed;
|
|
231629
|
+
if (source.kind === "capRuntimeState") return {
|
|
231630
|
+
snapshot: toPlainJson(deps.batteryState),
|
|
231631
|
+
source,
|
|
231632
|
+
age: capStateAge(deps.batteryState, deps.now)
|
|
231633
|
+
};
|
|
231634
|
+
const age = resolveSnapshotAge(deps.cache, source.snapshotKey, deps.now);
|
|
231444
231635
|
return {
|
|
231445
|
-
snapshot: toPlainJson(cache === void 0 ? void 0 : Reflect.get(cache,
|
|
231446
|
-
|
|
231636
|
+
snapshot: toPlainJson(deps.cache === void 0 ? void 0 : Reflect.get(deps.cache, source.snapshotKey)),
|
|
231637
|
+
source,
|
|
231447
231638
|
age
|
|
231448
231639
|
};
|
|
231449
231640
|
}
|
|
@@ -231463,8 +231654,9 @@ function toPlainJson(value) {
|
|
|
231463
231654
|
*/
|
|
231464
231655
|
async function performRawRead(slice, deps) {
|
|
231465
231656
|
const entry = RAW_READ_CATALOG[slice];
|
|
231466
|
-
const believed = believedState(deps
|
|
231467
|
-
|
|
231657
|
+
const believed = believedState(deps, entry);
|
|
231658
|
+
const servedWithoutTouchingCamera = deps.hubChild && entry.servedByParentHub === true;
|
|
231659
|
+
if (deps.sleeping && !servedWithoutTouchingCamera) {
|
|
231468
231660
|
deps.logger.info("reolink raw read refused — battery cam is sleeping", {
|
|
231469
231661
|
tags: { deviceId: deps.deviceId },
|
|
231470
231662
|
meta: { slice }
|
|
@@ -231474,7 +231666,7 @@ async function performRawRead(slice, deps) {
|
|
|
231474
231666
|
deviceId: deps.deviceId,
|
|
231475
231667
|
slice,
|
|
231476
231668
|
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.",
|
|
231669
|
+
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
231670
|
believed
|
|
231479
231671
|
};
|
|
231480
231672
|
}
|
|
@@ -231500,12 +231692,14 @@ async function performRawRead(slice, deps) {
|
|
|
231500
231692
|
};
|
|
231501
231693
|
}
|
|
231502
231694
|
try {
|
|
231503
|
-
const payload = await entry.invoke(api, deps.channel);
|
|
231695
|
+
const payload = await entry.invoke(api, deps.channel, { hubChild: deps.hubChild });
|
|
231504
231696
|
deps.logger.info("reolink raw read served", {
|
|
231505
231697
|
tags: { deviceId: deps.deviceId },
|
|
231506
231698
|
meta: {
|
|
231507
231699
|
slice,
|
|
231508
|
-
command: entry.command
|
|
231700
|
+
command: entry.command,
|
|
231701
|
+
viaParentHub: servedWithoutTouchingCamera,
|
|
231702
|
+
cameraWasSleeping: deps.sleeping
|
|
231509
231703
|
}
|
|
231510
231704
|
});
|
|
231511
231705
|
return {
|
|
@@ -231564,6 +231758,58 @@ var reolinkDebugActions = defineCustomActions({ debugRawRead: customAction(RawRe
|
|
|
231564
231758
|
auth: "admin"
|
|
231565
231759
|
}) });
|
|
231566
231760
|
//#endregion
|
|
231761
|
+
//#region src/battery-charging.ts
|
|
231762
|
+
/**
|
|
231763
|
+
* Da `adapterStatus` + `chargeStatus` alla sorgente di alimentazione.
|
|
231764
|
+
*
|
|
231765
|
+
* I due campi rispondono a DUE domande diverse, e confonderle è il bug che
|
|
231766
|
+
* questo modulo esiste per chiudere:
|
|
231767
|
+
*
|
|
231768
|
+
* - `adapterStatus` — l'alimentazione è COLLEGATA? Il firmware manda 0/1/2
|
|
231769
|
+
* (nessuna · adattatore DC · pannello solare) e il confine dell'hub lo
|
|
231770
|
+
* traduce in `'none' | 'dc' | 'solarPanel'` (`mapHubChargeStatus` e
|
|
231771
|
+
* `mapHubAdapterStatus` in `reolink-hub.ts`), perché il vocabolario del
|
|
231772
|
+
* percorso standalone è già a stringhe.
|
|
231773
|
+
* - `chargeStatus` — la CELLA cosa sta facendo? 0 ferma · 1 in carica ·
|
|
231774
|
+
* 2 carica completa.
|
|
231775
|
+
*
|
|
231776
|
+
* La vecchia derivazione consultava l'adattatore SOLO per cercarci 'solar', e
|
|
231777
|
+
* un DC collegato cadeva nel ripiego `'none'`. Due Argus 3E lo nascondevano per
|
|
231778
|
+
* caso: piene e collegate rispondono `chargeStatus: 2`, che il ramo `isCharging`
|
|
231779
|
+
* mappava su `'dc'` per la ragione sbagliata. La Argus MagiCam (batteryVersion
|
|
231780
|
+
* 1, firmware 2026) collegata in permanenza risponde `chargeStatus: 0`, e per
|
|
231781
|
+
* tutta la sua vita è stata riportata NON in carica.
|
|
231782
|
+
*
|
|
231783
|
+
* Che `adapterStatus` sia un segnale vero e non una costante è MISURATO: il
|
|
231784
|
+
* 2026-09-05 l'operatore ha staccato il device 3628 e il payload grezzo è
|
|
231785
|
+
* passato da `adapterStatus: 1` a `0`, con `chargeStatus: 0` da entrambe le
|
|
231786
|
+
* parti — cioè `chargeStatus` da solo NON distingue i due stati.
|
|
231787
|
+
*
|
|
231788
|
+
* ⚠️ Questa funzione non sa ancora dire "non lo so": `BatteryStatus['charging']`
|
|
231789
|
+
* non ha un valore `unknown`, quindi due campi illeggibili producono `'none'`,
|
|
231790
|
+
* che è un valore NEGATIVO al posto di un non-noto. È il difetto D315 che
|
|
231791
|
+
* `percentage` ha già chiuso (è `nullable`) e questo campo no; chiuderlo tocca
|
|
231792
|
+
* `battery.cap.ts`, cioè la closure del server, e vive nel proprio lavoro.
|
|
231793
|
+
*/
|
|
231794
|
+
function deriveChargingSource(adapterStatus, chargeStatus) {
|
|
231795
|
+
const adapter = (adapterStatus ?? "").toLowerCase();
|
|
231796
|
+
const charge = (chargeStatus ?? "").toLowerCase();
|
|
231797
|
+
if (adapter.includes("solar")) return "solar";
|
|
231798
|
+
if (adapter === "dc") return "dc";
|
|
231799
|
+
if (charge === "charging" || charge === "chargecomplete") return "dc";
|
|
231800
|
+
return "none";
|
|
231801
|
+
}
|
|
231802
|
+
/**
|
|
231803
|
+
* Did this reading SAY anything about the power source? A battery push may
|
|
231804
|
+
* carry only the level (every `BatteryInfo` field is optional and nodelink
|
|
231805
|
+
* dispatches as soon as one is present); a caller that derives `charging`
|
|
231806
|
+
* from such a push turns "nothing was said" into 'none' and overwrites a known
|
|
231807
|
+
* 'dc'. When this is false the caller keeps its previous belief.
|
|
231808
|
+
*/
|
|
231809
|
+
function reportsPowerSource(info) {
|
|
231810
|
+
return info.adapterStatus !== void 0 || info.chargeStatus !== void 0;
|
|
231811
|
+
}
|
|
231812
|
+
//#endregion
|
|
231567
231813
|
//#region src/log-channels.ts
|
|
231568
231814
|
/**
|
|
231569
231815
|
* The diagnostic log CHANNELS `provider-reolink` declares.
|
|
@@ -236339,8 +236585,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
236339
236585
|
deviceId: this.id,
|
|
236340
236586
|
channel: this.getChannel(),
|
|
236341
236587
|
sleeping: this.isBattery && this.sleeping,
|
|
236588
|
+
hubChild: this.isHubChild(),
|
|
236342
236589
|
getApi: () => this.ensureApi(),
|
|
236343
236590
|
cache: this.config.get("deviceCache"),
|
|
236591
|
+
batteryState: this.runtimeState.getCapState("battery"),
|
|
236344
236592
|
logger: this.ctx.logger,
|
|
236345
236593
|
now: Date.now()
|
|
236346
236594
|
});
|
|
@@ -236671,13 +236919,9 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
236671
236919
|
* tracker is more reliable.
|
|
236672
236920
|
*/
|
|
236673
236921
|
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
236922
|
return {
|
|
236679
|
-
percentage,
|
|
236680
|
-
charging:
|
|
236923
|
+
percentage: typeof info.batteryPercent === "number" ? Math.max(0, Math.min(100, Math.round(info.batteryPercent))) : this.state.battery.percentage ?? null,
|
|
236924
|
+
charging: reportsPowerSource(info) ? deriveChargingSource(info.adapterStatus, info.chargeStatus) : this.state.battery.charging ?? "none",
|
|
236681
236925
|
sleeping: info.sleeping === true || this.sleeping,
|
|
236682
236926
|
lastUpdated: Date.now()
|
|
236683
236927
|
};
|
|
@@ -245192,6 +245436,7 @@ exports.collectMultifocalDiagnostics = collectMultifocalDiagnostics;
|
|
|
245192
245436
|
exports.collectNativeDiagnostics = collectNativeDiagnostics;
|
|
245193
245437
|
exports.collectNvrDiagnostics = collectNvrDiagnostics;
|
|
245194
245438
|
exports.createDiagnosticsBundle = createDiagnosticsBundle;
|
|
245439
|
+
exports.customActions = reolinkDebugActions;
|
|
245195
245440
|
exports.reolinkCameraSchema = reolinkCameraSchema;
|
|
245196
245441
|
exports.reolinkDebugActions = reolinkDebugActions;
|
|
245197
245442
|
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
|
|
@@ -19293,6 +19311,9 @@ var RetrainStatusSchema = _enum([
|
|
|
19293
19311
|
*
|
|
19294
19312
|
* `debug` does NOT pin; it is attention, not durability.
|
|
19295
19313
|
*
|
|
19314
|
+
* `debugNote` is the operator's own words about WHAT should be checked on this
|
|
19315
|
+
* track, and it lives and dies with `debug` — see {@link MAX_TRACK_DEBUG_NOTE_LEN}.
|
|
19316
|
+
*
|
|
19296
19317
|
* `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
|
|
19297
19318
|
* A favourited track is skipped by retention the same way `staging` is, but
|
|
19298
19319
|
* it does not enter `none|staging|trained` and has no staging budget.
|
|
@@ -19303,6 +19324,21 @@ var TrackFlagFields = {
|
|
|
19303
19324
|
markForTrain: boolean().optional(),
|
|
19304
19325
|
/** Operator marked this track for diagnostic attention. */
|
|
19305
19326
|
debug: boolean().optional(),
|
|
19327
|
+
/**
|
|
19328
|
+
* What the operator wants CHECKED on this track, in their own words.
|
|
19329
|
+
*
|
|
19330
|
+
* The flag alone says "look at this" and nothing about what for; a debug set
|
|
19331
|
+
* reviewed hours later is a list of tracks with no question attached. Bound
|
|
19332
|
+
* to `debug` on the WRITE side — the body clears it when `debug` goes off,
|
|
19333
|
+
* and refuses it on a track whose debug is off — so a note can never outlive
|
|
19334
|
+
* the attention it belongs to.
|
|
19335
|
+
*
|
|
19336
|
+
* `''` is a real value ("marked, nothing to add"); the ABSENT key means
|
|
19337
|
+
* "leave whatever is stored alone", which is what lets a surface turn debug
|
|
19338
|
+
* on without a note (a cancelled prompt) and what lets it edit the note
|
|
19339
|
+
* without touching the flag.
|
|
19340
|
+
*/
|
|
19341
|
+
debugNote: string().max(500).optional(),
|
|
19306
19342
|
/** Operator favourited this track. Pins it against pruning. */
|
|
19307
19343
|
favourited: boolean().optional()
|
|
19308
19344
|
};
|
|
@@ -19329,6 +19365,12 @@ var TrackFlagsSchema = object({
|
|
|
19329
19365
|
trackId: string(),
|
|
19330
19366
|
markForTrain: boolean(),
|
|
19331
19367
|
debug: boolean(),
|
|
19368
|
+
/** The note as it STANDS after the write — never absent here (a track with no
|
|
19369
|
+
* note reports `''`), for the same reason the booleans are required: a
|
|
19370
|
+
* surface that has just written must be able to render the note it now owns
|
|
19371
|
+
* without a re-fetch, and an absent key would read as "unchanged" on a
|
|
19372
|
+
* screen that has no previous value to keep. */
|
|
19373
|
+
debugNote: string(),
|
|
19332
19374
|
favourited: boolean(),
|
|
19333
19375
|
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
19334
19376
|
* a track row) because this shape is only ever produced by the write body,
|
|
@@ -179513,7 +179555,7 @@ ${xml}`);
|
|
|
179513
179555
|
* @returns Test results for all stream types and profiles
|
|
179514
179556
|
*/
|
|
179515
179557
|
async testChannelStreams(channel, logger) {
|
|
179516
|
-
const { testChannelStreams } = await import("./DiagnosticsTools-QJ3CRYGA-
|
|
179558
|
+
const { testChannelStreams } = await import("./DiagnosticsTools-QJ3CRYGA-9NV95vRN.mjs");
|
|
179517
179559
|
return await testChannelStreams({
|
|
179518
179560
|
api: this,
|
|
179519
179561
|
channel: this.normalizeChannel(channel),
|
|
@@ -179529,7 +179571,7 @@ ${xml}`);
|
|
|
179529
179571
|
* @returns Complete diagnostics for all channels and streams
|
|
179530
179572
|
*/
|
|
179531
179573
|
async collectMultifocalDiagnostics(logger) {
|
|
179532
|
-
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-QJ3CRYGA-
|
|
179574
|
+
const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-QJ3CRYGA-9NV95vRN.mjs");
|
|
179533
179575
|
return await collectMultifocalDiagnostics({
|
|
179534
179576
|
api: this,
|
|
179535
179577
|
logger
|
|
@@ -231278,7 +231320,8 @@ var RawReadSliceSchema = _enum([
|
|
|
231278
231320
|
"osd",
|
|
231279
231321
|
"led",
|
|
231280
231322
|
"pir",
|
|
231281
|
-
"autoReboot"
|
|
231323
|
+
"autoReboot",
|
|
231324
|
+
"battery"
|
|
231282
231325
|
]);
|
|
231283
231326
|
/**
|
|
231284
231327
|
* The allow-list. `Record<RawReadSlice, …>` keeps the catalog and the enum
|
|
@@ -231289,17 +231332,26 @@ var RawReadSliceSchema = _enum([
|
|
|
231289
231332
|
var RAW_READ_CATALOG = {
|
|
231290
231333
|
image: {
|
|
231291
231334
|
command: "getVideoInput",
|
|
231292
|
-
|
|
231335
|
+
believed: {
|
|
231336
|
+
kind: "deviceCache",
|
|
231337
|
+
snapshotKey: "imageSnapshot"
|
|
231338
|
+
},
|
|
231293
231339
|
invoke: (api, channel) => api.getVideoInput(channel)
|
|
231294
231340
|
},
|
|
231295
231341
|
motion: {
|
|
231296
231342
|
command: "getMotionAlarm",
|
|
231297
|
-
|
|
231343
|
+
believed: {
|
|
231344
|
+
kind: "deviceCache",
|
|
231345
|
+
snapshotKey: "motionSnapshot"
|
|
231346
|
+
},
|
|
231298
231347
|
invoke: (api, channel) => api.getMotionAlarm(channel)
|
|
231299
231348
|
},
|
|
231300
231349
|
ai: {
|
|
231301
231350
|
command: "getAiDetectTypes + getAiDetectionFull (per type)",
|
|
231302
|
-
|
|
231351
|
+
believed: {
|
|
231352
|
+
kind: "deviceCache",
|
|
231353
|
+
snapshotKey: "aiSensitivitySnapshot"
|
|
231354
|
+
},
|
|
231303
231355
|
invoke: async (api, channel) => {
|
|
231304
231356
|
const detectTypes = await api.getAiDetectTypes(channel, { timeoutMs: 1500 });
|
|
231305
231357
|
const perType = {};
|
|
@@ -231316,71 +231368,204 @@ var RAW_READ_CATALOG = {
|
|
|
231316
231368
|
},
|
|
231317
231369
|
enc: {
|
|
231318
231370
|
command: "getEnc",
|
|
231319
|
-
|
|
231371
|
+
believed: {
|
|
231372
|
+
kind: "deviceCache",
|
|
231373
|
+
snapshotKey: "encSnapshot"
|
|
231374
|
+
},
|
|
231320
231375
|
invoke: (api, channel) => api.getEnc(channel)
|
|
231321
231376
|
},
|
|
231322
231377
|
encOptions: {
|
|
231323
231378
|
command: "getEncOptions",
|
|
231324
|
-
|
|
231379
|
+
believed: {
|
|
231380
|
+
kind: "deviceCache",
|
|
231381
|
+
snapshotKey: "encOptionsSnapshot"
|
|
231382
|
+
},
|
|
231325
231383
|
invoke: (api, channel) => api.getEncOptions(channel)
|
|
231326
231384
|
},
|
|
231327
231385
|
mask: {
|
|
231328
231386
|
command: "getMask",
|
|
231329
|
-
|
|
231387
|
+
believed: {
|
|
231388
|
+
kind: "deviceCache",
|
|
231389
|
+
snapshotKey: "maskSnapshot"
|
|
231390
|
+
},
|
|
231330
231391
|
invoke: (api, channel) => api.getMask(channel)
|
|
231331
231392
|
},
|
|
231332
231393
|
audioNoise: {
|
|
231333
231394
|
command: "getAudioNoise",
|
|
231334
|
-
|
|
231395
|
+
believed: {
|
|
231396
|
+
kind: "deviceCache",
|
|
231397
|
+
snapshotKey: "audioNoiseSnapshot"
|
|
231398
|
+
},
|
|
231335
231399
|
invoke: (api, channel) => api.getAudioNoise(channel)
|
|
231336
231400
|
},
|
|
231337
231401
|
autofocus: {
|
|
231338
231402
|
command: "getAutoFocus",
|
|
231339
|
-
|
|
231403
|
+
believed: {
|
|
231404
|
+
kind: "deviceCache",
|
|
231405
|
+
snapshotKey: "autoFocusSnapshot"
|
|
231406
|
+
},
|
|
231340
231407
|
invoke: (api, channel) => api.getAutoFocus(channel, { timeoutMs: 1500 })
|
|
231341
231408
|
},
|
|
231342
231409
|
netPort: {
|
|
231343
231410
|
command: "getNetPort",
|
|
231344
|
-
|
|
231411
|
+
believed: {
|
|
231412
|
+
kind: "deviceCache",
|
|
231413
|
+
snapshotKey: "netPortSnapshot"
|
|
231414
|
+
},
|
|
231345
231415
|
invoke: (api) => api.getNetPort()
|
|
231346
231416
|
},
|
|
231347
231417
|
ntp: {
|
|
231348
231418
|
command: "getNtp",
|
|
231349
|
-
|
|
231419
|
+
believed: {
|
|
231420
|
+
kind: "deviceCache",
|
|
231421
|
+
snapshotKey: "ntpSnapshot"
|
|
231422
|
+
},
|
|
231350
231423
|
invoke: (api) => api.getNtp()
|
|
231351
231424
|
},
|
|
231352
231425
|
systemGeneral: {
|
|
231353
231426
|
command: "getSystemGeneral",
|
|
231354
|
-
|
|
231427
|
+
believed: {
|
|
231428
|
+
kind: "deviceCache",
|
|
231429
|
+
snapshotKey: "systemGeneralSnapshot"
|
|
231430
|
+
},
|
|
231355
231431
|
invoke: (api) => api.getSystemGeneral()
|
|
231356
231432
|
},
|
|
231357
231433
|
osd: {
|
|
231358
231434
|
command: "getOsd",
|
|
231359
|
-
|
|
231435
|
+
believed: {
|
|
231436
|
+
kind: "deviceCache",
|
|
231437
|
+
snapshotKey: "osdSnapshot"
|
|
231438
|
+
},
|
|
231360
231439
|
invoke: (api, channel) => api.getOsd(channel)
|
|
231361
231440
|
},
|
|
231362
231441
|
led: {
|
|
231363
231442
|
command: "getIrLights",
|
|
231364
|
-
|
|
231443
|
+
believed: {
|
|
231444
|
+
kind: "deviceCache",
|
|
231445
|
+
snapshotKey: "ledSnapshot"
|
|
231446
|
+
},
|
|
231365
231447
|
invoke: (api, channel) => api.getIrLights(channel)
|
|
231366
231448
|
},
|
|
231367
231449
|
pir: {
|
|
231368
231450
|
command: "getPirInfo",
|
|
231369
|
-
|
|
231451
|
+
believed: {
|
|
231452
|
+
kind: "deviceCache",
|
|
231453
|
+
snapshotKey: "pirSnapshot"
|
|
231454
|
+
},
|
|
231370
231455
|
invoke: (api, channel) => api.getPirInfo(channel)
|
|
231371
231456
|
},
|
|
231372
231457
|
autoReboot: {
|
|
231373
231458
|
command: "getAutoReboot",
|
|
231374
|
-
|
|
231459
|
+
believed: {
|
|
231460
|
+
kind: "deviceCache",
|
|
231461
|
+
snapshotKey: "autoRebootSnapshot"
|
|
231462
|
+
},
|
|
231375
231463
|
invoke: (api) => api.getAutoReboot()
|
|
231464
|
+
},
|
|
231465
|
+
/**
|
|
231466
|
+
* Battery — one slice, TWO firmware reads, chosen by how the camera is
|
|
231467
|
+
* attached. They are not variants of one call:
|
|
231468
|
+
*
|
|
231469
|
+
* - **standalone**: `getBatteryInfo(channel)` — the camera's own
|
|
231470
|
+
* answer, one `BatteryInfo`;
|
|
231471
|
+
* - **hub child**: `getAllChannelsBatteryInfo()` — a HUB-level CGI
|
|
231472
|
+
* keyed by channel, projected to the requested one here so the slice
|
|
231473
|
+
* stays per-camera like the other fifteen (a "cluster-wide" second
|
|
231474
|
+
* slice shape would have to be understood by every caller).
|
|
231475
|
+
*
|
|
231476
|
+
* Both vocabularies carry `adapterStatus` next to `chargeStatus`, which
|
|
231477
|
+
* is why this slice exists: the provider keeps only the latter, and
|
|
231478
|
+
* whether `adapterStatus` MOVES was previously observable exactly once
|
|
231479
|
+
* per hub process (`ReolinkHub.batteryShapeLogged`). The answer is
|
|
231480
|
+
* returned as the firmware gave it, under a `readVia` that names the path
|
|
231481
|
+
* — NOT normalised into a shared shape. The paths do not carry the same
|
|
231482
|
+
* fields (`voltage`/`lowPower` on the camera read, `channelsReported` and
|
|
231483
|
+
* the channel-status row on the hub read); a field the firmware did not
|
|
231484
|
+
* send must be ABSENT, because a default here would be indistinguishable
|
|
231485
|
+
* from a reading (D315).
|
|
231486
|
+
*
|
|
231487
|
+
* A camera that cannot answer at all throws, and the read comes back
|
|
231488
|
+
* `read-failed` carrying the firmware's reason — "we could not read it"
|
|
231489
|
+
* must never be served as "it has no battery".
|
|
231490
|
+
*/
|
|
231491
|
+
battery: {
|
|
231492
|
+
command: "getBatteryInfo (standalone) / getAllChannelsBatteryInfo projected (hub child)",
|
|
231493
|
+
believed: {
|
|
231494
|
+
kind: "capRuntimeState",
|
|
231495
|
+
capName: "battery"
|
|
231496
|
+
},
|
|
231497
|
+
servedByParentHub: true,
|
|
231498
|
+
invoke: async (api, channel, ctx) => {
|
|
231499
|
+
if (!ctx.hubChild) return {
|
|
231500
|
+
readVia: "camera",
|
|
231501
|
+
command: "getBatteryInfo",
|
|
231502
|
+
channel,
|
|
231503
|
+
batteryInfo: await api.getBatteryInfo(channel)
|
|
231504
|
+
};
|
|
231505
|
+
const byChannel = (await api.getAllChannelsBatteryInfo()).batteryInfoData;
|
|
231506
|
+
const channelsReported = Object.keys(byChannel).map(Number).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
|
|
231507
|
+
const info = byChannel[channel];
|
|
231508
|
+
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".`);
|
|
231509
|
+
const [cgiBattery, channelStatus] = info.entries;
|
|
231510
|
+
return {
|
|
231511
|
+
readVia: "parentHub",
|
|
231512
|
+
command: "getAllChannelsBatteryInfo",
|
|
231513
|
+
channel,
|
|
231514
|
+
channelsReported,
|
|
231515
|
+
batteryInfo: {
|
|
231516
|
+
...info,
|
|
231517
|
+
entries: [cgiBattery ?? null, redactChannelIdentity(channelStatus)]
|
|
231518
|
+
}
|
|
231519
|
+
};
|
|
231520
|
+
}
|
|
231376
231521
|
}
|
|
231377
231522
|
};
|
|
231523
|
+
/**
|
|
231524
|
+
* The hub's channel-status row (`entries[1]`) carries the camera's Reolink
|
|
231525
|
+
* P2P `uid` — a routable cloud identifier for the device. A charge-state
|
|
231526
|
+
* read has no reason to hand it out, so it is dropped here rather than
|
|
231527
|
+
* relied on not to be looked at. The drop is STATED (`uidRedacted`): a
|
|
231528
|
+
* removed field and a firmware that never sent one must not look alike.
|
|
231529
|
+
* Everything else in the row (channel, name, online, sleep, typeInfo) is
|
|
231530
|
+
* kept verbatim — it is the context that makes the battery row readable.
|
|
231531
|
+
*/
|
|
231532
|
+
function redactChannelIdentity(entry) {
|
|
231533
|
+
if (entry === void 0) return null;
|
|
231534
|
+
const { uid, ...rest } = entry;
|
|
231535
|
+
return {
|
|
231536
|
+
...rest,
|
|
231537
|
+
uidRedacted: uid !== void 0
|
|
231538
|
+
};
|
|
231539
|
+
}
|
|
231540
|
+
/**
|
|
231541
|
+
* Age of a `capRuntimeState` belief. The cap slice carries `lastUpdated`
|
|
231542
|
+
* rather than a freshness-map stamp, and its `empty` seed is `0` — which is
|
|
231543
|
+
* NOT an observation, so it resolves to `never`. A slice that was never
|
|
231544
|
+
* written at all is `never` too: on a battery reading, "we hold no value"
|
|
231545
|
+
* and "we measured 0%" are an alarm apart (see `BatteryStatusSchema`).
|
|
231546
|
+
*/
|
|
231547
|
+
function capStateAge(state, now) {
|
|
231548
|
+
if (state === void 0) return { state: "never" };
|
|
231549
|
+
const lastUpdated = state.lastUpdated;
|
|
231550
|
+
if (typeof lastUpdated !== "number" || lastUpdated <= 0) return { state: "never" };
|
|
231551
|
+
return {
|
|
231552
|
+
state: "known",
|
|
231553
|
+
fetchedAt: lastUpdated,
|
|
231554
|
+
ageMs: Math.max(0, now - lastUpdated)
|
|
231555
|
+
};
|
|
231556
|
+
}
|
|
231378
231557
|
/** What CamStack currently believes about the slice, with its own age. */
|
|
231379
231558
|
var BelievedStateSchema = object({
|
|
231380
|
-
/** The persisted projection
|
|
231559
|
+
/** The persisted projection / cap slice, verbatim. */
|
|
231381
231560
|
snapshot: unknown(),
|
|
231382
|
-
/**
|
|
231383
|
-
|
|
231561
|
+
/** Where that belief lives — mirrors {@link BelievedSource}. */
|
|
231562
|
+
source: discriminatedUnion("kind", [object({
|
|
231563
|
+
kind: literal("deviceCache"),
|
|
231564
|
+
snapshotKey: string()
|
|
231565
|
+
}), object({
|
|
231566
|
+
kind: literal("capRuntimeState"),
|
|
231567
|
+
capName: literal("battery")
|
|
231568
|
+
})]),
|
|
231384
231569
|
/** Tri-state freshness — `never` / `unknown` (legacy, no stamp) / `known`. */
|
|
231385
231570
|
age: union([
|
|
231386
231571
|
object({ state: literal("never") }),
|
|
@@ -231419,11 +231604,17 @@ var RawReadInputSchema = object({
|
|
|
231419
231604
|
deviceId: number().int().nonnegative(),
|
|
231420
231605
|
slice: RawReadSliceSchema
|
|
231421
231606
|
});
|
|
231422
|
-
function believedState(
|
|
231423
|
-
const
|
|
231607
|
+
function believedState(deps, entry) {
|
|
231608
|
+
const source = entry.believed;
|
|
231609
|
+
if (source.kind === "capRuntimeState") return {
|
|
231610
|
+
snapshot: toPlainJson(deps.batteryState),
|
|
231611
|
+
source,
|
|
231612
|
+
age: capStateAge(deps.batteryState, deps.now)
|
|
231613
|
+
};
|
|
231614
|
+
const age = resolveSnapshotAge(deps.cache, source.snapshotKey, deps.now);
|
|
231424
231615
|
return {
|
|
231425
|
-
snapshot: toPlainJson(cache === void 0 ? void 0 : Reflect.get(cache,
|
|
231426
|
-
|
|
231616
|
+
snapshot: toPlainJson(deps.cache === void 0 ? void 0 : Reflect.get(deps.cache, source.snapshotKey)),
|
|
231617
|
+
source,
|
|
231427
231618
|
age
|
|
231428
231619
|
};
|
|
231429
231620
|
}
|
|
@@ -231443,8 +231634,9 @@ function toPlainJson(value) {
|
|
|
231443
231634
|
*/
|
|
231444
231635
|
async function performRawRead(slice, deps) {
|
|
231445
231636
|
const entry = RAW_READ_CATALOG[slice];
|
|
231446
|
-
const believed = believedState(deps
|
|
231447
|
-
|
|
231637
|
+
const believed = believedState(deps, entry);
|
|
231638
|
+
const servedWithoutTouchingCamera = deps.hubChild && entry.servedByParentHub === true;
|
|
231639
|
+
if (deps.sleeping && !servedWithoutTouchingCamera) {
|
|
231448
231640
|
deps.logger.info("reolink raw read refused — battery cam is sleeping", {
|
|
231449
231641
|
tags: { deviceId: deps.deviceId },
|
|
231450
231642
|
meta: { slice }
|
|
@@ -231454,7 +231646,7 @@ async function performRawRead(slice, deps) {
|
|
|
231454
231646
|
deviceId: deps.deviceId,
|
|
231455
231647
|
slice,
|
|
231456
231648
|
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.",
|
|
231649
|
+
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
231650
|
believed
|
|
231459
231651
|
};
|
|
231460
231652
|
}
|
|
@@ -231480,12 +231672,14 @@ async function performRawRead(slice, deps) {
|
|
|
231480
231672
|
};
|
|
231481
231673
|
}
|
|
231482
231674
|
try {
|
|
231483
|
-
const payload = await entry.invoke(api, deps.channel);
|
|
231675
|
+
const payload = await entry.invoke(api, deps.channel, { hubChild: deps.hubChild });
|
|
231484
231676
|
deps.logger.info("reolink raw read served", {
|
|
231485
231677
|
tags: { deviceId: deps.deviceId },
|
|
231486
231678
|
meta: {
|
|
231487
231679
|
slice,
|
|
231488
|
-
command: entry.command
|
|
231680
|
+
command: entry.command,
|
|
231681
|
+
viaParentHub: servedWithoutTouchingCamera,
|
|
231682
|
+
cameraWasSleeping: deps.sleeping
|
|
231489
231683
|
}
|
|
231490
231684
|
});
|
|
231491
231685
|
return {
|
|
@@ -231544,6 +231738,58 @@ var reolinkDebugActions = defineCustomActions({ debugRawRead: customAction(RawRe
|
|
|
231544
231738
|
auth: "admin"
|
|
231545
231739
|
}) });
|
|
231546
231740
|
//#endregion
|
|
231741
|
+
//#region src/battery-charging.ts
|
|
231742
|
+
/**
|
|
231743
|
+
* Da `adapterStatus` + `chargeStatus` alla sorgente di alimentazione.
|
|
231744
|
+
*
|
|
231745
|
+
* I due campi rispondono a DUE domande diverse, e confonderle è il bug che
|
|
231746
|
+
* questo modulo esiste per chiudere:
|
|
231747
|
+
*
|
|
231748
|
+
* - `adapterStatus` — l'alimentazione è COLLEGATA? Il firmware manda 0/1/2
|
|
231749
|
+
* (nessuna · adattatore DC · pannello solare) e il confine dell'hub lo
|
|
231750
|
+
* traduce in `'none' | 'dc' | 'solarPanel'` (`mapHubChargeStatus` e
|
|
231751
|
+
* `mapHubAdapterStatus` in `reolink-hub.ts`), perché il vocabolario del
|
|
231752
|
+
* percorso standalone è già a stringhe.
|
|
231753
|
+
* - `chargeStatus` — la CELLA cosa sta facendo? 0 ferma · 1 in carica ·
|
|
231754
|
+
* 2 carica completa.
|
|
231755
|
+
*
|
|
231756
|
+
* La vecchia derivazione consultava l'adattatore SOLO per cercarci 'solar', e
|
|
231757
|
+
* un DC collegato cadeva nel ripiego `'none'`. Due Argus 3E lo nascondevano per
|
|
231758
|
+
* caso: piene e collegate rispondono `chargeStatus: 2`, che il ramo `isCharging`
|
|
231759
|
+
* mappava su `'dc'` per la ragione sbagliata. La Argus MagiCam (batteryVersion
|
|
231760
|
+
* 1, firmware 2026) collegata in permanenza risponde `chargeStatus: 0`, e per
|
|
231761
|
+
* tutta la sua vita è stata riportata NON in carica.
|
|
231762
|
+
*
|
|
231763
|
+
* Che `adapterStatus` sia un segnale vero e non una costante è MISURATO: il
|
|
231764
|
+
* 2026-09-05 l'operatore ha staccato il device 3628 e il payload grezzo è
|
|
231765
|
+
* passato da `adapterStatus: 1` a `0`, con `chargeStatus: 0` da entrambe le
|
|
231766
|
+
* parti — cioè `chargeStatus` da solo NON distingue i due stati.
|
|
231767
|
+
*
|
|
231768
|
+
* ⚠️ Questa funzione non sa ancora dire "non lo so": `BatteryStatus['charging']`
|
|
231769
|
+
* non ha un valore `unknown`, quindi due campi illeggibili producono `'none'`,
|
|
231770
|
+
* che è un valore NEGATIVO al posto di un non-noto. È il difetto D315 che
|
|
231771
|
+
* `percentage` ha già chiuso (è `nullable`) e questo campo no; chiuderlo tocca
|
|
231772
|
+
* `battery.cap.ts`, cioè la closure del server, e vive nel proprio lavoro.
|
|
231773
|
+
*/
|
|
231774
|
+
function deriveChargingSource(adapterStatus, chargeStatus) {
|
|
231775
|
+
const adapter = (adapterStatus ?? "").toLowerCase();
|
|
231776
|
+
const charge = (chargeStatus ?? "").toLowerCase();
|
|
231777
|
+
if (adapter.includes("solar")) return "solar";
|
|
231778
|
+
if (adapter === "dc") return "dc";
|
|
231779
|
+
if (charge === "charging" || charge === "chargecomplete") return "dc";
|
|
231780
|
+
return "none";
|
|
231781
|
+
}
|
|
231782
|
+
/**
|
|
231783
|
+
* Did this reading SAY anything about the power source? A battery push may
|
|
231784
|
+
* carry only the level (every `BatteryInfo` field is optional and nodelink
|
|
231785
|
+
* dispatches as soon as one is present); a caller that derives `charging`
|
|
231786
|
+
* from such a push turns "nothing was said" into 'none' and overwrites a known
|
|
231787
|
+
* 'dc'. When this is false the caller keeps its previous belief.
|
|
231788
|
+
*/
|
|
231789
|
+
function reportsPowerSource(info) {
|
|
231790
|
+
return info.adapterStatus !== void 0 || info.chargeStatus !== void 0;
|
|
231791
|
+
}
|
|
231792
|
+
//#endregion
|
|
231547
231793
|
//#region src/log-channels.ts
|
|
231548
231794
|
/**
|
|
231549
231795
|
* The diagnostic log CHANNELS `provider-reolink` declares.
|
|
@@ -236319,8 +236565,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
236319
236565
|
deviceId: this.id,
|
|
236320
236566
|
channel: this.getChannel(),
|
|
236321
236567
|
sleeping: this.isBattery && this.sleeping,
|
|
236568
|
+
hubChild: this.isHubChild(),
|
|
236322
236569
|
getApi: () => this.ensureApi(),
|
|
236323
236570
|
cache: this.config.get("deviceCache"),
|
|
236571
|
+
batteryState: this.runtimeState.getCapState("battery"),
|
|
236324
236572
|
logger: this.ctx.logger,
|
|
236325
236573
|
now: Date.now()
|
|
236326
236574
|
});
|
|
@@ -236651,13 +236899,9 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
236651
236899
|
* tracker is more reliable.
|
|
236652
236900
|
*/
|
|
236653
236901
|
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
236902
|
return {
|
|
236659
|
-
percentage,
|
|
236660
|
-
charging:
|
|
236903
|
+
percentage: typeof info.batteryPercent === "number" ? Math.max(0, Math.min(100, Math.round(info.batteryPercent))) : this.state.battery.percentage ?? null,
|
|
236904
|
+
charging: reportsPowerSource(info) ? deriveChargingSource(info.adapterStatus, info.chargeStatus) : this.state.battery.charging ?? "none",
|
|
236661
236905
|
sleeping: info.sleeping === true || this.sleeping,
|
|
236662
236906
|
lastUpdated: Date.now()
|
|
236663
236907
|
};
|
|
@@ -245165,4 +245409,4 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
245165
245409
|
}
|
|
245166
245410
|
};
|
|
245167
245411
|
//#endregion
|
|
245168
|
-
export { ReolinkProviderAddon,
|
|
245412
|
+
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