@camstack/addon-provider-reolink 1.2.85 → 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.
@@ -0,0 +1,2 @@
1
+ import { d as testChannelStreams, i as collectMultifocalDiagnostics } from "./addon.mjs";
2
+ export { collectMultifocalDiagnostics, testChannelStreams };
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
- snapshotKey: "imageSnapshot",
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
- snapshotKey: "motionSnapshot",
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
- snapshotKey: "aiSensitivitySnapshot",
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
- snapshotKey: "encSnapshot",
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
- snapshotKey: "encOptionsSnapshot",
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
- snapshotKey: "maskSnapshot",
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
- snapshotKey: "audioNoiseSnapshot",
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
- snapshotKey: "autoFocusSnapshot",
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
- snapshotKey: "netPortSnapshot",
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
- snapshotKey: "ntpSnapshot",
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
- snapshotKey: "systemGeneralSnapshot",
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
- snapshotKey: "osdSnapshot",
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
- snapshotKey: "ledSnapshot",
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
- snapshotKey: "pirSnapshot",
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
- snapshotKey: "autoRebootSnapshot",
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 (`deviceCache.<snapshotKey>`), verbatim. */
231579
+ /** The persisted projection / cap slice, verbatim. */
231401
231580
  snapshot: unknown(),
231402
- /** deviceCache field the projection lives in. */
231403
- snapshotKey: string(),
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(cache, entry, now) {
231443
- const age = resolveSnapshotAge(cache, entry.snapshotKey, now);
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, entry.snapshotKey)),
231446
- snapshotKey: entry.snapshotKey,
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.cache, entry, deps.now);
231467
- if (deps.sleeping) {
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: adapter.includes("solar") ? "solar" : isCharging ? "dc" : "none",
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-CcYIIfQN.mjs");
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-CcYIIfQN.mjs");
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
- snapshotKey: "imageSnapshot",
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
- snapshotKey: "motionSnapshot",
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
- snapshotKey: "aiSensitivitySnapshot",
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
- snapshotKey: "encSnapshot",
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
- snapshotKey: "encOptionsSnapshot",
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
- snapshotKey: "maskSnapshot",
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
- snapshotKey: "audioNoiseSnapshot",
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
- snapshotKey: "autoFocusSnapshot",
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
- snapshotKey: "netPortSnapshot",
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
- snapshotKey: "ntpSnapshot",
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
- snapshotKey: "systemGeneralSnapshot",
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
- snapshotKey: "osdSnapshot",
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
- snapshotKey: "ledSnapshot",
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
- snapshotKey: "pirSnapshot",
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
- snapshotKey: "autoRebootSnapshot",
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 (`deviceCache.<snapshotKey>`), verbatim. */
231559
+ /** The persisted projection / cap slice, verbatim. */
231381
231560
  snapshot: unknown(),
231382
- /** deviceCache field the projection lives in. */
231383
- snapshotKey: string(),
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(cache, entry, now) {
231423
- const age = resolveSnapshotAge(cache, entry.snapshotKey, now);
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, entry.snapshotKey)),
231426
- snapshotKey: entry.snapshotKey,
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.cache, entry, deps.now);
231447
- if (deps.sleeping) {
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: adapter.includes("solar") ? "solar" : isCharging ? "dc" : "none",
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, collectMultifocalDiagnostics as a, createDiagnosticsBundle as c, sampleStreams as d, testChannelStreams as f, collectCgiDiagnostics as i, runAllDiagnosticsConsecutively as l, reolinkCameraSchema as n, collectNativeDiagnostics as o, reolinkDebugActions as r, collectNvrDiagnostics as s, ReolinkCamera as t, runMultifocalDiagnosticsConsecutively as u };
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, n as reolinkCameraSchema, r as reolinkDebugActions, t as ReolinkCamera } from "./addon.mjs";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.85",
3
+ "version": "1.2.87",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",
@@ -1,2 +0,0 @@
1
- import { a as collectMultifocalDiagnostics, f as testChannelStreams } from "./addon.mjs";
2
- export { collectMultifocalDiagnostics, testChannelStreams };