@camstack/addon-provider-reolink 1.2.87 → 1.2.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon.js +182 -25
- package/dist/addon.mjs +182 -25
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -7592,7 +7592,8 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
|
|
|
7592
7592
|
var CameraSwitchUnavailableReasonSchema = _enum([
|
|
7593
7593
|
"no-provider",
|
|
7594
7594
|
"source-unreachable",
|
|
7595
|
-
"not-configured"
|
|
7595
|
+
"not-configured",
|
|
7596
|
+
"device-disabled"
|
|
7596
7597
|
]);
|
|
7597
7598
|
/**
|
|
7598
7599
|
* One switch, resolved for one camera.
|
|
@@ -13648,7 +13649,12 @@ var ConfigEntrySchema = object({
|
|
|
13648
13649
|
value: unknown(),
|
|
13649
13650
|
description: string().optional()
|
|
13650
13651
|
});
|
|
13651
|
-
|
|
13652
|
+
/**
|
|
13653
|
+
* Only `manual` since D356: the operator picks, nothing links itself. The
|
|
13654
|
+
* field survives on the wire because a viewer already OTA'd parses it —
|
|
13655
|
+
* drop it once no shipped client reads `mode`.
|
|
13656
|
+
*/
|
|
13657
|
+
var LinkedDevicesModeSchema = _enum(["manual"]);
|
|
13652
13658
|
/** One resolved linked device — the compact projection consumers need. */
|
|
13653
13659
|
var LinkedDeviceSchema = object({
|
|
13654
13660
|
deviceId: number(),
|
|
@@ -24320,12 +24326,20 @@ var BatteryStatusSchema = object({
|
|
|
24320
24326
|
* Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
|
|
24321
24327
|
* Reolink-specific for the Solar Panel 2 accessory (will become
|
|
24322
24328
|
* common on other battery cams). `'none'` means running on battery
|
|
24323
|
-
* alone.
|
|
24329
|
+
* alone — a NEGATIVE the firmware actually reported.
|
|
24330
|
+
*
|
|
24331
|
+
* `'unknown'` is the absence of an answer: the provider has not read the
|
|
24332
|
+
* power fields yet, or the firmware reported neither. Before it existed,
|
|
24333
|
+
* two unreadable fields produced `'none'`, and a camera nobody had reached
|
|
24334
|
+
* was drawn "on battery alone" — the same D315 defect `percentage` closed
|
|
24335
|
+
* by going nullable. Consumers SKIP it (no charger state published, no
|
|
24336
|
+
* glyph, no HomeKit ChargingState) rather than coerce it either way.
|
|
24324
24337
|
*/
|
|
24325
24338
|
charging: _enum([
|
|
24326
24339
|
"dc",
|
|
24327
24340
|
"solar",
|
|
24328
|
-
"none"
|
|
24341
|
+
"none",
|
|
24342
|
+
"unknown"
|
|
24329
24343
|
]),
|
|
24330
24344
|
/**
|
|
24331
24345
|
* True when the camera firmware has gone into low-power mode. Battery
|
|
@@ -24415,7 +24429,7 @@ onStatusChanged: { data: object({
|
|
|
24415
24429
|
kind: "push",
|
|
24416
24430
|
empty: {
|
|
24417
24431
|
percentage: 0,
|
|
24418
|
-
charging: "
|
|
24432
|
+
charging: "unknown",
|
|
24419
24433
|
sleeping: false,
|
|
24420
24434
|
lastUpdated: 0
|
|
24421
24435
|
}
|
|
@@ -34123,8 +34137,17 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
34123
34137
|
async onRestoreDevices(savedDevices) {
|
|
34124
34138
|
const restored = /* @__PURE__ */ new Set();
|
|
34125
34139
|
const failures = [];
|
|
34140
|
+
const liveStableIds = new Set((await this.ctx.kernel.devices.getAll()).map((device) => device.stableId));
|
|
34126
34141
|
const attemptRestore = async (saved) => {
|
|
34127
34142
|
if (restored.has(saved.id)) return;
|
|
34143
|
+
if (liveStableIds.has(saved.stableId)) {
|
|
34144
|
+
this.ctx.logger.debug("Restore row already live — not dialled again", { tags: {
|
|
34145
|
+
deviceId: saved.id,
|
|
34146
|
+
stableId: saved.stableId
|
|
34147
|
+
} });
|
|
34148
|
+
restored.add(saved.id);
|
|
34149
|
+
return;
|
|
34150
|
+
}
|
|
34128
34151
|
const Class = this.deviceClasses[saved.type];
|
|
34129
34152
|
if (!Class) throw new Error(`no device class registered for type "${saved.type}"`);
|
|
34130
34153
|
if (saved.parentDeviceId !== null && !restored.has(saved.parentDeviceId)) throw new Error(`parent device ${saved.parentDeviceId} not restored`);
|
|
@@ -34222,6 +34245,17 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
34222
34245
|
return toDeviceSummary(device, this.addonId);
|
|
34223
34246
|
}
|
|
34224
34247
|
};
|
|
34248
|
+
/**
|
|
34249
|
+
* Default silence budget before a sleeping battery camera is called gone.
|
|
34250
|
+
*
|
|
34251
|
+
* Sized off the mechanism that produces contact, not off taste: a battery cam
|
|
34252
|
+
* on this deployment surfaces a firmware push (battery level, sleep/wake, or a
|
|
34253
|
+
* motion email) on the order of hours even with no visitors, and the snapshot
|
|
34254
|
+
* wrapper's own battery window is 1 hour. Six hours is therefore several
|
|
34255
|
+
* missed opportunities, not one — so a single quiet night does not raise a
|
|
34256
|
+
* fault, and a genuinely flat camera is named well inside a day.
|
|
34257
|
+
*/
|
|
34258
|
+
var BATTERY_UNREACHABLE_AFTER_MS = 360 * 6e4;
|
|
34225
34259
|
DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
|
|
34226
34260
|
new Set(Object.values(DeviceType));
|
|
34227
34261
|
DeviceFeature.BatteryOperated;
|
|
@@ -231785,13 +231819,15 @@ var reolinkDebugActions = defineCustomActions({ debugRawRead: customAction(RawRe
|
|
|
231785
231819
|
* passato da `adapterStatus: 1` a `0`, con `chargeStatus: 0` da entrambe le
|
|
231786
231820
|
* parti — cioè `chargeStatus` da solo NON distingue i due stati.
|
|
231787
231821
|
*
|
|
231788
|
-
*
|
|
231789
|
-
*
|
|
231790
|
-
*
|
|
231791
|
-
* `
|
|
231792
|
-
*
|
|
231822
|
+
* Due campi assenti NON sono `'none'`: `'none'` è un negativo che il firmware
|
|
231823
|
+
* ha detto ("nessun adattatore, cella ferma"), l'assenza di entrambi è
|
|
231824
|
+
* `'unknown'` — lo stesso confine che `percentage` ha chiuso diventando
|
|
231825
|
+
* `nullable` (D315). Il chiamante (`mapBatteryInfo`) non arriva qui con un
|
|
231826
|
+
* push che non parla di alimentazione: `reportsPowerSource` lo filtra prima e
|
|
231827
|
+
* conserva la convinzione precedente.
|
|
231793
231828
|
*/
|
|
231794
231829
|
function deriveChargingSource(adapterStatus, chargeStatus) {
|
|
231830
|
+
if (adapterStatus === void 0 && chargeStatus === void 0) return "unknown";
|
|
231795
231831
|
const adapter = (adapterStatus ?? "").toLowerCase();
|
|
231796
231832
|
const charge = (chargeStatus ?? "").toLowerCase();
|
|
231797
231833
|
if (adapter.includes("solar")) return "solar";
|
|
@@ -231810,6 +231846,15 @@ function reportsPowerSource(info) {
|
|
|
231810
231846
|
return info.adapterStatus !== void 0 || info.chargeStatus !== void 0;
|
|
231811
231847
|
}
|
|
231812
231848
|
//#endregion
|
|
231849
|
+
//#region src/battery-contact-expiry.ts
|
|
231850
|
+
function shouldWakeForContact(input) {
|
|
231851
|
+
if (input.hubChild) return false;
|
|
231852
|
+
if (!input.sleeping) return false;
|
|
231853
|
+
const last = input.lastContactAt;
|
|
231854
|
+
if (typeof last !== "number" || last <= 0) return false;
|
|
231855
|
+
return input.nowMs - last + input.tickMs >= input.unreachableAfterMs;
|
|
231856
|
+
}
|
|
231857
|
+
//#endregion
|
|
231813
231858
|
//#region src/log-channels.ts
|
|
231814
231859
|
/**
|
|
231815
231860
|
* The diagnostic log CHANNELS `provider-reolink` declares.
|
|
@@ -236921,7 +236966,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
236921
236966
|
mapBatteryInfo(info) {
|
|
236922
236967
|
return {
|
|
236923
236968
|
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 ?? "
|
|
236969
|
+
charging: reportsPowerSource(info) ? deriveChargingSource(info.adapterStatus, info.chargeStatus) : this.state.battery.charging ?? "unknown",
|
|
236925
236970
|
sleeping: info.sleeping === true || this.sleeping,
|
|
236926
236971
|
lastUpdated: Date.now()
|
|
236927
236972
|
};
|
|
@@ -240470,6 +240515,17 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
240470
240515
|
const cycle = (async () => {
|
|
240471
240516
|
try {
|
|
240472
240517
|
if (this.isBattery && this.sleeping) {
|
|
240518
|
+
if (shouldWakeForContact({
|
|
240519
|
+
nowMs: Date.now(),
|
|
240520
|
+
lastContactAt: this.state.battery.lastContactAt,
|
|
240521
|
+
sleeping: true,
|
|
240522
|
+
hubChild: this.isHubChild(),
|
|
240523
|
+
unreachableAfterMs: 216e5,
|
|
240524
|
+
tickMs: ReolinkCamera.BATTERY_UPDATE_INTERVAL_MS
|
|
240525
|
+
})) {
|
|
240526
|
+
await this.wakeForContactRefresh();
|
|
240527
|
+
return;
|
|
240528
|
+
}
|
|
240473
240529
|
this.ctx.logger.debug("battery-update: cam sleeping — skipping cycle without connecting", { tags: { deviceId: this.id } });
|
|
240474
240530
|
return;
|
|
240475
240531
|
}
|
|
@@ -240514,6 +240570,43 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
240514
240570
|
this.batteryUpdateInFlight = cycle;
|
|
240515
240571
|
return cycle;
|
|
240516
240572
|
}
|
|
240573
|
+
/**
|
|
240574
|
+
* D355: wake a standalone battery camera ONCE because its last passive
|
|
240575
|
+
* contact is about to expire, and refresh every slice while it is up.
|
|
240576
|
+
* Bounded by `canProactivelyWake` (the shared cooldown) and by the fact
|
|
240577
|
+
* that a wake that answers resets the silence. A wake that fails is left
|
|
240578
|
+
* alone: the silence then reaches the budget and `deriveBatteryPresence`
|
|
240579
|
+
* says `unreachable` — for the first time on evidence.
|
|
240580
|
+
*/
|
|
240581
|
+
async wakeForContactRefresh() {
|
|
240582
|
+
if (!this.canProactivelyWake("contact-expiry")) return;
|
|
240583
|
+
this.markWakeIssued();
|
|
240584
|
+
const lastContactAt = this.state.battery.lastContactAt ?? 0;
|
|
240585
|
+
const silenceMs = Date.now() - lastContactAt;
|
|
240586
|
+
this.ctx.logger.info("battery contact expiring — waking once to refresh every slice (D355)", {
|
|
240587
|
+
tags: { deviceId: this.id },
|
|
240588
|
+
meta: {
|
|
240589
|
+
silenceMs,
|
|
240590
|
+
budgetMs: BATTERY_UNREACHABLE_AFTER_MS
|
|
240591
|
+
}
|
|
240592
|
+
});
|
|
240593
|
+
try {
|
|
240594
|
+
await this.ensureApi();
|
|
240595
|
+
} catch (err) {
|
|
240596
|
+
this.ctx.logger.warn("battery contact wake FAILED — silence continues and the camera will read unreachable at the budget", {
|
|
240597
|
+
tags: { deviceId: this.id },
|
|
240598
|
+
meta: {
|
|
240599
|
+
silenceMs,
|
|
240600
|
+
error: err instanceof Error ? err.message : String(err)
|
|
240601
|
+
}
|
|
240602
|
+
});
|
|
240603
|
+
return;
|
|
240604
|
+
}
|
|
240605
|
+
await this.refreshBatteryFromApi("demand").catch(() => {});
|
|
240606
|
+
await this.warmCapSlicesOnWake().catch(() => {});
|
|
240607
|
+
await this.alignAuxDevicesState("wake").catch(() => {});
|
|
240608
|
+
await this.warmSnapshotOnWake().catch(() => {});
|
|
240609
|
+
}
|
|
240517
240610
|
/** Arm the periodic battery-cam refresh timer. No-op for wired cams
|
|
240518
240611
|
* (their state is kept fresh by the 10 s aux-align poll and push
|
|
240519
240612
|
* events). Idempotent. */
|
|
@@ -242930,6 +243023,69 @@ function asDebugSocketFlags(value) {
|
|
|
242930
243023
|
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
242931
243024
|
}
|
|
242932
243025
|
//#endregion
|
|
243026
|
+
//#region src/hub-battery-fields.ts
|
|
243027
|
+
/**
|
|
243028
|
+
* The hub boundary for the per-channel battery power fields.
|
|
243029
|
+
*
|
|
243030
|
+
* The Reolink hub/NVR reports `chargeStatus` and `adapterStatus` as NUMBERS
|
|
243031
|
+
* (0/1/2); the standalone camera path reports the same facts as strings, and
|
|
243032
|
+
* `deriveChargingSource` (`battery-charging.ts`) speaks only the string
|
|
243033
|
+
* vocabulary. This module is the translation, and — the part that was
|
|
243034
|
+
* missing — it NAMES a code it cannot translate instead of dropping the field
|
|
243035
|
+
* silently. A dropped field reads as "the push said nothing about power", so
|
|
243036
|
+
* a new firmware value would have kept the previous belief forever, with not
|
|
243037
|
+
* one log line. The ledger lets the hub warn once per (device, field, code).
|
|
243038
|
+
*/
|
|
243039
|
+
var CHARGE_STATUS_BY_CODE = {
|
|
243040
|
+
0: "none",
|
|
243041
|
+
1: "charging",
|
|
243042
|
+
2: "chargeComplete"
|
|
243043
|
+
};
|
|
243044
|
+
var ADAPTER_STATUS_BY_CODE = {
|
|
243045
|
+
0: "none",
|
|
243046
|
+
1: "dc",
|
|
243047
|
+
2: "solarPanel"
|
|
243048
|
+
};
|
|
243049
|
+
function mapOne(field, value, byCode) {
|
|
243050
|
+
if (value === void 0 || value === null) return {};
|
|
243051
|
+
if (typeof value === "string") return { mapped: value };
|
|
243052
|
+
if (typeof value === "number") {
|
|
243053
|
+
const mapped = byCode[value];
|
|
243054
|
+
if (mapped !== void 0) return { mapped };
|
|
243055
|
+
}
|
|
243056
|
+
return { unmapped: {
|
|
243057
|
+
field,
|
|
243058
|
+
value
|
|
243059
|
+
} };
|
|
243060
|
+
}
|
|
243061
|
+
/** Translate one channel's raw CGI power fields; never throws. */
|
|
243062
|
+
function mapHubBatteryPower(cgi) {
|
|
243063
|
+
const charge = mapOne("chargeStatus", cgi?.["chargeStatus"], CHARGE_STATUS_BY_CODE);
|
|
243064
|
+
const adapter = mapOne("adapterStatus", cgi?.["adapterStatus"], ADAPTER_STATUS_BY_CODE);
|
|
243065
|
+
return {
|
|
243066
|
+
fields: {
|
|
243067
|
+
...charge.mapped !== void 0 ? { chargeStatus: charge.mapped } : {},
|
|
243068
|
+
...adapter.mapped !== void 0 ? { adapterStatus: adapter.mapped } : {}
|
|
243069
|
+
},
|
|
243070
|
+
unmapped: [...charge.unmapped !== void 0 ? [charge.unmapped] : [], ...adapter.unmapped !== void 0 ? [adapter.unmapped] : []]
|
|
243071
|
+
};
|
|
243072
|
+
}
|
|
243073
|
+
/**
|
|
243074
|
+
* Remembers which (device, field, code) triples were already reported, so a
|
|
243075
|
+
* polled read warns once per finding and not once per tick. Unbounded only in
|
|
243076
|
+
* theory: the key space is a few cameras times two fields times the handful of
|
|
243077
|
+
* codes a firmware can invent.
|
|
243078
|
+
*/
|
|
243079
|
+
var UnmappedCodeLedger = class {
|
|
243080
|
+
seen = /* @__PURE__ */ new Set();
|
|
243081
|
+
firstSeen(deviceId, field, value) {
|
|
243082
|
+
const key = `${deviceId}:${field}:${String(value)}`;
|
|
243083
|
+
if (this.seen.has(key)) return false;
|
|
243084
|
+
this.seen.add(key);
|
|
243085
|
+
return true;
|
|
243086
|
+
}
|
|
243087
|
+
};
|
|
243088
|
+
//#endregion
|
|
242933
243089
|
//#region src/reolink-hub.ts
|
|
242934
243090
|
var HUB_DISCOVERY_REFRESH_TIMEOUT_MS = 8e3;
|
|
242935
243091
|
/**
|
|
@@ -242991,6 +243147,8 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
242991
243147
|
/** One-shot: the raw per-channel battery payload is logged once per
|
|
242992
243148
|
* process so the firmware's charge-field naming is on record. */
|
|
242993
243149
|
batteryShapeLogged = false;
|
|
243150
|
+
/** Firmware power codes this hub could not translate — warned once each. */
|
|
243151
|
+
unmappedBatteryCodes = new UnmappedCodeLedger();
|
|
242994
243152
|
/** Channels the last successful discovery refresh listed as present but
|
|
242995
243153
|
* NOT adopted — their pushes are expected drops (debug), not faults. */
|
|
242996
243154
|
knownUnadoptedChannels = /* @__PURE__ */ new Set();
|
|
@@ -243471,10 +243629,21 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
243471
243629
|
const child = all.find((d) => d.id === deviceId);
|
|
243472
243630
|
if (!(child instanceof ReolinkCamera)) continue;
|
|
243473
243631
|
const cgi = info.entries[0];
|
|
243632
|
+
const power = mapHubBatteryPower(cgi);
|
|
243633
|
+
for (const { field, value } of power.unmapped) {
|
|
243634
|
+
if (!this.unmappedBatteryCodes.firstSeen(deviceId, field, value)) continue;
|
|
243635
|
+
this.ctx.logger.warn("Reolink Hub: unmapped battery power code — field dropped", {
|
|
243636
|
+
tags: { deviceId },
|
|
243637
|
+
meta: {
|
|
243638
|
+
channel: Number(chKey),
|
|
243639
|
+
field,
|
|
243640
|
+
value
|
|
243641
|
+
}
|
|
243642
|
+
});
|
|
243643
|
+
}
|
|
243474
243644
|
child.applyHubBatteryInfo({
|
|
243475
243645
|
batteryPercent: info.batteryLevel,
|
|
243476
|
-
...
|
|
243477
|
-
...mapHubAdapterStatus(cgi?.["adapterStatus"]) !== void 0 ? { adapterStatus: mapHubAdapterStatus(cgi?.["adapterStatus"]) } : {}
|
|
243646
|
+
...power.fields
|
|
243478
243647
|
});
|
|
243479
243648
|
}
|
|
243480
243649
|
} catch (err) {
|
|
@@ -243915,18 +244084,6 @@ function discoveredStatusFor(d) {
|
|
|
243915
244084
|
* chargeStatus: 0 none · 1 charging · 2 charge complete
|
|
243916
244085
|
* adapterStatus: 0 none · 1 DC adapter · 2 solar panel
|
|
243917
244086
|
*/
|
|
243918
|
-
function mapHubChargeStatus(value) {
|
|
243919
|
-
if (typeof value === "string") return value;
|
|
243920
|
-
if (value === 0) return "none";
|
|
243921
|
-
if (value === 1) return "charging";
|
|
243922
|
-
if (value === 2) return "chargeComplete";
|
|
243923
|
-
}
|
|
243924
|
-
function mapHubAdapterStatus(value) {
|
|
243925
|
-
if (typeof value === "string") return value;
|
|
243926
|
-
if (value === 0) return "none";
|
|
243927
|
-
if (value === 1) return "dc";
|
|
243928
|
-
if (value === 2) return "solarPanel";
|
|
243929
|
-
}
|
|
243930
244087
|
//#endregion
|
|
243931
244088
|
//#region src/creation-schema.ts
|
|
243932
244089
|
/**
|
package/dist/addon.mjs
CHANGED
|
@@ -7587,7 +7587,8 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
|
|
|
7587
7587
|
var CameraSwitchUnavailableReasonSchema = _enum([
|
|
7588
7588
|
"no-provider",
|
|
7589
7589
|
"source-unreachable",
|
|
7590
|
-
"not-configured"
|
|
7590
|
+
"not-configured",
|
|
7591
|
+
"device-disabled"
|
|
7591
7592
|
]);
|
|
7592
7593
|
/**
|
|
7593
7594
|
* One switch, resolved for one camera.
|
|
@@ -13643,7 +13644,12 @@ var ConfigEntrySchema = object({
|
|
|
13643
13644
|
value: unknown(),
|
|
13644
13645
|
description: string().optional()
|
|
13645
13646
|
});
|
|
13646
|
-
|
|
13647
|
+
/**
|
|
13648
|
+
* Only `manual` since D356: the operator picks, nothing links itself. The
|
|
13649
|
+
* field survives on the wire because a viewer already OTA'd parses it —
|
|
13650
|
+
* drop it once no shipped client reads `mode`.
|
|
13651
|
+
*/
|
|
13652
|
+
var LinkedDevicesModeSchema = _enum(["manual"]);
|
|
13647
13653
|
/** One resolved linked device — the compact projection consumers need. */
|
|
13648
13654
|
var LinkedDeviceSchema = object({
|
|
13649
13655
|
deviceId: number(),
|
|
@@ -24315,12 +24321,20 @@ var BatteryStatusSchema = object({
|
|
|
24315
24321
|
* Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
|
|
24316
24322
|
* Reolink-specific for the Solar Panel 2 accessory (will become
|
|
24317
24323
|
* common on other battery cams). `'none'` means running on battery
|
|
24318
|
-
* alone.
|
|
24324
|
+
* alone — a NEGATIVE the firmware actually reported.
|
|
24325
|
+
*
|
|
24326
|
+
* `'unknown'` is the absence of an answer: the provider has not read the
|
|
24327
|
+
* power fields yet, or the firmware reported neither. Before it existed,
|
|
24328
|
+
* two unreadable fields produced `'none'`, and a camera nobody had reached
|
|
24329
|
+
* was drawn "on battery alone" — the same D315 defect `percentage` closed
|
|
24330
|
+
* by going nullable. Consumers SKIP it (no charger state published, no
|
|
24331
|
+
* glyph, no HomeKit ChargingState) rather than coerce it either way.
|
|
24319
24332
|
*/
|
|
24320
24333
|
charging: _enum([
|
|
24321
24334
|
"dc",
|
|
24322
24335
|
"solar",
|
|
24323
|
-
"none"
|
|
24336
|
+
"none",
|
|
24337
|
+
"unknown"
|
|
24324
24338
|
]),
|
|
24325
24339
|
/**
|
|
24326
24340
|
* True when the camera firmware has gone into low-power mode. Battery
|
|
@@ -24410,7 +24424,7 @@ onStatusChanged: { data: object({
|
|
|
24410
24424
|
kind: "push",
|
|
24411
24425
|
empty: {
|
|
24412
24426
|
percentage: 0,
|
|
24413
|
-
charging: "
|
|
24427
|
+
charging: "unknown",
|
|
24414
24428
|
sleeping: false,
|
|
24415
24429
|
lastUpdated: 0
|
|
24416
24430
|
}
|
|
@@ -34118,8 +34132,17 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
34118
34132
|
async onRestoreDevices(savedDevices) {
|
|
34119
34133
|
const restored = /* @__PURE__ */ new Set();
|
|
34120
34134
|
const failures = [];
|
|
34135
|
+
const liveStableIds = new Set((await this.ctx.kernel.devices.getAll()).map((device) => device.stableId));
|
|
34121
34136
|
const attemptRestore = async (saved) => {
|
|
34122
34137
|
if (restored.has(saved.id)) return;
|
|
34138
|
+
if (liveStableIds.has(saved.stableId)) {
|
|
34139
|
+
this.ctx.logger.debug("Restore row already live — not dialled again", { tags: {
|
|
34140
|
+
deviceId: saved.id,
|
|
34141
|
+
stableId: saved.stableId
|
|
34142
|
+
} });
|
|
34143
|
+
restored.add(saved.id);
|
|
34144
|
+
return;
|
|
34145
|
+
}
|
|
34123
34146
|
const Class = this.deviceClasses[saved.type];
|
|
34124
34147
|
if (!Class) throw new Error(`no device class registered for type "${saved.type}"`);
|
|
34125
34148
|
if (saved.parentDeviceId !== null && !restored.has(saved.parentDeviceId)) throw new Error(`parent device ${saved.parentDeviceId} not restored`);
|
|
@@ -34217,6 +34240,17 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
34217
34240
|
return toDeviceSummary(device, this.addonId);
|
|
34218
34241
|
}
|
|
34219
34242
|
};
|
|
34243
|
+
/**
|
|
34244
|
+
* Default silence budget before a sleeping battery camera is called gone.
|
|
34245
|
+
*
|
|
34246
|
+
* Sized off the mechanism that produces contact, not off taste: a battery cam
|
|
34247
|
+
* on this deployment surfaces a firmware push (battery level, sleep/wake, or a
|
|
34248
|
+
* motion email) on the order of hours even with no visitors, and the snapshot
|
|
34249
|
+
* wrapper's own battery window is 1 hour. Six hours is therefore several
|
|
34250
|
+
* missed opportunities, not one — so a single quiet night does not raise a
|
|
34251
|
+
* fault, and a genuinely flat camera is named well inside a day.
|
|
34252
|
+
*/
|
|
34253
|
+
var BATTERY_UNREACHABLE_AFTER_MS = 360 * 6e4;
|
|
34220
34254
|
DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
|
|
34221
34255
|
new Set(Object.values(DeviceType));
|
|
34222
34256
|
DeviceFeature.BatteryOperated;
|
|
@@ -231765,13 +231799,15 @@ var reolinkDebugActions = defineCustomActions({ debugRawRead: customAction(RawRe
|
|
|
231765
231799
|
* passato da `adapterStatus: 1` a `0`, con `chargeStatus: 0` da entrambe le
|
|
231766
231800
|
* parti — cioè `chargeStatus` da solo NON distingue i due stati.
|
|
231767
231801
|
*
|
|
231768
|
-
*
|
|
231769
|
-
*
|
|
231770
|
-
*
|
|
231771
|
-
* `
|
|
231772
|
-
*
|
|
231802
|
+
* Due campi assenti NON sono `'none'`: `'none'` è un negativo che il firmware
|
|
231803
|
+
* ha detto ("nessun adattatore, cella ferma"), l'assenza di entrambi è
|
|
231804
|
+
* `'unknown'` — lo stesso confine che `percentage` ha chiuso diventando
|
|
231805
|
+
* `nullable` (D315). Il chiamante (`mapBatteryInfo`) non arriva qui con un
|
|
231806
|
+
* push che non parla di alimentazione: `reportsPowerSource` lo filtra prima e
|
|
231807
|
+
* conserva la convinzione precedente.
|
|
231773
231808
|
*/
|
|
231774
231809
|
function deriveChargingSource(adapterStatus, chargeStatus) {
|
|
231810
|
+
if (adapterStatus === void 0 && chargeStatus === void 0) return "unknown";
|
|
231775
231811
|
const adapter = (adapterStatus ?? "").toLowerCase();
|
|
231776
231812
|
const charge = (chargeStatus ?? "").toLowerCase();
|
|
231777
231813
|
if (adapter.includes("solar")) return "solar";
|
|
@@ -231790,6 +231826,15 @@ function reportsPowerSource(info) {
|
|
|
231790
231826
|
return info.adapterStatus !== void 0 || info.chargeStatus !== void 0;
|
|
231791
231827
|
}
|
|
231792
231828
|
//#endregion
|
|
231829
|
+
//#region src/battery-contact-expiry.ts
|
|
231830
|
+
function shouldWakeForContact(input) {
|
|
231831
|
+
if (input.hubChild) return false;
|
|
231832
|
+
if (!input.sleeping) return false;
|
|
231833
|
+
const last = input.lastContactAt;
|
|
231834
|
+
if (typeof last !== "number" || last <= 0) return false;
|
|
231835
|
+
return input.nowMs - last + input.tickMs >= input.unreachableAfterMs;
|
|
231836
|
+
}
|
|
231837
|
+
//#endregion
|
|
231793
231838
|
//#region src/log-channels.ts
|
|
231794
231839
|
/**
|
|
231795
231840
|
* The diagnostic log CHANNELS `provider-reolink` declares.
|
|
@@ -236901,7 +236946,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
236901
236946
|
mapBatteryInfo(info) {
|
|
236902
236947
|
return {
|
|
236903
236948
|
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 ?? "
|
|
236949
|
+
charging: reportsPowerSource(info) ? deriveChargingSource(info.adapterStatus, info.chargeStatus) : this.state.battery.charging ?? "unknown",
|
|
236905
236950
|
sleeping: info.sleeping === true || this.sleeping,
|
|
236906
236951
|
lastUpdated: Date.now()
|
|
236907
236952
|
};
|
|
@@ -240450,6 +240495,17 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
240450
240495
|
const cycle = (async () => {
|
|
240451
240496
|
try {
|
|
240452
240497
|
if (this.isBattery && this.sleeping) {
|
|
240498
|
+
if (shouldWakeForContact({
|
|
240499
|
+
nowMs: Date.now(),
|
|
240500
|
+
lastContactAt: this.state.battery.lastContactAt,
|
|
240501
|
+
sleeping: true,
|
|
240502
|
+
hubChild: this.isHubChild(),
|
|
240503
|
+
unreachableAfterMs: 216e5,
|
|
240504
|
+
tickMs: ReolinkCamera.BATTERY_UPDATE_INTERVAL_MS
|
|
240505
|
+
})) {
|
|
240506
|
+
await this.wakeForContactRefresh();
|
|
240507
|
+
return;
|
|
240508
|
+
}
|
|
240453
240509
|
this.ctx.logger.debug("battery-update: cam sleeping — skipping cycle without connecting", { tags: { deviceId: this.id } });
|
|
240454
240510
|
return;
|
|
240455
240511
|
}
|
|
@@ -240494,6 +240550,43 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
240494
240550
|
this.batteryUpdateInFlight = cycle;
|
|
240495
240551
|
return cycle;
|
|
240496
240552
|
}
|
|
240553
|
+
/**
|
|
240554
|
+
* D355: wake a standalone battery camera ONCE because its last passive
|
|
240555
|
+
* contact is about to expire, and refresh every slice while it is up.
|
|
240556
|
+
* Bounded by `canProactivelyWake` (the shared cooldown) and by the fact
|
|
240557
|
+
* that a wake that answers resets the silence. A wake that fails is left
|
|
240558
|
+
* alone: the silence then reaches the budget and `deriveBatteryPresence`
|
|
240559
|
+
* says `unreachable` — for the first time on evidence.
|
|
240560
|
+
*/
|
|
240561
|
+
async wakeForContactRefresh() {
|
|
240562
|
+
if (!this.canProactivelyWake("contact-expiry")) return;
|
|
240563
|
+
this.markWakeIssued();
|
|
240564
|
+
const lastContactAt = this.state.battery.lastContactAt ?? 0;
|
|
240565
|
+
const silenceMs = Date.now() - lastContactAt;
|
|
240566
|
+
this.ctx.logger.info("battery contact expiring — waking once to refresh every slice (D355)", {
|
|
240567
|
+
tags: { deviceId: this.id },
|
|
240568
|
+
meta: {
|
|
240569
|
+
silenceMs,
|
|
240570
|
+
budgetMs: BATTERY_UNREACHABLE_AFTER_MS
|
|
240571
|
+
}
|
|
240572
|
+
});
|
|
240573
|
+
try {
|
|
240574
|
+
await this.ensureApi();
|
|
240575
|
+
} catch (err) {
|
|
240576
|
+
this.ctx.logger.warn("battery contact wake FAILED — silence continues and the camera will read unreachable at the budget", {
|
|
240577
|
+
tags: { deviceId: this.id },
|
|
240578
|
+
meta: {
|
|
240579
|
+
silenceMs,
|
|
240580
|
+
error: err instanceof Error ? err.message : String(err)
|
|
240581
|
+
}
|
|
240582
|
+
});
|
|
240583
|
+
return;
|
|
240584
|
+
}
|
|
240585
|
+
await this.refreshBatteryFromApi("demand").catch(() => {});
|
|
240586
|
+
await this.warmCapSlicesOnWake().catch(() => {});
|
|
240587
|
+
await this.alignAuxDevicesState("wake").catch(() => {});
|
|
240588
|
+
await this.warmSnapshotOnWake().catch(() => {});
|
|
240589
|
+
}
|
|
240497
240590
|
/** Arm the periodic battery-cam refresh timer. No-op for wired cams
|
|
240498
240591
|
* (their state is kept fresh by the 10 s aux-align poll and push
|
|
240499
240592
|
* events). Idempotent. */
|
|
@@ -242910,6 +243003,69 @@ function asDebugSocketFlags(value) {
|
|
|
242910
243003
|
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
242911
243004
|
}
|
|
242912
243005
|
//#endregion
|
|
243006
|
+
//#region src/hub-battery-fields.ts
|
|
243007
|
+
/**
|
|
243008
|
+
* The hub boundary for the per-channel battery power fields.
|
|
243009
|
+
*
|
|
243010
|
+
* The Reolink hub/NVR reports `chargeStatus` and `adapterStatus` as NUMBERS
|
|
243011
|
+
* (0/1/2); the standalone camera path reports the same facts as strings, and
|
|
243012
|
+
* `deriveChargingSource` (`battery-charging.ts`) speaks only the string
|
|
243013
|
+
* vocabulary. This module is the translation, and — the part that was
|
|
243014
|
+
* missing — it NAMES a code it cannot translate instead of dropping the field
|
|
243015
|
+
* silently. A dropped field reads as "the push said nothing about power", so
|
|
243016
|
+
* a new firmware value would have kept the previous belief forever, with not
|
|
243017
|
+
* one log line. The ledger lets the hub warn once per (device, field, code).
|
|
243018
|
+
*/
|
|
243019
|
+
var CHARGE_STATUS_BY_CODE = {
|
|
243020
|
+
0: "none",
|
|
243021
|
+
1: "charging",
|
|
243022
|
+
2: "chargeComplete"
|
|
243023
|
+
};
|
|
243024
|
+
var ADAPTER_STATUS_BY_CODE = {
|
|
243025
|
+
0: "none",
|
|
243026
|
+
1: "dc",
|
|
243027
|
+
2: "solarPanel"
|
|
243028
|
+
};
|
|
243029
|
+
function mapOne(field, value, byCode) {
|
|
243030
|
+
if (value === void 0 || value === null) return {};
|
|
243031
|
+
if (typeof value === "string") return { mapped: value };
|
|
243032
|
+
if (typeof value === "number") {
|
|
243033
|
+
const mapped = byCode[value];
|
|
243034
|
+
if (mapped !== void 0) return { mapped };
|
|
243035
|
+
}
|
|
243036
|
+
return { unmapped: {
|
|
243037
|
+
field,
|
|
243038
|
+
value
|
|
243039
|
+
} };
|
|
243040
|
+
}
|
|
243041
|
+
/** Translate one channel's raw CGI power fields; never throws. */
|
|
243042
|
+
function mapHubBatteryPower(cgi) {
|
|
243043
|
+
const charge = mapOne("chargeStatus", cgi?.["chargeStatus"], CHARGE_STATUS_BY_CODE);
|
|
243044
|
+
const adapter = mapOne("adapterStatus", cgi?.["adapterStatus"], ADAPTER_STATUS_BY_CODE);
|
|
243045
|
+
return {
|
|
243046
|
+
fields: {
|
|
243047
|
+
...charge.mapped !== void 0 ? { chargeStatus: charge.mapped } : {},
|
|
243048
|
+
...adapter.mapped !== void 0 ? { adapterStatus: adapter.mapped } : {}
|
|
243049
|
+
},
|
|
243050
|
+
unmapped: [...charge.unmapped !== void 0 ? [charge.unmapped] : [], ...adapter.unmapped !== void 0 ? [adapter.unmapped] : []]
|
|
243051
|
+
};
|
|
243052
|
+
}
|
|
243053
|
+
/**
|
|
243054
|
+
* Remembers which (device, field, code) triples were already reported, so a
|
|
243055
|
+
* polled read warns once per finding and not once per tick. Unbounded only in
|
|
243056
|
+
* theory: the key space is a few cameras times two fields times the handful of
|
|
243057
|
+
* codes a firmware can invent.
|
|
243058
|
+
*/
|
|
243059
|
+
var UnmappedCodeLedger = class {
|
|
243060
|
+
seen = /* @__PURE__ */ new Set();
|
|
243061
|
+
firstSeen(deviceId, field, value) {
|
|
243062
|
+
const key = `${deviceId}:${field}:${String(value)}`;
|
|
243063
|
+
if (this.seen.has(key)) return false;
|
|
243064
|
+
this.seen.add(key);
|
|
243065
|
+
return true;
|
|
243066
|
+
}
|
|
243067
|
+
};
|
|
243068
|
+
//#endregion
|
|
242913
243069
|
//#region src/reolink-hub.ts
|
|
242914
243070
|
var HUB_DISCOVERY_REFRESH_TIMEOUT_MS = 8e3;
|
|
242915
243071
|
/**
|
|
@@ -242971,6 +243127,8 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
242971
243127
|
/** One-shot: the raw per-channel battery payload is logged once per
|
|
242972
243128
|
* process so the firmware's charge-field naming is on record. */
|
|
242973
243129
|
batteryShapeLogged = false;
|
|
243130
|
+
/** Firmware power codes this hub could not translate — warned once each. */
|
|
243131
|
+
unmappedBatteryCodes = new UnmappedCodeLedger();
|
|
242974
243132
|
/** Channels the last successful discovery refresh listed as present but
|
|
242975
243133
|
* NOT adopted — their pushes are expected drops (debug), not faults. */
|
|
242976
243134
|
knownUnadoptedChannels = /* @__PURE__ */ new Set();
|
|
@@ -243451,10 +243609,21 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
243451
243609
|
const child = all.find((d) => d.id === deviceId);
|
|
243452
243610
|
if (!(child instanceof ReolinkCamera)) continue;
|
|
243453
243611
|
const cgi = info.entries[0];
|
|
243612
|
+
const power = mapHubBatteryPower(cgi);
|
|
243613
|
+
for (const { field, value } of power.unmapped) {
|
|
243614
|
+
if (!this.unmappedBatteryCodes.firstSeen(deviceId, field, value)) continue;
|
|
243615
|
+
this.ctx.logger.warn("Reolink Hub: unmapped battery power code — field dropped", {
|
|
243616
|
+
tags: { deviceId },
|
|
243617
|
+
meta: {
|
|
243618
|
+
channel: Number(chKey),
|
|
243619
|
+
field,
|
|
243620
|
+
value
|
|
243621
|
+
}
|
|
243622
|
+
});
|
|
243623
|
+
}
|
|
243454
243624
|
child.applyHubBatteryInfo({
|
|
243455
243625
|
batteryPercent: info.batteryLevel,
|
|
243456
|
-
...
|
|
243457
|
-
...mapHubAdapterStatus(cgi?.["adapterStatus"]) !== void 0 ? { adapterStatus: mapHubAdapterStatus(cgi?.["adapterStatus"]) } : {}
|
|
243626
|
+
...power.fields
|
|
243458
243627
|
});
|
|
243459
243628
|
}
|
|
243460
243629
|
} catch (err) {
|
|
@@ -243895,18 +244064,6 @@ function discoveredStatusFor(d) {
|
|
|
243895
244064
|
* chargeStatus: 0 none · 1 charging · 2 charge complete
|
|
243896
244065
|
* adapterStatus: 0 none · 1 DC adapter · 2 solar panel
|
|
243897
244066
|
*/
|
|
243898
|
-
function mapHubChargeStatus(value) {
|
|
243899
|
-
if (typeof value === "string") return value;
|
|
243900
|
-
if (value === 0) return "none";
|
|
243901
|
-
if (value === 1) return "charging";
|
|
243902
|
-
if (value === 2) return "chargeComplete";
|
|
243903
|
-
}
|
|
243904
|
-
function mapHubAdapterStatus(value) {
|
|
243905
|
-
if (typeof value === "string") return value;
|
|
243906
|
-
if (value === 0) return "none";
|
|
243907
|
-
if (value === 1) return "dc";
|
|
243908
|
-
if (value === 2) return "solarPanel";
|
|
243909
|
-
}
|
|
243910
244067
|
//#endregion
|
|
243911
244068
|
//#region src/creation-schema.ts
|
|
243912
244069
|
/**
|