@camstack/addon-provider-reolink 1.2.91 → 1.2.93
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 +256 -2
- package/dist/addon.mjs +256 -2
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -26,7 +26,7 @@ let fs_promises = require("fs/promises");
|
|
|
26
26
|
fs_promises = require_chunk.__toESM(fs_promises, 1);
|
|
27
27
|
let node_os = require("node:os");
|
|
28
28
|
node_os = require_chunk.__toESM(node_os);
|
|
29
|
-
//#region ../types/dist/event-category-
|
|
29
|
+
//#region ../types/dist/event-category-zAv7pMUz.mjs
|
|
30
30
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
31
31
|
EventCategory["SystemBoot"] = "system.boot";
|
|
32
32
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -398,6 +398,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
398
398
|
*/
|
|
399
399
|
EventCategory["BatteryOnStatusChanged"] = "battery.onStatusChanged";
|
|
400
400
|
/**
|
|
401
|
+
* Cap event fired by every device that registers the `network-link`
|
|
402
|
+
* capability. Mirrors the cap definition's `onStatusChanged`. Carries
|
|
403
|
+
* `{ deviceId, status: NetworkLinkStatus }` — a link switch or a signal
|
|
404
|
+
* reading that moved.
|
|
405
|
+
*/
|
|
406
|
+
EventCategory["NetworkLinkOnStatusChanged"] = "network-link.onStatusChanged";
|
|
407
|
+
/**
|
|
401
408
|
* Emitted by the battery cap provider WHEN `wakeForStream` enters the
|
|
402
409
|
* "wake in progress" window — between the Baichuan wake-up issue and
|
|
403
410
|
* the camera's first dialed-back RTP packet. The stream-broker
|
|
@@ -20034,6 +20041,33 @@ var RecentTracksQueryInput = object({
|
|
|
20034
20041
|
*/
|
|
20035
20042
|
classes: array(string()).optional()
|
|
20036
20043
|
});
|
|
20044
|
+
/**
|
|
20045
|
+
* A Summary (D359): the per-camera session envelope that references tracks.
|
|
20046
|
+
* `groupShotKey` is the media key of its group shot (`ownerKind: 'summary'`,
|
|
20047
|
+
* served by the event-media plane by key), null until the first frame with a
|
|
20048
|
+
* member visible has been materialised.
|
|
20049
|
+
*/
|
|
20050
|
+
var SummarySchema = object({
|
|
20051
|
+
id: string(),
|
|
20052
|
+
deviceId: number(),
|
|
20053
|
+
openedAt: number(),
|
|
20054
|
+
lastActiveAt: number(),
|
|
20055
|
+
sealedAt: number().nullable(),
|
|
20056
|
+
memberCount: number().int().nonnegative(),
|
|
20057
|
+
groupShotKey: string().nullable()
|
|
20058
|
+
});
|
|
20059
|
+
var SummariesQueryInput = object({
|
|
20060
|
+
deviceIds: array(number()).min(1),
|
|
20061
|
+
since: number().optional(),
|
|
20062
|
+
until: number().optional(),
|
|
20063
|
+
limit: number().int().min(1).max(200).default(50)
|
|
20064
|
+
});
|
|
20065
|
+
var SummariesPageSchema = object({ summaries: array(SummarySchema).readonly() });
|
|
20066
|
+
var SummaryDetailSchema = object({
|
|
20067
|
+
summary: SummarySchema,
|
|
20068
|
+
/** Member track ids, in absorption order. */
|
|
20069
|
+
trackIds: array(string()).readonly()
|
|
20070
|
+
});
|
|
20037
20071
|
var RecentTracksPageSchema = object({
|
|
20038
20072
|
/** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
|
|
20039
20073
|
tracks: array(TrackSchema).readonly(),
|
|
@@ -20434,7 +20468,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
20434
20468
|
* timeline would read as a camera that saw nothing.
|
|
20435
20469
|
*/
|
|
20436
20470
|
classes: array(string()).optional()
|
|
20437
|
-
}), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
|
|
20471
|
+
}), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(SummariesQueryInput, SummariesPageSchema), method(object({ id: string() }), SummaryDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
|
|
20438
20472
|
kind: "mutation",
|
|
20439
20473
|
auth: "admin"
|
|
20440
20474
|
}), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
|
|
@@ -24471,6 +24505,108 @@ onStatusChanged: { data: object({
|
|
|
24471
24505
|
volatileStateFields: ["lastUpdated"]
|
|
24472
24506
|
};
|
|
24473
24507
|
/**
|
|
24508
|
+
* Network-link snapshot. Same shape for every provider (a Reolink wifi
|
|
24509
|
+
* camera, a Home Assistant device with a signal-strength sensor, a Tapo
|
|
24510
|
+
* plug): one slice under `device.runtimeState['network-link']`, one badge,
|
|
24511
|
+
* one Home Assistant projection.
|
|
24512
|
+
*/
|
|
24513
|
+
var NetworkLinkStatusSchema = object({
|
|
24514
|
+
/** The link the device is on. `'unknown'` = not read yet, not "no link". */
|
|
24515
|
+
type: _enum([
|
|
24516
|
+
"wifi",
|
|
24517
|
+
"ethernet",
|
|
24518
|
+
"cellular",
|
|
24519
|
+
"unknown"
|
|
24520
|
+
]),
|
|
24521
|
+
/**
|
|
24522
|
+
* Link quality, 0..100 inclusive, normalised by the provider from whatever
|
|
24523
|
+
* the firmware reports (bars, RSSI, a vendor scale). **`null` means NOT
|
|
24524
|
+
* KNOWN or NOT APPLICABLE** — a wired link has no signal, and a wireless
|
|
24525
|
+
* one whose reading has not landed must not be drawn at 0 %. Consumers
|
|
24526
|
+
* SKIP a null rather than coerce it.
|
|
24527
|
+
*/
|
|
24528
|
+
signalPercent: number().min(0).max(100).nullable(),
|
|
24529
|
+
/** Raw received signal strength in dBm, when the firmware reports one. */
|
|
24530
|
+
rssiDbm: number().optional(),
|
|
24531
|
+
/** Network name of a wireless link, when the firmware reports it. */
|
|
24532
|
+
ssid: string().optional(),
|
|
24533
|
+
/** Ms epoch of the last observation. Lets consumers reason about freshness. */
|
|
24534
|
+
lastUpdated: number()
|
|
24535
|
+
});
|
|
24536
|
+
/** The slice a provider seeds before its first read: nothing is known yet. */
|
|
24537
|
+
var NETWORK_LINK_UNKNOWN = {
|
|
24538
|
+
type: "unknown",
|
|
24539
|
+
signalPercent: null,
|
|
24540
|
+
lastUpdated: 0
|
|
24541
|
+
};
|
|
24542
|
+
/**
|
|
24543
|
+
* Normalise a signal the firmware reports as BARS (0..`maxBars`) to 0..100.
|
|
24544
|
+
* Out-of-range or non-finite input is not a reading: `null`.
|
|
24545
|
+
*/
|
|
24546
|
+
function signalPercentFromBars(bars, maxBars) {
|
|
24547
|
+
if (bars === void 0 || !Number.isFinite(bars) || maxBars <= 0) return null;
|
|
24548
|
+
if (bars < 0 || bars > maxBars) return null;
|
|
24549
|
+
return Math.round(bars / maxBars * 100);
|
|
24550
|
+
}
|
|
24551
|
+
/**
|
|
24552
|
+
* Normalise an RSSI in dBm to 0..100 on the usual wifi scale: -100 dBm and
|
|
24553
|
+
* below is 0 %, -50 dBm and above is 100 %, linear between. Non-finite or
|
|
24554
|
+
* positive input is not an RSSI: `null`.
|
|
24555
|
+
*/
|
|
24556
|
+
function signalPercentFromRssi(rssiDbm) {
|
|
24557
|
+
if (rssiDbm === void 0 || !Number.isFinite(rssiDbm) || rssiDbm > 0) return null;
|
|
24558
|
+
return Math.round((Math.min(-50, Math.max(-100, rssiDbm)) + 100) * 2);
|
|
24559
|
+
}
|
|
24560
|
+
var networkLinkCapability = {
|
|
24561
|
+
name: "network-link",
|
|
24562
|
+
scope: "device",
|
|
24563
|
+
deviceNative: true,
|
|
24564
|
+
mode: "singleton",
|
|
24565
|
+
deviceTypes: [
|
|
24566
|
+
DeviceType.Camera,
|
|
24567
|
+
DeviceType.Sensor,
|
|
24568
|
+
DeviceType.Button,
|
|
24569
|
+
DeviceType.Switch,
|
|
24570
|
+
DeviceType.Light,
|
|
24571
|
+
DeviceType.Lock,
|
|
24572
|
+
DeviceType.Siren
|
|
24573
|
+
],
|
|
24574
|
+
methods: {},
|
|
24575
|
+
events: {
|
|
24576
|
+
/**
|
|
24577
|
+
* Emitted whenever the cached status changes (a link switch, a signal
|
|
24578
|
+
* reading that moved). Mirrored on the parent chain by the
|
|
24579
|
+
* DeviceEventPropagator like `battery.onStatusChanged`.
|
|
24580
|
+
*/
|
|
24581
|
+
onStatusChanged: { data: object({
|
|
24582
|
+
deviceId: number(),
|
|
24583
|
+
status: NetworkLinkStatusSchema
|
|
24584
|
+
}) } },
|
|
24585
|
+
status: {
|
|
24586
|
+
schema: NetworkLinkStatusSchema,
|
|
24587
|
+
kind: "push",
|
|
24588
|
+
empty: NETWORK_LINK_UNKNOWN
|
|
24589
|
+
},
|
|
24590
|
+
/**
|
|
24591
|
+
* Runtime-state slice — every provider stores the same shape under
|
|
24592
|
+
* `device.runtimeState['network-link']`, read once by the badge and the
|
|
24593
|
+
* Home Assistant projector regardless of the driver.
|
|
24594
|
+
*/
|
|
24595
|
+
runtimeState: NetworkLinkStatusSchema,
|
|
24596
|
+
/**
|
|
24597
|
+
* Runtime-state durability: **restored** — a link reading is slow to
|
|
24598
|
+
* change and a sleeping battery camera may not report for hours; the
|
|
24599
|
+
* restored slice is what the badge shows until the next read.
|
|
24600
|
+
*
|
|
24601
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
24602
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
24603
|
+
*/
|
|
24604
|
+
durability: "restored",
|
|
24605
|
+
/** Clock fields: written, but excluded from the compare that decides
|
|
24606
|
+
* whether persisting is worth a SQLite commit. */
|
|
24607
|
+
volatileStateFields: ["lastUpdated"]
|
|
24608
|
+
};
|
|
24609
|
+
/**
|
|
24474
24610
|
* Generic boolean sensor — last-resort fallback when no domain-
|
|
24475
24611
|
* specific binary cap fits (Home Assistant `binary_sensor` without a
|
|
24476
24612
|
* known `device_class`, or a domain we haven't typed yet). Pure
|
|
@@ -33155,6 +33291,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
33155
33291
|
motionTrigger: motionTriggerCapability,
|
|
33156
33292
|
motionZones: motionZonesCapability,
|
|
33157
33293
|
nativeObjectDetection: nativeObjectDetectionCapability,
|
|
33294
|
+
networkLink: networkLinkCapability,
|
|
33158
33295
|
notifier: notifierCapability,
|
|
33159
33296
|
numericSensor: numericSensorCapability,
|
|
33160
33297
|
petFeeder: petFeederCapability,
|
|
@@ -37735,6 +37872,12 @@ Object.freeze({
|
|
|
37735
37872
|
addonId: null,
|
|
37736
37873
|
access: "view"
|
|
37737
37874
|
},
|
|
37875
|
+
"pipelineAnalytics.getSummary": {
|
|
37876
|
+
capName: "pipeline-analytics",
|
|
37877
|
+
capScope: "device",
|
|
37878
|
+
addonId: null,
|
|
37879
|
+
access: "view"
|
|
37880
|
+
},
|
|
37738
37881
|
"pipelineAnalytics.getTrack": {
|
|
37739
37882
|
capName: "pipeline-analytics",
|
|
37740
37883
|
capScope: "device",
|
|
@@ -37813,6 +37956,12 @@ Object.freeze({
|
|
|
37813
37956
|
addonId: null,
|
|
37814
37957
|
access: "view"
|
|
37815
37958
|
},
|
|
37959
|
+
"pipelineAnalytics.listSummaries": {
|
|
37960
|
+
capName: "pipeline-analytics",
|
|
37961
|
+
capScope: "device",
|
|
37962
|
+
addonId: null,
|
|
37963
|
+
access: "view"
|
|
37964
|
+
},
|
|
37816
37965
|
"pipelineAnalytics.listTrackMedia": {
|
|
37817
37966
|
capName: "pipeline-analytics",
|
|
37818
37967
|
capScope: "device",
|
|
@@ -41508,6 +41657,11 @@ Object.freeze({
|
|
|
41508
41657
|
form: "array",
|
|
41509
41658
|
optional: true
|
|
41510
41659
|
}],
|
|
41660
|
+
"pipelineAnalytics.listSummaries": [{
|
|
41661
|
+
name: "deviceIds",
|
|
41662
|
+
form: "array",
|
|
41663
|
+
optional: false
|
|
41664
|
+
}],
|
|
41511
41665
|
"pipelineAnalytics.listTrackMedia": [{
|
|
41512
41666
|
name: "deviceId",
|
|
41513
41667
|
form: "single",
|
|
@@ -233550,6 +233704,50 @@ async function populateReolinkMetadata(api, channel, target) {
|
|
|
233550
233704
|
});
|
|
233551
233705
|
}
|
|
233552
233706
|
}
|
|
233707
|
+
/**
|
|
233708
|
+
* The link the label names. A label the firmware did not give is `unknown`,
|
|
233709
|
+
* UNLESS a wifi signal was read — a camera that answers a wifi signal is on
|
|
233710
|
+
* wifi whatever its label says.
|
|
233711
|
+
*/
|
|
233712
|
+
function linkTypeOf(readout) {
|
|
233713
|
+
const label = (readout.activeLink ?? "").toLowerCase();
|
|
233714
|
+
if (label.includes("wifi") || label.includes("wlan") || label.includes("wireless")) return "wifi";
|
|
233715
|
+
if (label.includes("lan") || label.includes("eth") || label.includes("wire")) return "ethernet";
|
|
233716
|
+
if (/\b(4g|5g|lte|cell|sim)\b/.test(label)) return "cellular";
|
|
233717
|
+
if (readout.wifiSignal !== void 0) return "wifi";
|
|
233718
|
+
return "unknown";
|
|
233719
|
+
}
|
|
233720
|
+
/** True when the label names a wireless link — the only case worth a signal read. */
|
|
233721
|
+
function isWirelessLabel(activeLink) {
|
|
233722
|
+
const type = linkTypeOf({
|
|
233723
|
+
activeLink,
|
|
233724
|
+
wifiSignal: void 0
|
|
233725
|
+
});
|
|
233726
|
+
return type === "wifi" || type === "cellular";
|
|
233727
|
+
}
|
|
233728
|
+
/** 0..4 → bars; 5..100 → percent; negative → dBm; anything else → not a reading. */
|
|
233729
|
+
function signalPercentOf(raw) {
|
|
233730
|
+
if (raw === void 0 || !Number.isFinite(raw)) return null;
|
|
233731
|
+
if (raw < 0) return signalPercentFromRssi(raw);
|
|
233732
|
+
if (raw <= 4) return signalPercentFromBars(raw, 4);
|
|
233733
|
+
if (raw <= 100) return Math.round(raw);
|
|
233734
|
+
return null;
|
|
233735
|
+
}
|
|
233736
|
+
function mapNetworkReadout(readout, now) {
|
|
233737
|
+
const type = linkTypeOf(readout);
|
|
233738
|
+
const raw = readout.wifiSignal;
|
|
233739
|
+
const wireless = type === "wifi" || type === "cellular";
|
|
233740
|
+
const signalPercent = wireless ? signalPercentOf(raw) : null;
|
|
233741
|
+
const rssiDbm = wireless && raw !== void 0 && Number.isFinite(raw) && raw < 0 ? raw : void 0;
|
|
233742
|
+
const ssid = wireless && readout.ssid !== void 0 && readout.ssid !== "" ? readout.ssid : void 0;
|
|
233743
|
+
return {
|
|
233744
|
+
type,
|
|
233745
|
+
signalPercent,
|
|
233746
|
+
...rssiDbm !== void 0 ? { rssiDbm } : {},
|
|
233747
|
+
...ssid !== void 0 ? { ssid } : {},
|
|
233748
|
+
lastUpdated: now
|
|
233749
|
+
};
|
|
233750
|
+
}
|
|
233553
233751
|
//#endregion
|
|
233554
233752
|
//#region src/error-classifier.ts
|
|
233555
233753
|
/**
|
|
@@ -236349,6 +236547,58 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
236349
236547
|
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
236350
236548
|
});
|
|
236351
236549
|
});
|
|
236550
|
+
await this.refreshNetworkLinkFromApi(api, "probe");
|
|
236551
|
+
}
|
|
236552
|
+
/**
|
|
236553
|
+
* Register the `network-link` cap: the read path serves the slice, the
|
|
236554
|
+
* writers are {@link refreshNetworkLinkFromApi}. Seeded unknown so the
|
|
236555
|
+
* restored slice (if any) stays valid and a fresh device draws no link.
|
|
236556
|
+
*/
|
|
236557
|
+
registerNetworkLink() {
|
|
236558
|
+
this.ctx.registerNativeCap(networkLinkCapability, { getStatus: async ({ deviceId }) => {
|
|
236559
|
+
if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
236560
|
+
return this.state.networkLink;
|
|
236561
|
+
} });
|
|
236562
|
+
if (this.getCapSlice(networkLinkCapability) === null) this.setCapSlice(networkLinkCapability, NETWORK_LINK_UNKNOWN);
|
|
236563
|
+
}
|
|
236564
|
+
/**
|
|
236565
|
+
* Read the active link and, on a wireless one, its signal and network name
|
|
236566
|
+
* over a socket that is ALREADY up. Called post-login and after a
|
|
236567
|
+
* successful battery read — never on its own timer, so a sleeping battery
|
|
236568
|
+
* camera is never woken for a bar count. Each read is best-effort with its
|
|
236569
|
+
* own bound; a failure keeps the last slice and says so at debug level.
|
|
236570
|
+
*/
|
|
236571
|
+
async refreshNetworkLinkFromApi(api, reason) {
|
|
236572
|
+
const channel = this.getChannel();
|
|
236573
|
+
const timeoutMs = ReolinkCamera.NETWORK_LINK_READ_TIMEOUT_MS;
|
|
236574
|
+
try {
|
|
236575
|
+
const activeLink = (await api.getNetworkInfo(channel, { timeoutMs }))?.activeLink;
|
|
236576
|
+
const askWireless = activeLink === void 0 || isWirelessLabel(activeLink);
|
|
236577
|
+
const wifiSignal = askWireless ? (await api.getWifiSignal(channel, { timeoutMs }).catch(() => ({ signal: void 0 }))).signal : void 0;
|
|
236578
|
+
const mapped = mapNetworkReadout({
|
|
236579
|
+
activeLink,
|
|
236580
|
+
wifiSignal,
|
|
236581
|
+
ssid: askWireless && wifiSignal !== void 0 ? (await api.getWifi(channel, { timeoutMs }).catch(() => ({ ssid: void 0 }))).ssid : void 0
|
|
236582
|
+
}, Date.now());
|
|
236583
|
+
this.ctx.logger.debug("network link read", {
|
|
236584
|
+
tags: { deviceId: this.id },
|
|
236585
|
+
meta: {
|
|
236586
|
+
reason,
|
|
236587
|
+
activeLink,
|
|
236588
|
+
rawSignal: wifiSignal,
|
|
236589
|
+
...mapped
|
|
236590
|
+
}
|
|
236591
|
+
});
|
|
236592
|
+
this.setCapSlice(networkLinkCapability, mapped);
|
|
236593
|
+
} catch (err) {
|
|
236594
|
+
this.ctx.logger.debug("network link read failed — keeping the last slice", {
|
|
236595
|
+
tags: { deviceId: this.id },
|
|
236596
|
+
meta: {
|
|
236597
|
+
reason,
|
|
236598
|
+
error: err instanceof Error ? err.message : String(err)
|
|
236599
|
+
}
|
|
236600
|
+
});
|
|
236601
|
+
}
|
|
236352
236602
|
}
|
|
236353
236603
|
/**
|
|
236354
236604
|
* Phase 5 (kernel-driven) — fired after `onProbe()` + accessory
|
|
@@ -236906,6 +237156,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
236906
237156
|
await (await this.ensureApi()).reboot(this.getChannel());
|
|
236907
237157
|
return { success: true };
|
|
236908
237158
|
} });
|
|
237159
|
+
this.registerNetworkLink();
|
|
236909
237160
|
this.ctx.registerNativeCap(motionCapability, { isDetected: async ({ deviceId }) => {
|
|
236910
237161
|
if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
236911
237162
|
return this.state.motion.detected ?? false;
|
|
@@ -237160,6 +237411,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
237160
237411
|
try {
|
|
237161
237412
|
const info = await api.getBatteryInfo(this.getChannel());
|
|
237162
237413
|
this.updateBatteryCache(info);
|
|
237414
|
+
await this.refreshNetworkLinkFromApi(api, "battery");
|
|
237163
237415
|
} catch (err) {
|
|
237164
237416
|
this.ctx.logger.debug("battery refresh failed", {
|
|
237165
237417
|
tags: { deviceId: this.id },
|
|
@@ -240697,6 +240949,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
240697
240949
|
/** Write granularity for `battery.lastContactAt` — see `markPassiveContact`.
|
|
240698
240950
|
* Bounds the commit rate this field can cost at 12/hour/device, and only
|
|
240699
240951
|
* for a device something is actually reaching. */
|
|
240952
|
+
/** Bound on each best-effort network read; three reads at most per refresh. */
|
|
240953
|
+
static NETWORK_LINK_READ_TIMEOUT_MS = 4e3;
|
|
240700
240954
|
static CONTACT_WRITE_QUANTUM_MS = 5 * 6e4;
|
|
240701
240955
|
/**
|
|
240702
240956
|
* Shared wake-transition handler invoked by both the simpleEvent
|
package/dist/addon.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import netImpl from "net";
|
|
|
21
21
|
import { fileURLToPath } from "url";
|
|
22
22
|
import { mkdir } from "fs/promises";
|
|
23
23
|
import os from "node:os";
|
|
24
|
-
//#region ../types/dist/event-category-
|
|
24
|
+
//#region ../types/dist/event-category-zAv7pMUz.mjs
|
|
25
25
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
26
26
|
EventCategory["SystemBoot"] = "system.boot";
|
|
27
27
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -393,6 +393,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
393
393
|
*/
|
|
394
394
|
EventCategory["BatteryOnStatusChanged"] = "battery.onStatusChanged";
|
|
395
395
|
/**
|
|
396
|
+
* Cap event fired by every device that registers the `network-link`
|
|
397
|
+
* capability. Mirrors the cap definition's `onStatusChanged`. Carries
|
|
398
|
+
* `{ deviceId, status: NetworkLinkStatus }` — a link switch or a signal
|
|
399
|
+
* reading that moved.
|
|
400
|
+
*/
|
|
401
|
+
EventCategory["NetworkLinkOnStatusChanged"] = "network-link.onStatusChanged";
|
|
402
|
+
/**
|
|
396
403
|
* Emitted by the battery cap provider WHEN `wakeForStream` enters the
|
|
397
404
|
* "wake in progress" window — between the Baichuan wake-up issue and
|
|
398
405
|
* the camera's first dialed-back RTP packet. The stream-broker
|
|
@@ -20029,6 +20036,33 @@ var RecentTracksQueryInput = object({
|
|
|
20029
20036
|
*/
|
|
20030
20037
|
classes: array(string()).optional()
|
|
20031
20038
|
});
|
|
20039
|
+
/**
|
|
20040
|
+
* A Summary (D359): the per-camera session envelope that references tracks.
|
|
20041
|
+
* `groupShotKey` is the media key of its group shot (`ownerKind: 'summary'`,
|
|
20042
|
+
* served by the event-media plane by key), null until the first frame with a
|
|
20043
|
+
* member visible has been materialised.
|
|
20044
|
+
*/
|
|
20045
|
+
var SummarySchema = object({
|
|
20046
|
+
id: string(),
|
|
20047
|
+
deviceId: number(),
|
|
20048
|
+
openedAt: number(),
|
|
20049
|
+
lastActiveAt: number(),
|
|
20050
|
+
sealedAt: number().nullable(),
|
|
20051
|
+
memberCount: number().int().nonnegative(),
|
|
20052
|
+
groupShotKey: string().nullable()
|
|
20053
|
+
});
|
|
20054
|
+
var SummariesQueryInput = object({
|
|
20055
|
+
deviceIds: array(number()).min(1),
|
|
20056
|
+
since: number().optional(),
|
|
20057
|
+
until: number().optional(),
|
|
20058
|
+
limit: number().int().min(1).max(200).default(50)
|
|
20059
|
+
});
|
|
20060
|
+
var SummariesPageSchema = object({ summaries: array(SummarySchema).readonly() });
|
|
20061
|
+
var SummaryDetailSchema = object({
|
|
20062
|
+
summary: SummarySchema,
|
|
20063
|
+
/** Member track ids, in absorption order. */
|
|
20064
|
+
trackIds: array(string()).readonly()
|
|
20065
|
+
});
|
|
20032
20066
|
var RecentTracksPageSchema = object({
|
|
20033
20067
|
/** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
|
|
20034
20068
|
tracks: array(TrackSchema).readonly(),
|
|
@@ -20429,7 +20463,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
20429
20463
|
* timeline would read as a camera that saw nothing.
|
|
20430
20464
|
*/
|
|
20431
20465
|
classes: array(string()).optional()
|
|
20432
|
-
}), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
|
|
20466
|
+
}), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(SummariesQueryInput, SummariesPageSchema), method(object({ id: string() }), SummaryDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
|
|
20433
20467
|
kind: "mutation",
|
|
20434
20468
|
auth: "admin"
|
|
20435
20469
|
}), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
|
|
@@ -24466,6 +24500,108 @@ onStatusChanged: { data: object({
|
|
|
24466
24500
|
volatileStateFields: ["lastUpdated"]
|
|
24467
24501
|
};
|
|
24468
24502
|
/**
|
|
24503
|
+
* Network-link snapshot. Same shape for every provider (a Reolink wifi
|
|
24504
|
+
* camera, a Home Assistant device with a signal-strength sensor, a Tapo
|
|
24505
|
+
* plug): one slice under `device.runtimeState['network-link']`, one badge,
|
|
24506
|
+
* one Home Assistant projection.
|
|
24507
|
+
*/
|
|
24508
|
+
var NetworkLinkStatusSchema = object({
|
|
24509
|
+
/** The link the device is on. `'unknown'` = not read yet, not "no link". */
|
|
24510
|
+
type: _enum([
|
|
24511
|
+
"wifi",
|
|
24512
|
+
"ethernet",
|
|
24513
|
+
"cellular",
|
|
24514
|
+
"unknown"
|
|
24515
|
+
]),
|
|
24516
|
+
/**
|
|
24517
|
+
* Link quality, 0..100 inclusive, normalised by the provider from whatever
|
|
24518
|
+
* the firmware reports (bars, RSSI, a vendor scale). **`null` means NOT
|
|
24519
|
+
* KNOWN or NOT APPLICABLE** — a wired link has no signal, and a wireless
|
|
24520
|
+
* one whose reading has not landed must not be drawn at 0 %. Consumers
|
|
24521
|
+
* SKIP a null rather than coerce it.
|
|
24522
|
+
*/
|
|
24523
|
+
signalPercent: number().min(0).max(100).nullable(),
|
|
24524
|
+
/** Raw received signal strength in dBm, when the firmware reports one. */
|
|
24525
|
+
rssiDbm: number().optional(),
|
|
24526
|
+
/** Network name of a wireless link, when the firmware reports it. */
|
|
24527
|
+
ssid: string().optional(),
|
|
24528
|
+
/** Ms epoch of the last observation. Lets consumers reason about freshness. */
|
|
24529
|
+
lastUpdated: number()
|
|
24530
|
+
});
|
|
24531
|
+
/** The slice a provider seeds before its first read: nothing is known yet. */
|
|
24532
|
+
var NETWORK_LINK_UNKNOWN = {
|
|
24533
|
+
type: "unknown",
|
|
24534
|
+
signalPercent: null,
|
|
24535
|
+
lastUpdated: 0
|
|
24536
|
+
};
|
|
24537
|
+
/**
|
|
24538
|
+
* Normalise a signal the firmware reports as BARS (0..`maxBars`) to 0..100.
|
|
24539
|
+
* Out-of-range or non-finite input is not a reading: `null`.
|
|
24540
|
+
*/
|
|
24541
|
+
function signalPercentFromBars(bars, maxBars) {
|
|
24542
|
+
if (bars === void 0 || !Number.isFinite(bars) || maxBars <= 0) return null;
|
|
24543
|
+
if (bars < 0 || bars > maxBars) return null;
|
|
24544
|
+
return Math.round(bars / maxBars * 100);
|
|
24545
|
+
}
|
|
24546
|
+
/**
|
|
24547
|
+
* Normalise an RSSI in dBm to 0..100 on the usual wifi scale: -100 dBm and
|
|
24548
|
+
* below is 0 %, -50 dBm and above is 100 %, linear between. Non-finite or
|
|
24549
|
+
* positive input is not an RSSI: `null`.
|
|
24550
|
+
*/
|
|
24551
|
+
function signalPercentFromRssi(rssiDbm) {
|
|
24552
|
+
if (rssiDbm === void 0 || !Number.isFinite(rssiDbm) || rssiDbm > 0) return null;
|
|
24553
|
+
return Math.round((Math.min(-50, Math.max(-100, rssiDbm)) + 100) * 2);
|
|
24554
|
+
}
|
|
24555
|
+
var networkLinkCapability = {
|
|
24556
|
+
name: "network-link",
|
|
24557
|
+
scope: "device",
|
|
24558
|
+
deviceNative: true,
|
|
24559
|
+
mode: "singleton",
|
|
24560
|
+
deviceTypes: [
|
|
24561
|
+
DeviceType.Camera,
|
|
24562
|
+
DeviceType.Sensor,
|
|
24563
|
+
DeviceType.Button,
|
|
24564
|
+
DeviceType.Switch,
|
|
24565
|
+
DeviceType.Light,
|
|
24566
|
+
DeviceType.Lock,
|
|
24567
|
+
DeviceType.Siren
|
|
24568
|
+
],
|
|
24569
|
+
methods: {},
|
|
24570
|
+
events: {
|
|
24571
|
+
/**
|
|
24572
|
+
* Emitted whenever the cached status changes (a link switch, a signal
|
|
24573
|
+
* reading that moved). Mirrored on the parent chain by the
|
|
24574
|
+
* DeviceEventPropagator like `battery.onStatusChanged`.
|
|
24575
|
+
*/
|
|
24576
|
+
onStatusChanged: { data: object({
|
|
24577
|
+
deviceId: number(),
|
|
24578
|
+
status: NetworkLinkStatusSchema
|
|
24579
|
+
}) } },
|
|
24580
|
+
status: {
|
|
24581
|
+
schema: NetworkLinkStatusSchema,
|
|
24582
|
+
kind: "push",
|
|
24583
|
+
empty: NETWORK_LINK_UNKNOWN
|
|
24584
|
+
},
|
|
24585
|
+
/**
|
|
24586
|
+
* Runtime-state slice — every provider stores the same shape under
|
|
24587
|
+
* `device.runtimeState['network-link']`, read once by the badge and the
|
|
24588
|
+
* Home Assistant projector regardless of the driver.
|
|
24589
|
+
*/
|
|
24590
|
+
runtimeState: NetworkLinkStatusSchema,
|
|
24591
|
+
/**
|
|
24592
|
+
* Runtime-state durability: **restored** — a link reading is slow to
|
|
24593
|
+
* change and a sleeping battery camera may not report for hours; the
|
|
24594
|
+
* restored slice is what the badge shows until the next read.
|
|
24595
|
+
*
|
|
24596
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
24597
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
24598
|
+
*/
|
|
24599
|
+
durability: "restored",
|
|
24600
|
+
/** Clock fields: written, but excluded from the compare that decides
|
|
24601
|
+
* whether persisting is worth a SQLite commit. */
|
|
24602
|
+
volatileStateFields: ["lastUpdated"]
|
|
24603
|
+
};
|
|
24604
|
+
/**
|
|
24469
24605
|
* Generic boolean sensor — last-resort fallback when no domain-
|
|
24470
24606
|
* specific binary cap fits (Home Assistant `binary_sensor` without a
|
|
24471
24607
|
* known `device_class`, or a domain we haven't typed yet). Pure
|
|
@@ -33150,6 +33286,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
|
|
|
33150
33286
|
motionTrigger: motionTriggerCapability,
|
|
33151
33287
|
motionZones: motionZonesCapability,
|
|
33152
33288
|
nativeObjectDetection: nativeObjectDetectionCapability,
|
|
33289
|
+
networkLink: networkLinkCapability,
|
|
33153
33290
|
notifier: notifierCapability,
|
|
33154
33291
|
numericSensor: numericSensorCapability,
|
|
33155
33292
|
petFeeder: petFeederCapability,
|
|
@@ -37730,6 +37867,12 @@ Object.freeze({
|
|
|
37730
37867
|
addonId: null,
|
|
37731
37868
|
access: "view"
|
|
37732
37869
|
},
|
|
37870
|
+
"pipelineAnalytics.getSummary": {
|
|
37871
|
+
capName: "pipeline-analytics",
|
|
37872
|
+
capScope: "device",
|
|
37873
|
+
addonId: null,
|
|
37874
|
+
access: "view"
|
|
37875
|
+
},
|
|
37733
37876
|
"pipelineAnalytics.getTrack": {
|
|
37734
37877
|
capName: "pipeline-analytics",
|
|
37735
37878
|
capScope: "device",
|
|
@@ -37808,6 +37951,12 @@ Object.freeze({
|
|
|
37808
37951
|
addonId: null,
|
|
37809
37952
|
access: "view"
|
|
37810
37953
|
},
|
|
37954
|
+
"pipelineAnalytics.listSummaries": {
|
|
37955
|
+
capName: "pipeline-analytics",
|
|
37956
|
+
capScope: "device",
|
|
37957
|
+
addonId: null,
|
|
37958
|
+
access: "view"
|
|
37959
|
+
},
|
|
37811
37960
|
"pipelineAnalytics.listTrackMedia": {
|
|
37812
37961
|
capName: "pipeline-analytics",
|
|
37813
37962
|
capScope: "device",
|
|
@@ -41503,6 +41652,11 @@ Object.freeze({
|
|
|
41503
41652
|
form: "array",
|
|
41504
41653
|
optional: true
|
|
41505
41654
|
}],
|
|
41655
|
+
"pipelineAnalytics.listSummaries": [{
|
|
41656
|
+
name: "deviceIds",
|
|
41657
|
+
form: "array",
|
|
41658
|
+
optional: false
|
|
41659
|
+
}],
|
|
41506
41660
|
"pipelineAnalytics.listTrackMedia": [{
|
|
41507
41661
|
name: "deviceId",
|
|
41508
41662
|
form: "single",
|
|
@@ -233530,6 +233684,50 @@ async function populateReolinkMetadata(api, channel, target) {
|
|
|
233530
233684
|
});
|
|
233531
233685
|
}
|
|
233532
233686
|
}
|
|
233687
|
+
/**
|
|
233688
|
+
* The link the label names. A label the firmware did not give is `unknown`,
|
|
233689
|
+
* UNLESS a wifi signal was read — a camera that answers a wifi signal is on
|
|
233690
|
+
* wifi whatever its label says.
|
|
233691
|
+
*/
|
|
233692
|
+
function linkTypeOf(readout) {
|
|
233693
|
+
const label = (readout.activeLink ?? "").toLowerCase();
|
|
233694
|
+
if (label.includes("wifi") || label.includes("wlan") || label.includes("wireless")) return "wifi";
|
|
233695
|
+
if (label.includes("lan") || label.includes("eth") || label.includes("wire")) return "ethernet";
|
|
233696
|
+
if (/\b(4g|5g|lte|cell|sim)\b/.test(label)) return "cellular";
|
|
233697
|
+
if (readout.wifiSignal !== void 0) return "wifi";
|
|
233698
|
+
return "unknown";
|
|
233699
|
+
}
|
|
233700
|
+
/** True when the label names a wireless link — the only case worth a signal read. */
|
|
233701
|
+
function isWirelessLabel(activeLink) {
|
|
233702
|
+
const type = linkTypeOf({
|
|
233703
|
+
activeLink,
|
|
233704
|
+
wifiSignal: void 0
|
|
233705
|
+
});
|
|
233706
|
+
return type === "wifi" || type === "cellular";
|
|
233707
|
+
}
|
|
233708
|
+
/** 0..4 → bars; 5..100 → percent; negative → dBm; anything else → not a reading. */
|
|
233709
|
+
function signalPercentOf(raw) {
|
|
233710
|
+
if (raw === void 0 || !Number.isFinite(raw)) return null;
|
|
233711
|
+
if (raw < 0) return signalPercentFromRssi(raw);
|
|
233712
|
+
if (raw <= 4) return signalPercentFromBars(raw, 4);
|
|
233713
|
+
if (raw <= 100) return Math.round(raw);
|
|
233714
|
+
return null;
|
|
233715
|
+
}
|
|
233716
|
+
function mapNetworkReadout(readout, now) {
|
|
233717
|
+
const type = linkTypeOf(readout);
|
|
233718
|
+
const raw = readout.wifiSignal;
|
|
233719
|
+
const wireless = type === "wifi" || type === "cellular";
|
|
233720
|
+
const signalPercent = wireless ? signalPercentOf(raw) : null;
|
|
233721
|
+
const rssiDbm = wireless && raw !== void 0 && Number.isFinite(raw) && raw < 0 ? raw : void 0;
|
|
233722
|
+
const ssid = wireless && readout.ssid !== void 0 && readout.ssid !== "" ? readout.ssid : void 0;
|
|
233723
|
+
return {
|
|
233724
|
+
type,
|
|
233725
|
+
signalPercent,
|
|
233726
|
+
...rssiDbm !== void 0 ? { rssiDbm } : {},
|
|
233727
|
+
...ssid !== void 0 ? { ssid } : {},
|
|
233728
|
+
lastUpdated: now
|
|
233729
|
+
};
|
|
233730
|
+
}
|
|
233533
233731
|
//#endregion
|
|
233534
233732
|
//#region src/error-classifier.ts
|
|
233535
233733
|
/**
|
|
@@ -236329,6 +236527,58 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
236329
236527
|
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
236330
236528
|
});
|
|
236331
236529
|
});
|
|
236530
|
+
await this.refreshNetworkLinkFromApi(api, "probe");
|
|
236531
|
+
}
|
|
236532
|
+
/**
|
|
236533
|
+
* Register the `network-link` cap: the read path serves the slice, the
|
|
236534
|
+
* writers are {@link refreshNetworkLinkFromApi}. Seeded unknown so the
|
|
236535
|
+
* restored slice (if any) stays valid and a fresh device draws no link.
|
|
236536
|
+
*/
|
|
236537
|
+
registerNetworkLink() {
|
|
236538
|
+
this.ctx.registerNativeCap(networkLinkCapability, { getStatus: async ({ deviceId }) => {
|
|
236539
|
+
if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
236540
|
+
return this.state.networkLink;
|
|
236541
|
+
} });
|
|
236542
|
+
if (this.getCapSlice(networkLinkCapability) === null) this.setCapSlice(networkLinkCapability, NETWORK_LINK_UNKNOWN);
|
|
236543
|
+
}
|
|
236544
|
+
/**
|
|
236545
|
+
* Read the active link and, on a wireless one, its signal and network name
|
|
236546
|
+
* over a socket that is ALREADY up. Called post-login and after a
|
|
236547
|
+
* successful battery read — never on its own timer, so a sleeping battery
|
|
236548
|
+
* camera is never woken for a bar count. Each read is best-effort with its
|
|
236549
|
+
* own bound; a failure keeps the last slice and says so at debug level.
|
|
236550
|
+
*/
|
|
236551
|
+
async refreshNetworkLinkFromApi(api, reason) {
|
|
236552
|
+
const channel = this.getChannel();
|
|
236553
|
+
const timeoutMs = ReolinkCamera.NETWORK_LINK_READ_TIMEOUT_MS;
|
|
236554
|
+
try {
|
|
236555
|
+
const activeLink = (await api.getNetworkInfo(channel, { timeoutMs }))?.activeLink;
|
|
236556
|
+
const askWireless = activeLink === void 0 || isWirelessLabel(activeLink);
|
|
236557
|
+
const wifiSignal = askWireless ? (await api.getWifiSignal(channel, { timeoutMs }).catch(() => ({ signal: void 0 }))).signal : void 0;
|
|
236558
|
+
const mapped = mapNetworkReadout({
|
|
236559
|
+
activeLink,
|
|
236560
|
+
wifiSignal,
|
|
236561
|
+
ssid: askWireless && wifiSignal !== void 0 ? (await api.getWifi(channel, { timeoutMs }).catch(() => ({ ssid: void 0 }))).ssid : void 0
|
|
236562
|
+
}, Date.now());
|
|
236563
|
+
this.ctx.logger.debug("network link read", {
|
|
236564
|
+
tags: { deviceId: this.id },
|
|
236565
|
+
meta: {
|
|
236566
|
+
reason,
|
|
236567
|
+
activeLink,
|
|
236568
|
+
rawSignal: wifiSignal,
|
|
236569
|
+
...mapped
|
|
236570
|
+
}
|
|
236571
|
+
});
|
|
236572
|
+
this.setCapSlice(networkLinkCapability, mapped);
|
|
236573
|
+
} catch (err) {
|
|
236574
|
+
this.ctx.logger.debug("network link read failed — keeping the last slice", {
|
|
236575
|
+
tags: { deviceId: this.id },
|
|
236576
|
+
meta: {
|
|
236577
|
+
reason,
|
|
236578
|
+
error: err instanceof Error ? err.message : String(err)
|
|
236579
|
+
}
|
|
236580
|
+
});
|
|
236581
|
+
}
|
|
236332
236582
|
}
|
|
236333
236583
|
/**
|
|
236334
236584
|
* Phase 5 (kernel-driven) — fired after `onProbe()` + accessory
|
|
@@ -236886,6 +237136,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
236886
237136
|
await (await this.ensureApi()).reboot(this.getChannel());
|
|
236887
237137
|
return { success: true };
|
|
236888
237138
|
} });
|
|
237139
|
+
this.registerNetworkLink();
|
|
236889
237140
|
this.ctx.registerNativeCap(motionCapability, { isDetected: async ({ deviceId }) => {
|
|
236890
237141
|
if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
236891
237142
|
return this.state.motion.detected ?? false;
|
|
@@ -237140,6 +237391,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
237140
237391
|
try {
|
|
237141
237392
|
const info = await api.getBatteryInfo(this.getChannel());
|
|
237142
237393
|
this.updateBatteryCache(info);
|
|
237394
|
+
await this.refreshNetworkLinkFromApi(api, "battery");
|
|
237143
237395
|
} catch (err) {
|
|
237144
237396
|
this.ctx.logger.debug("battery refresh failed", {
|
|
237145
237397
|
tags: { deviceId: this.id },
|
|
@@ -240677,6 +240929,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
240677
240929
|
/** Write granularity for `battery.lastContactAt` — see `markPassiveContact`.
|
|
240678
240930
|
* Bounds the commit rate this field can cost at 12/hour/device, and only
|
|
240679
240931
|
* for a device something is actually reaching. */
|
|
240932
|
+
/** Bound on each best-effort network read; three reads at most per refresh. */
|
|
240933
|
+
static NETWORK_LINK_READ_TIMEOUT_MS = 4e3;
|
|
240680
240934
|
static CONTACT_WRITE_QUANTUM_MS = 5 * 6e4;
|
|
240681
240935
|
/**
|
|
240682
240936
|
* Shared wake-transition handler invoked by both the simpleEvent
|