@camstack/addon-provider-reolink 1.2.83 → 1.2.85
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 +276 -8
- package/dist/addon.mjs +276 -8
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -13361,7 +13361,26 @@ var DiscoveryCandidateSchema = object({
|
|
|
13361
13361
|
* identity ahead of adoption. Rendering metadata (unit, precision)
|
|
13362
13362
|
* flows live through the cap STATUS SLICE after adoption.
|
|
13363
13363
|
*/
|
|
13364
|
-
sourceInfo: SourceInfoSchema.optional()
|
|
13364
|
+
sourceInfo: SourceInfoSchema.optional(),
|
|
13365
|
+
/**
|
|
13366
|
+
* Set when this candidate is a device the provider ALREADY owns.
|
|
13367
|
+
*
|
|
13368
|
+
* A scan cannot generally produce the identity a device was onboarded under
|
|
13369
|
+
* (Reolink keys on `mac-<mac>`, learned at adopt time), so a stableId
|
|
13370
|
+
* comparison never matches and an owned device looks addable. Re-adopting one
|
|
13371
|
+
* overwrites its config with scan-derived values — that is how a Home Hub's
|
|
13372
|
+
* Baichuan port was overwritten with its ONVIF port, taking the hub and its
|
|
13373
|
+
* three child cameras offline for four hours.
|
|
13374
|
+
*
|
|
13375
|
+
* A provider that can recognise its own devices says so here. Absent means
|
|
13376
|
+
* "not recognised", which is not the same as "known to be new" — a provider
|
|
13377
|
+
* that cannot tell simply never sets it.
|
|
13378
|
+
*/
|
|
13379
|
+
alreadyOnboarded: boolean().optional(),
|
|
13380
|
+
/** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
|
|
13381
|
+
onboardedDeviceId: number().optional(),
|
|
13382
|
+
/** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
|
|
13383
|
+
onboardedName: string().optional()
|
|
13365
13384
|
});
|
|
13366
13385
|
/**
|
|
13367
13386
|
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
@@ -34043,6 +34062,22 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
34043
34062
|
* failed — logged at ERROR with `tags.deviceId` and surfaced via
|
|
34044
34063
|
* `getStatus().error`.
|
|
34045
34064
|
*/
|
|
34065
|
+
/**
|
|
34066
|
+
* Repair a row's PERSISTED config blob immediately before it is restored.
|
|
34067
|
+
* Default: no-op — most providers have nothing to heal.
|
|
34068
|
+
*
|
|
34069
|
+
* This exists because a restored device self-hydrates from the DB: `create()`
|
|
34070
|
+
* passes `{}` and `BaseDevice` parses the stored blob against the device
|
|
34071
|
+
* schema. A blob that lost a REQUIRED field therefore fails restore forever,
|
|
34072
|
+
* and no later pass revisits it — a hub-adopted Reolink camera whose blob had
|
|
34073
|
+
* been emptied failed all four bounded attempts against fields
|
|
34074
|
+
* (`host`, `password`) it inherits from its parent and never dials itself.
|
|
34075
|
+
*
|
|
34076
|
+
* Implementations get every saved row, so a child can read its parent's blob.
|
|
34077
|
+
* A heal that throws is treated like any other restore failure: retried under
|
|
34078
|
+
* the bound, then reported — never swallowed.
|
|
34079
|
+
*/
|
|
34080
|
+
async healSavedConfig(_saved, _allSaved) {}
|
|
34046
34081
|
async onRestoreDevices(savedDevices) {
|
|
34047
34082
|
const restored = /* @__PURE__ */ new Set();
|
|
34048
34083
|
const failures = [];
|
|
@@ -34051,6 +34086,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
34051
34086
|
const Class = this.deviceClasses[saved.type];
|
|
34052
34087
|
if (!Class) throw new Error(`no device class registered for type "${saved.type}"`);
|
|
34053
34088
|
if (saved.parentDeviceId !== null && !restored.has(saved.parentDeviceId)) throw new Error(`parent device ${saved.parentDeviceId} not restored`);
|
|
34089
|
+
await this.healSavedConfig(saved, savedDevices);
|
|
34054
34090
|
await this.ctx.kernel.devices.create(saved.stableId, Class, {}, saved.parentDeviceId);
|
|
34055
34091
|
restored.add(saved.id);
|
|
34056
34092
|
};
|
|
@@ -242593,6 +242629,43 @@ function computeHealthCheckBackoffMs(consecutive) {
|
|
|
242593
242629
|
return 15 * 6e4;
|
|
242594
242630
|
}
|
|
242595
242631
|
//#endregion
|
|
242632
|
+
//#region src/hub-channel-reconcile.ts
|
|
242633
|
+
/**
|
|
242634
|
+
* Match adopted children to the hub's own channel list by IDENTITY, then report the
|
|
242635
|
+
* ones whose persisted `channel` is missing or wrong.
|
|
242636
|
+
*
|
|
242637
|
+
* A child's stableId is `${hubStableId}-${childNativeId}`, and `childNativeId` is built
|
|
242638
|
+
* from the camera's UID — so identity survives a channel move, while `channel` does not.
|
|
242639
|
+
* The hub is the authority on which slot a UID currently occupies.
|
|
242640
|
+
*
|
|
242641
|
+
* Why this exists: `loadAdoptedChildrenByChannel` keys the adopted set on the persisted
|
|
242642
|
+
* `channel` alone. A child that lost it is absent from that map, so the discovery panel
|
|
242643
|
+
* renders it `alreadyAdopted: false` — one click away from being adopted a SECOND time —
|
|
242644
|
+
* and `routeSimpleEvent` silently drops every push for its slot. Observed live on
|
|
242645
|
+
* 2026-09-04 with an Argus MagiCam that was actually on channel 3.
|
|
242646
|
+
*
|
|
242647
|
+
* Pure: no I/O. A discovered entry with no channel, or with no adopted child, is skipped —
|
|
242648
|
+
* this repairs what exists, it never adopts.
|
|
242649
|
+
*/
|
|
242650
|
+
function planChannelReconciliation(hubStableId, discovered, children) {
|
|
242651
|
+
const byStableId = /* @__PURE__ */ new Map();
|
|
242652
|
+
for (const child of children) byStableId.set(child.stableId, child);
|
|
242653
|
+
const repairs = [];
|
|
242654
|
+
for (const entry of discovered) {
|
|
242655
|
+
if (typeof entry.rtspChannel !== "number") continue;
|
|
242656
|
+
const child = byStableId.get(`${hubStableId}-${entry.childNativeId}`);
|
|
242657
|
+
if (child === void 0) continue;
|
|
242658
|
+
if (child.channel === entry.rtspChannel) continue;
|
|
242659
|
+
repairs.push({
|
|
242660
|
+
deviceId: child.deviceId,
|
|
242661
|
+
stableId: child.stableId,
|
|
242662
|
+
from: child.channel ?? null,
|
|
242663
|
+
to: entry.rtspChannel
|
|
242664
|
+
});
|
|
242665
|
+
}
|
|
242666
|
+
return repairs;
|
|
242667
|
+
}
|
|
242668
|
+
//#endregion
|
|
242596
242669
|
//#region src/simple-event-dispatch-trace.ts
|
|
242597
242670
|
/**
|
|
242598
242671
|
* Gate for hub-side simpleEvent traces.
|
|
@@ -243089,6 +243162,7 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
243089
243162
|
lastFetchedAt: Date.now()
|
|
243090
243163
|
};
|
|
243091
243164
|
this.runtimeState.setCapState(deviceDiscoveryCapability.name, slice);
|
|
243165
|
+
if (lastError === null) await this.reconcileChildChannels(discovered);
|
|
243092
243166
|
if (lastError === null) {
|
|
243093
243167
|
this.knownUnadoptedChannels.clear();
|
|
243094
243168
|
for (const d of discovered) {
|
|
@@ -243261,6 +243335,64 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
243261
243335
|
* the channel index. Returns a `channel → kernel deviceId` map.
|
|
243262
243336
|
* Used to gate the `alreadyAdopted` flag in the discovery list.
|
|
243263
243337
|
*/
|
|
243338
|
+
/**
|
|
243339
|
+
* Re-derive each adopted child's `channel` from the hub's own channel list, matching by
|
|
243340
|
+
* IDENTITY (stableId carries the camera UID) rather than by the channel itself.
|
|
243341
|
+
*
|
|
243342
|
+
* `loadAdoptedChildrenByChannel` keys the adopted set on the persisted `channel` alone,
|
|
243343
|
+
* so a child that lost it is invisible: the discovery panel renders it available (one
|
|
243344
|
+
* click from a second adoption of the same camera) and `routeSimpleEvent` drops every
|
|
243345
|
+
* push for its slot. A channel move produces the same damage under a wrong number.
|
|
243346
|
+
*
|
|
243347
|
+
* Writes through the live device so its in-memory config and the DB stay in step.
|
|
243348
|
+
* A child that is not live is skipped — it is repaired on its next successful restore.
|
|
243349
|
+
*/
|
|
243350
|
+
async reconcileChildChannels(discovered) {
|
|
243351
|
+
const children = await this.ctx.devices.getChildren(this.id);
|
|
243352
|
+
const adopted = children.map((child) => {
|
|
243353
|
+
const channel = child instanceof ReolinkCamera ? child.config.values.channel : void 0;
|
|
243354
|
+
return {
|
|
243355
|
+
deviceId: child.id,
|
|
243356
|
+
stableId: child.stableId,
|
|
243357
|
+
...typeof channel === "number" ? { channel } : {}
|
|
243358
|
+
};
|
|
243359
|
+
});
|
|
243360
|
+
const repairs = planChannelReconciliation(this.stableId, discovered.map((d) => ({
|
|
243361
|
+
childNativeId: d.childNativeId,
|
|
243362
|
+
...typeof d.metadata.rtspChannel === "number" ? { rtspChannel: d.metadata.rtspChannel } : {}
|
|
243363
|
+
})), adopted);
|
|
243364
|
+
if (repairs.length === 0) return;
|
|
243365
|
+
for (const repair of repairs) {
|
|
243366
|
+
const child = children.find((c) => c.id === repair.deviceId);
|
|
243367
|
+
if (!(child instanceof ReolinkCamera)) continue;
|
|
243368
|
+
try {
|
|
243369
|
+
await child.config.setAll({ channel: repair.to });
|
|
243370
|
+
this.channelToDeviceId.set(repair.to, repair.deviceId);
|
|
243371
|
+
this.ctx.logger.warn("Reolink Hub: repaired a child channel from the hub channel list", {
|
|
243372
|
+
tags: {
|
|
243373
|
+
deviceId: repair.deviceId,
|
|
243374
|
+
stableId: repair.stableId
|
|
243375
|
+
},
|
|
243376
|
+
meta: {
|
|
243377
|
+
from: repair.from,
|
|
243378
|
+
to: repair.to
|
|
243379
|
+
}
|
|
243380
|
+
});
|
|
243381
|
+
} catch (err) {
|
|
243382
|
+
this.ctx.logger.warn("Reolink Hub: child channel repair failed", {
|
|
243383
|
+
tags: {
|
|
243384
|
+
deviceId: repair.deviceId,
|
|
243385
|
+
stableId: repair.stableId
|
|
243386
|
+
},
|
|
243387
|
+
meta: {
|
|
243388
|
+
from: repair.from,
|
|
243389
|
+
to: repair.to,
|
|
243390
|
+
error: err instanceof Error ? err.message : String(err)
|
|
243391
|
+
}
|
|
243392
|
+
});
|
|
243393
|
+
}
|
|
243394
|
+
}
|
|
243395
|
+
}
|
|
243264
243396
|
async loadAdoptedChildrenByChannel() {
|
|
243265
243397
|
const result = /* @__PURE__ */ new Map();
|
|
243266
243398
|
const children = await this.ctx.devices.getChildren(this.id);
|
|
@@ -243725,6 +243857,53 @@ function buildCreationFormSchema() {
|
|
|
243725
243857
|
] };
|
|
243726
243858
|
}
|
|
243727
243859
|
//#endregion
|
|
243860
|
+
//#region src/child-config-heal.ts
|
|
243861
|
+
/** Connection fields a hub-adopted child inherits from its parent at adoption time. */
|
|
243862
|
+
var INHERITED_CONNECTION_KEYS = [
|
|
243863
|
+
"host",
|
|
243864
|
+
"port",
|
|
243865
|
+
"username",
|
|
243866
|
+
"password",
|
|
243867
|
+
"transport"
|
|
243868
|
+
];
|
|
243869
|
+
/** A stored value counts as present only when it is a usable string/number. */
|
|
243870
|
+
function isPresent(value) {
|
|
243871
|
+
if (typeof value === "string") return value !== "";
|
|
243872
|
+
return typeof value === "number";
|
|
243873
|
+
}
|
|
243874
|
+
/**
|
|
243875
|
+
* Work out what a hub-adopted child's persisted blob is missing from the connection
|
|
243876
|
+
* fields it inherited from its parent when it was adopted.
|
|
243877
|
+
*
|
|
243878
|
+
* `ReolinkCamera` with a parent uses `parent.api` and never dials its own login — but
|
|
243879
|
+
* `reolinkCameraSchema` still requires `host` and `password` as strings, so a blob that
|
|
243880
|
+
* lost them fails to parse and the camera cannot be restored at all. Observed live on
|
|
243881
|
+
* 2026-09-04: an Argus MagiCam under a Home Hub carried `{}` and burned all four bounded
|
|
243882
|
+
* restore attempts on `host`/`password` — fields it would never have used.
|
|
243883
|
+
*
|
|
243884
|
+
* Only ABSENT fields are filled. A child that deliberately differs from its parent (a
|
|
243885
|
+
* per-child credential, a non-default port) keeps what it has.
|
|
243886
|
+
*/
|
|
243887
|
+
function planChildConfigHeal(childConfig, parentConfig) {
|
|
243888
|
+
const patch = {};
|
|
243889
|
+
const missingKeys = [];
|
|
243890
|
+
for (const key of INHERITED_CONNECTION_KEYS) {
|
|
243891
|
+
if (isPresent(childConfig[key])) continue;
|
|
243892
|
+
if (!isPresent(parentConfig[key])) continue;
|
|
243893
|
+
patch[key] = parentConfig[key];
|
|
243894
|
+
missingKeys.push(key);
|
|
243895
|
+
}
|
|
243896
|
+
return {
|
|
243897
|
+
patch,
|
|
243898
|
+
missingKeys
|
|
243899
|
+
};
|
|
243900
|
+
}
|
|
243901
|
+
/** Find the saved row a child names as its parent. */
|
|
243902
|
+
function findParentRow(child, allSaved) {
|
|
243903
|
+
if (child.parentDeviceId === null) return void 0;
|
|
243904
|
+
return allSaved.find((row) => row.id === child.parentDeviceId);
|
|
243905
|
+
}
|
|
243906
|
+
//#endregion
|
|
243728
243907
|
//#region src/reolink-discovery-map.ts
|
|
243729
243908
|
/**
|
|
243730
243909
|
* Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
|
|
@@ -243734,21 +243913,44 @@ function slugifyReolinkHost(host) {
|
|
|
243734
243913
|
return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
243735
243914
|
}
|
|
243736
243915
|
/**
|
|
243916
|
+
* Decide whether a discovered host is a device this provider already owns.
|
|
243917
|
+
*
|
|
243918
|
+
* Matching is by UID first (stable across a DHCP lease change) and by host second. A UID match
|
|
243919
|
+
* wins even when the host differs, because the address is the thing that moves.
|
|
243920
|
+
*/
|
|
243921
|
+
function findOnboardedMatch(discovered, onboarded) {
|
|
243922
|
+
if (discovered.uid !== void 0 && discovered.uid !== "") {
|
|
243923
|
+
const byUid = onboarded.find((o) => o.uid === discovered.uid);
|
|
243924
|
+
if (byUid !== void 0) return byUid;
|
|
243925
|
+
}
|
|
243926
|
+
return onboarded.find((o) => o.host !== void 0 && o.host === discovered.host);
|
|
243927
|
+
}
|
|
243928
|
+
/**
|
|
243737
243929
|
* Map discovered Reolink hosts to adoption {@link DiscoveryCandidate}s, de-duplicated by host (the
|
|
243738
243930
|
* same camera can answer on more than one discovery method — UDP broadcast, ONVIF, HTTP scan). The
|
|
243739
243931
|
* first responder for a host wins. Pure: no I/O.
|
|
243740
243932
|
*
|
|
243933
|
+
* **The scan never supplies `port`.** `port` in the Reolink config schema is the BAICHUAN port
|
|
243934
|
+
* (default 9000). Discovery reports `httpPort`, the HTTP/ONVIF listener — on a Reolink Home Hub
|
|
243935
|
+
* that is 8000, while Baichuan lives on `mediaPort` 9000. Writing the discovered value into `port`
|
|
243936
|
+
* pointed every Baichuan login at the ONVIF listener, which accepts the TCP connection and then
|
|
243937
|
+
* closes it: `Baichuan socket closed`, 316 times in 12 hours, taking the hub and all three of its
|
|
243938
|
+
* child cameras offline. The default is correct; a non-default Baichuan port is an operator edit,
|
|
243939
|
+
* never a scan result.
|
|
243940
|
+
*
|
|
243741
243941
|
* The authoritative stableId is `mac-<mac>` (learned during autodetect at adopt time), which discovery
|
|
243742
|
-
* can't produce — so
|
|
243743
|
-
*
|
|
243942
|
+
* can't produce — so candidates carry `alreadyOnboarded` rather than relying on a stableId match,
|
|
243943
|
+
* which never fires for a MAC-keyed device. Re-adding an owned device is how the port got clobbered
|
|
243944
|
+
* in the first place, so a scan says so instead of offering it again.
|
|
243744
243945
|
*/
|
|
243745
|
-
function mapReolinkDiscoveryToCandidates(devices, credentials) {
|
|
243946
|
+
function mapReolinkDiscoveryToCandidates(devices, credentials, onboarded = []) {
|
|
243746
243947
|
const username = credentials.username?.trim() ?? "";
|
|
243747
243948
|
const password = credentials.password ?? "";
|
|
243748
243949
|
const byHost = /* @__PURE__ */ new Map();
|
|
243749
243950
|
for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
|
|
243750
243951
|
return [...byHost.values()].map((d) => {
|
|
243751
243952
|
const displayName = d.name ?? d.model ?? d.host;
|
|
243953
|
+
const match = findOnboardedMatch(d, onboarded);
|
|
243752
243954
|
return {
|
|
243753
243955
|
stableId: `host-${slugifyReolinkHost(d.host)}`,
|
|
243754
243956
|
type: DeviceType.Camera,
|
|
@@ -243757,11 +243959,15 @@ function mapReolinkDiscoveryToCandidates(devices, credentials) {
|
|
|
243757
243959
|
name: displayName,
|
|
243758
243960
|
host: d.host,
|
|
243759
243961
|
transport: "auto",
|
|
243760
|
-
...d.httpPort !== void 0 ? { port: d.httpPort } : {},
|
|
243761
243962
|
...d.uid ? { uid: d.uid } : {},
|
|
243762
243963
|
...username ? { username } : {},
|
|
243763
243964
|
...password ? { password } : {}
|
|
243764
|
-
}
|
|
243965
|
+
},
|
|
243966
|
+
...match !== void 0 ? {
|
|
243967
|
+
alreadyOnboarded: true,
|
|
243968
|
+
onboardedDeviceId: match.deviceId,
|
|
243969
|
+
onboardedName: match.name
|
|
243970
|
+
} : {}
|
|
243765
243971
|
};
|
|
243766
243972
|
});
|
|
243767
243973
|
}
|
|
@@ -244410,6 +244616,61 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
244410
244616
|
super({});
|
|
244411
244617
|
}
|
|
244412
244618
|
/**
|
|
244619
|
+
* Reduce this addon's live devices to the identities a network scan can produce, so
|
|
244620
|
+
* {@link mapReolinkDiscoveryToCandidates} can recognise a device it already owns.
|
|
244621
|
+
*
|
|
244622
|
+
* Hubs are included: the incident this exists for was a Home Hub re-offered by a scan and
|
|
244623
|
+
* re-added, which overwrote its Baichuan port with the ONVIF port the scan reported.
|
|
244624
|
+
*/
|
|
244625
|
+
/**
|
|
244626
|
+
* Re-inherit the parent's connection fields into a hub-adopted child whose persisted
|
|
244627
|
+
* blob lost them, so the child can be restored at all.
|
|
244628
|
+
*
|
|
244629
|
+
* `adoptDiscoveredChild` copies `host`/`port`/`username`/`password`/`transport` down
|
|
244630
|
+
* from the Hub at adoption. Nothing re-applied them afterwards, so a child whose blob
|
|
244631
|
+
* was emptied failed `reolinkCameraSchema` on every restore — on fields a child never
|
|
244632
|
+
* dials, because a child with a parent uses `parent.api`. Only ABSENT fields are
|
|
244633
|
+
* filled; a child that deliberately differs keeps what it has.
|
|
244634
|
+
*/
|
|
244635
|
+
async healSavedConfig(saved, allSaved) {
|
|
244636
|
+
const parent = findParentRow(saved, allSaved);
|
|
244637
|
+
if (parent === void 0) return;
|
|
244638
|
+
const { patch, missingKeys } = planChildConfigHeal(saved.config, parent.config);
|
|
244639
|
+
if (missingKeys.length === 0) return;
|
|
244640
|
+
await this.ctx.kernel.devices?.persistInitialConfig(saved.stableId, {
|
|
244641
|
+
...saved.config,
|
|
244642
|
+
...patch
|
|
244643
|
+
});
|
|
244644
|
+
this.ctx.logger.warn("Reolink child config healed from its parent before restore", {
|
|
244645
|
+
tags: {
|
|
244646
|
+
deviceId: saved.id,
|
|
244647
|
+
stableId: saved.stableId
|
|
244648
|
+
},
|
|
244649
|
+
meta: {
|
|
244650
|
+
parentDeviceId: parent.id,
|
|
244651
|
+
restoredKeys: [...missingKeys]
|
|
244652
|
+
}
|
|
244653
|
+
});
|
|
244654
|
+
}
|
|
244655
|
+
listOnboardedForDiscovery() {
|
|
244656
|
+
const all = this.ctx.kernel.deviceRegistry?.getAll() ?? [];
|
|
244657
|
+
const onboarded = [];
|
|
244658
|
+
for (const d of all) {
|
|
244659
|
+
if (!(d instanceof ReolinkCamera) && !(d instanceof ReolinkHub)) continue;
|
|
244660
|
+
const values = d.config.values;
|
|
244661
|
+
const host = typeof values.host === "string" && values.host !== "" ? values.host : void 0;
|
|
244662
|
+
const uid = typeof values.uid === "string" && values.uid !== "" ? values.uid : void 0;
|
|
244663
|
+
if (host === void 0 && uid === void 0) continue;
|
|
244664
|
+
onboarded.push({
|
|
244665
|
+
deviceId: d.id,
|
|
244666
|
+
name: d.name,
|
|
244667
|
+
...host !== void 0 ? { host } : {},
|
|
244668
|
+
...uid !== void 0 ? { uid } : {}
|
|
244669
|
+
});
|
|
244670
|
+
}
|
|
244671
|
+
return onboarded;
|
|
244672
|
+
}
|
|
244673
|
+
/**
|
|
244413
244674
|
* Enumerate this addon's live `ReolinkCamera` instances from the
|
|
244414
244675
|
* global device registry. Used by the email-push server to resolve
|
|
244415
244676
|
* recipients + route inbound motion. Returns `[]` when the registry
|
|
@@ -244692,10 +244953,17 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
244692
244953
|
networkCidr: networkCidr || "local",
|
|
244693
244954
|
enableOnvif
|
|
244694
244955
|
} });
|
|
244695
|
-
|
|
244956
|
+
const onboarded = this.listOnboardedForDiscovery();
|
|
244957
|
+
const candidates = mapReolinkDiscoveryToCandidates(devices, {
|
|
244696
244958
|
username,
|
|
244697
244959
|
password
|
|
244698
|
-
});
|
|
244960
|
+
}, onboarded);
|
|
244961
|
+
const owned = candidates.filter((c) => c.alreadyOnboarded === true).length;
|
|
244962
|
+
if (owned > 0) this.ctx.logger.info("Reolink discovery: candidates already owned by this provider", { meta: {
|
|
244963
|
+
owned,
|
|
244964
|
+
total: candidates.length
|
|
244965
|
+
} });
|
|
244966
|
+
return candidates;
|
|
244699
244967
|
}
|
|
244700
244968
|
async adoptDiscoveredDevice(input) {
|
|
244701
244969
|
return this.createDevice({
|
package/dist/addon.mjs
CHANGED
|
@@ -13356,7 +13356,26 @@ var DiscoveryCandidateSchema = object({
|
|
|
13356
13356
|
* identity ahead of adoption. Rendering metadata (unit, precision)
|
|
13357
13357
|
* flows live through the cap STATUS SLICE after adoption.
|
|
13358
13358
|
*/
|
|
13359
|
-
sourceInfo: SourceInfoSchema.optional()
|
|
13359
|
+
sourceInfo: SourceInfoSchema.optional(),
|
|
13360
|
+
/**
|
|
13361
|
+
* Set when this candidate is a device the provider ALREADY owns.
|
|
13362
|
+
*
|
|
13363
|
+
* A scan cannot generally produce the identity a device was onboarded under
|
|
13364
|
+
* (Reolink keys on `mac-<mac>`, learned at adopt time), so a stableId
|
|
13365
|
+
* comparison never matches and an owned device looks addable. Re-adopting one
|
|
13366
|
+
* overwrites its config with scan-derived values — that is how a Home Hub's
|
|
13367
|
+
* Baichuan port was overwritten with its ONVIF port, taking the hub and its
|
|
13368
|
+
* three child cameras offline for four hours.
|
|
13369
|
+
*
|
|
13370
|
+
* A provider that can recognise its own devices says so here. Absent means
|
|
13371
|
+
* "not recognised", which is not the same as "known to be new" — a provider
|
|
13372
|
+
* that cannot tell simply never sets it.
|
|
13373
|
+
*/
|
|
13374
|
+
alreadyOnboarded: boolean().optional(),
|
|
13375
|
+
/** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
|
|
13376
|
+
onboardedDeviceId: number().optional(),
|
|
13377
|
+
/** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
|
|
13378
|
+
onboardedName: string().optional()
|
|
13360
13379
|
});
|
|
13361
13380
|
/**
|
|
13362
13381
|
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
@@ -34038,6 +34057,22 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
34038
34057
|
* failed — logged at ERROR with `tags.deviceId` and surfaced via
|
|
34039
34058
|
* `getStatus().error`.
|
|
34040
34059
|
*/
|
|
34060
|
+
/**
|
|
34061
|
+
* Repair a row's PERSISTED config blob immediately before it is restored.
|
|
34062
|
+
* Default: no-op — most providers have nothing to heal.
|
|
34063
|
+
*
|
|
34064
|
+
* This exists because a restored device self-hydrates from the DB: `create()`
|
|
34065
|
+
* passes `{}` and `BaseDevice` parses the stored blob against the device
|
|
34066
|
+
* schema. A blob that lost a REQUIRED field therefore fails restore forever,
|
|
34067
|
+
* and no later pass revisits it — a hub-adopted Reolink camera whose blob had
|
|
34068
|
+
* been emptied failed all four bounded attempts against fields
|
|
34069
|
+
* (`host`, `password`) it inherits from its parent and never dials itself.
|
|
34070
|
+
*
|
|
34071
|
+
* Implementations get every saved row, so a child can read its parent's blob.
|
|
34072
|
+
* A heal that throws is treated like any other restore failure: retried under
|
|
34073
|
+
* the bound, then reported — never swallowed.
|
|
34074
|
+
*/
|
|
34075
|
+
async healSavedConfig(_saved, _allSaved) {}
|
|
34041
34076
|
async onRestoreDevices(savedDevices) {
|
|
34042
34077
|
const restored = /* @__PURE__ */ new Set();
|
|
34043
34078
|
const failures = [];
|
|
@@ -34046,6 +34081,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
34046
34081
|
const Class = this.deviceClasses[saved.type];
|
|
34047
34082
|
if (!Class) throw new Error(`no device class registered for type "${saved.type}"`);
|
|
34048
34083
|
if (saved.parentDeviceId !== null && !restored.has(saved.parentDeviceId)) throw new Error(`parent device ${saved.parentDeviceId} not restored`);
|
|
34084
|
+
await this.healSavedConfig(saved, savedDevices);
|
|
34049
34085
|
await this.ctx.kernel.devices.create(saved.stableId, Class, {}, saved.parentDeviceId);
|
|
34050
34086
|
restored.add(saved.id);
|
|
34051
34087
|
};
|
|
@@ -242573,6 +242609,43 @@ function computeHealthCheckBackoffMs(consecutive) {
|
|
|
242573
242609
|
return 15 * 6e4;
|
|
242574
242610
|
}
|
|
242575
242611
|
//#endregion
|
|
242612
|
+
//#region src/hub-channel-reconcile.ts
|
|
242613
|
+
/**
|
|
242614
|
+
* Match adopted children to the hub's own channel list by IDENTITY, then report the
|
|
242615
|
+
* ones whose persisted `channel` is missing or wrong.
|
|
242616
|
+
*
|
|
242617
|
+
* A child's stableId is `${hubStableId}-${childNativeId}`, and `childNativeId` is built
|
|
242618
|
+
* from the camera's UID — so identity survives a channel move, while `channel` does not.
|
|
242619
|
+
* The hub is the authority on which slot a UID currently occupies.
|
|
242620
|
+
*
|
|
242621
|
+
* Why this exists: `loadAdoptedChildrenByChannel` keys the adopted set on the persisted
|
|
242622
|
+
* `channel` alone. A child that lost it is absent from that map, so the discovery panel
|
|
242623
|
+
* renders it `alreadyAdopted: false` — one click away from being adopted a SECOND time —
|
|
242624
|
+
* and `routeSimpleEvent` silently drops every push for its slot. Observed live on
|
|
242625
|
+
* 2026-09-04 with an Argus MagiCam that was actually on channel 3.
|
|
242626
|
+
*
|
|
242627
|
+
* Pure: no I/O. A discovered entry with no channel, or with no adopted child, is skipped —
|
|
242628
|
+
* this repairs what exists, it never adopts.
|
|
242629
|
+
*/
|
|
242630
|
+
function planChannelReconciliation(hubStableId, discovered, children) {
|
|
242631
|
+
const byStableId = /* @__PURE__ */ new Map();
|
|
242632
|
+
for (const child of children) byStableId.set(child.stableId, child);
|
|
242633
|
+
const repairs = [];
|
|
242634
|
+
for (const entry of discovered) {
|
|
242635
|
+
if (typeof entry.rtspChannel !== "number") continue;
|
|
242636
|
+
const child = byStableId.get(`${hubStableId}-${entry.childNativeId}`);
|
|
242637
|
+
if (child === void 0) continue;
|
|
242638
|
+
if (child.channel === entry.rtspChannel) continue;
|
|
242639
|
+
repairs.push({
|
|
242640
|
+
deviceId: child.deviceId,
|
|
242641
|
+
stableId: child.stableId,
|
|
242642
|
+
from: child.channel ?? null,
|
|
242643
|
+
to: entry.rtspChannel
|
|
242644
|
+
});
|
|
242645
|
+
}
|
|
242646
|
+
return repairs;
|
|
242647
|
+
}
|
|
242648
|
+
//#endregion
|
|
242576
242649
|
//#region src/simple-event-dispatch-trace.ts
|
|
242577
242650
|
/**
|
|
242578
242651
|
* Gate for hub-side simpleEvent traces.
|
|
@@ -243069,6 +243142,7 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
243069
243142
|
lastFetchedAt: Date.now()
|
|
243070
243143
|
};
|
|
243071
243144
|
this.runtimeState.setCapState(deviceDiscoveryCapability.name, slice);
|
|
243145
|
+
if (lastError === null) await this.reconcileChildChannels(discovered);
|
|
243072
243146
|
if (lastError === null) {
|
|
243073
243147
|
this.knownUnadoptedChannels.clear();
|
|
243074
243148
|
for (const d of discovered) {
|
|
@@ -243241,6 +243315,64 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
|
|
|
243241
243315
|
* the channel index. Returns a `channel → kernel deviceId` map.
|
|
243242
243316
|
* Used to gate the `alreadyAdopted` flag in the discovery list.
|
|
243243
243317
|
*/
|
|
243318
|
+
/**
|
|
243319
|
+
* Re-derive each adopted child's `channel` from the hub's own channel list, matching by
|
|
243320
|
+
* IDENTITY (stableId carries the camera UID) rather than by the channel itself.
|
|
243321
|
+
*
|
|
243322
|
+
* `loadAdoptedChildrenByChannel` keys the adopted set on the persisted `channel` alone,
|
|
243323
|
+
* so a child that lost it is invisible: the discovery panel renders it available (one
|
|
243324
|
+
* click from a second adoption of the same camera) and `routeSimpleEvent` drops every
|
|
243325
|
+
* push for its slot. A channel move produces the same damage under a wrong number.
|
|
243326
|
+
*
|
|
243327
|
+
* Writes through the live device so its in-memory config and the DB stay in step.
|
|
243328
|
+
* A child that is not live is skipped — it is repaired on its next successful restore.
|
|
243329
|
+
*/
|
|
243330
|
+
async reconcileChildChannels(discovered) {
|
|
243331
|
+
const children = await this.ctx.devices.getChildren(this.id);
|
|
243332
|
+
const adopted = children.map((child) => {
|
|
243333
|
+
const channel = child instanceof ReolinkCamera ? child.config.values.channel : void 0;
|
|
243334
|
+
return {
|
|
243335
|
+
deviceId: child.id,
|
|
243336
|
+
stableId: child.stableId,
|
|
243337
|
+
...typeof channel === "number" ? { channel } : {}
|
|
243338
|
+
};
|
|
243339
|
+
});
|
|
243340
|
+
const repairs = planChannelReconciliation(this.stableId, discovered.map((d) => ({
|
|
243341
|
+
childNativeId: d.childNativeId,
|
|
243342
|
+
...typeof d.metadata.rtspChannel === "number" ? { rtspChannel: d.metadata.rtspChannel } : {}
|
|
243343
|
+
})), adopted);
|
|
243344
|
+
if (repairs.length === 0) return;
|
|
243345
|
+
for (const repair of repairs) {
|
|
243346
|
+
const child = children.find((c) => c.id === repair.deviceId);
|
|
243347
|
+
if (!(child instanceof ReolinkCamera)) continue;
|
|
243348
|
+
try {
|
|
243349
|
+
await child.config.setAll({ channel: repair.to });
|
|
243350
|
+
this.channelToDeviceId.set(repair.to, repair.deviceId);
|
|
243351
|
+
this.ctx.logger.warn("Reolink Hub: repaired a child channel from the hub channel list", {
|
|
243352
|
+
tags: {
|
|
243353
|
+
deviceId: repair.deviceId,
|
|
243354
|
+
stableId: repair.stableId
|
|
243355
|
+
},
|
|
243356
|
+
meta: {
|
|
243357
|
+
from: repair.from,
|
|
243358
|
+
to: repair.to
|
|
243359
|
+
}
|
|
243360
|
+
});
|
|
243361
|
+
} catch (err) {
|
|
243362
|
+
this.ctx.logger.warn("Reolink Hub: child channel repair failed", {
|
|
243363
|
+
tags: {
|
|
243364
|
+
deviceId: repair.deviceId,
|
|
243365
|
+
stableId: repair.stableId
|
|
243366
|
+
},
|
|
243367
|
+
meta: {
|
|
243368
|
+
from: repair.from,
|
|
243369
|
+
to: repair.to,
|
|
243370
|
+
error: err instanceof Error ? err.message : String(err)
|
|
243371
|
+
}
|
|
243372
|
+
});
|
|
243373
|
+
}
|
|
243374
|
+
}
|
|
243375
|
+
}
|
|
243244
243376
|
async loadAdoptedChildrenByChannel() {
|
|
243245
243377
|
const result = /* @__PURE__ */ new Map();
|
|
243246
243378
|
const children = await this.ctx.devices.getChildren(this.id);
|
|
@@ -243705,6 +243837,53 @@ function buildCreationFormSchema() {
|
|
|
243705
243837
|
] };
|
|
243706
243838
|
}
|
|
243707
243839
|
//#endregion
|
|
243840
|
+
//#region src/child-config-heal.ts
|
|
243841
|
+
/** Connection fields a hub-adopted child inherits from its parent at adoption time. */
|
|
243842
|
+
var INHERITED_CONNECTION_KEYS = [
|
|
243843
|
+
"host",
|
|
243844
|
+
"port",
|
|
243845
|
+
"username",
|
|
243846
|
+
"password",
|
|
243847
|
+
"transport"
|
|
243848
|
+
];
|
|
243849
|
+
/** A stored value counts as present only when it is a usable string/number. */
|
|
243850
|
+
function isPresent(value) {
|
|
243851
|
+
if (typeof value === "string") return value !== "";
|
|
243852
|
+
return typeof value === "number";
|
|
243853
|
+
}
|
|
243854
|
+
/**
|
|
243855
|
+
* Work out what a hub-adopted child's persisted blob is missing from the connection
|
|
243856
|
+
* fields it inherited from its parent when it was adopted.
|
|
243857
|
+
*
|
|
243858
|
+
* `ReolinkCamera` with a parent uses `parent.api` and never dials its own login — but
|
|
243859
|
+
* `reolinkCameraSchema` still requires `host` and `password` as strings, so a blob that
|
|
243860
|
+
* lost them fails to parse and the camera cannot be restored at all. Observed live on
|
|
243861
|
+
* 2026-09-04: an Argus MagiCam under a Home Hub carried `{}` and burned all four bounded
|
|
243862
|
+
* restore attempts on `host`/`password` — fields it would never have used.
|
|
243863
|
+
*
|
|
243864
|
+
* Only ABSENT fields are filled. A child that deliberately differs from its parent (a
|
|
243865
|
+
* per-child credential, a non-default port) keeps what it has.
|
|
243866
|
+
*/
|
|
243867
|
+
function planChildConfigHeal(childConfig, parentConfig) {
|
|
243868
|
+
const patch = {};
|
|
243869
|
+
const missingKeys = [];
|
|
243870
|
+
for (const key of INHERITED_CONNECTION_KEYS) {
|
|
243871
|
+
if (isPresent(childConfig[key])) continue;
|
|
243872
|
+
if (!isPresent(parentConfig[key])) continue;
|
|
243873
|
+
patch[key] = parentConfig[key];
|
|
243874
|
+
missingKeys.push(key);
|
|
243875
|
+
}
|
|
243876
|
+
return {
|
|
243877
|
+
patch,
|
|
243878
|
+
missingKeys
|
|
243879
|
+
};
|
|
243880
|
+
}
|
|
243881
|
+
/** Find the saved row a child names as its parent. */
|
|
243882
|
+
function findParentRow(child, allSaved) {
|
|
243883
|
+
if (child.parentDeviceId === null) return void 0;
|
|
243884
|
+
return allSaved.find((row) => row.id === child.parentDeviceId);
|
|
243885
|
+
}
|
|
243886
|
+
//#endregion
|
|
243708
243887
|
//#region src/reolink-discovery-map.ts
|
|
243709
243888
|
/**
|
|
243710
243889
|
* Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
|
|
@@ -243714,21 +243893,44 @@ function slugifyReolinkHost(host) {
|
|
|
243714
243893
|
return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
243715
243894
|
}
|
|
243716
243895
|
/**
|
|
243896
|
+
* Decide whether a discovered host is a device this provider already owns.
|
|
243897
|
+
*
|
|
243898
|
+
* Matching is by UID first (stable across a DHCP lease change) and by host second. A UID match
|
|
243899
|
+
* wins even when the host differs, because the address is the thing that moves.
|
|
243900
|
+
*/
|
|
243901
|
+
function findOnboardedMatch(discovered, onboarded) {
|
|
243902
|
+
if (discovered.uid !== void 0 && discovered.uid !== "") {
|
|
243903
|
+
const byUid = onboarded.find((o) => o.uid === discovered.uid);
|
|
243904
|
+
if (byUid !== void 0) return byUid;
|
|
243905
|
+
}
|
|
243906
|
+
return onboarded.find((o) => o.host !== void 0 && o.host === discovered.host);
|
|
243907
|
+
}
|
|
243908
|
+
/**
|
|
243717
243909
|
* Map discovered Reolink hosts to adoption {@link DiscoveryCandidate}s, de-duplicated by host (the
|
|
243718
243910
|
* same camera can answer on more than one discovery method — UDP broadcast, ONVIF, HTTP scan). The
|
|
243719
243911
|
* first responder for a host wins. Pure: no I/O.
|
|
243720
243912
|
*
|
|
243913
|
+
* **The scan never supplies `port`.** `port` in the Reolink config schema is the BAICHUAN port
|
|
243914
|
+
* (default 9000). Discovery reports `httpPort`, the HTTP/ONVIF listener — on a Reolink Home Hub
|
|
243915
|
+
* that is 8000, while Baichuan lives on `mediaPort` 9000. Writing the discovered value into `port`
|
|
243916
|
+
* pointed every Baichuan login at the ONVIF listener, which accepts the TCP connection and then
|
|
243917
|
+
* closes it: `Baichuan socket closed`, 316 times in 12 hours, taking the hub and all three of its
|
|
243918
|
+
* child cameras offline. The default is correct; a non-default Baichuan port is an operator edit,
|
|
243919
|
+
* never a scan result.
|
|
243920
|
+
*
|
|
243721
243921
|
* The authoritative stableId is `mac-<mac>` (learned during autodetect at adopt time), which discovery
|
|
243722
|
-
* can't produce — so
|
|
243723
|
-
*
|
|
243922
|
+
* can't produce — so candidates carry `alreadyOnboarded` rather than relying on a stableId match,
|
|
243923
|
+
* which never fires for a MAC-keyed device. Re-adding an owned device is how the port got clobbered
|
|
243924
|
+
* in the first place, so a scan says so instead of offering it again.
|
|
243724
243925
|
*/
|
|
243725
|
-
function mapReolinkDiscoveryToCandidates(devices, credentials) {
|
|
243926
|
+
function mapReolinkDiscoveryToCandidates(devices, credentials, onboarded = []) {
|
|
243726
243927
|
const username = credentials.username?.trim() ?? "";
|
|
243727
243928
|
const password = credentials.password ?? "";
|
|
243728
243929
|
const byHost = /* @__PURE__ */ new Map();
|
|
243729
243930
|
for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
|
|
243730
243931
|
return [...byHost.values()].map((d) => {
|
|
243731
243932
|
const displayName = d.name ?? d.model ?? d.host;
|
|
243933
|
+
const match = findOnboardedMatch(d, onboarded);
|
|
243732
243934
|
return {
|
|
243733
243935
|
stableId: `host-${slugifyReolinkHost(d.host)}`,
|
|
243734
243936
|
type: DeviceType.Camera,
|
|
@@ -243737,11 +243939,15 @@ function mapReolinkDiscoveryToCandidates(devices, credentials) {
|
|
|
243737
243939
|
name: displayName,
|
|
243738
243940
|
host: d.host,
|
|
243739
243941
|
transport: "auto",
|
|
243740
|
-
...d.httpPort !== void 0 ? { port: d.httpPort } : {},
|
|
243741
243942
|
...d.uid ? { uid: d.uid } : {},
|
|
243742
243943
|
...username ? { username } : {},
|
|
243743
243944
|
...password ? { password } : {}
|
|
243744
|
-
}
|
|
243945
|
+
},
|
|
243946
|
+
...match !== void 0 ? {
|
|
243947
|
+
alreadyOnboarded: true,
|
|
243948
|
+
onboardedDeviceId: match.deviceId,
|
|
243949
|
+
onboardedName: match.name
|
|
243950
|
+
} : {}
|
|
243745
243951
|
};
|
|
243746
243952
|
});
|
|
243747
243953
|
}
|
|
@@ -244390,6 +244596,61 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
244390
244596
|
super({});
|
|
244391
244597
|
}
|
|
244392
244598
|
/**
|
|
244599
|
+
* Reduce this addon's live devices to the identities a network scan can produce, so
|
|
244600
|
+
* {@link mapReolinkDiscoveryToCandidates} can recognise a device it already owns.
|
|
244601
|
+
*
|
|
244602
|
+
* Hubs are included: the incident this exists for was a Home Hub re-offered by a scan and
|
|
244603
|
+
* re-added, which overwrote its Baichuan port with the ONVIF port the scan reported.
|
|
244604
|
+
*/
|
|
244605
|
+
/**
|
|
244606
|
+
* Re-inherit the parent's connection fields into a hub-adopted child whose persisted
|
|
244607
|
+
* blob lost them, so the child can be restored at all.
|
|
244608
|
+
*
|
|
244609
|
+
* `adoptDiscoveredChild` copies `host`/`port`/`username`/`password`/`transport` down
|
|
244610
|
+
* from the Hub at adoption. Nothing re-applied them afterwards, so a child whose blob
|
|
244611
|
+
* was emptied failed `reolinkCameraSchema` on every restore — on fields a child never
|
|
244612
|
+
* dials, because a child with a parent uses `parent.api`. Only ABSENT fields are
|
|
244613
|
+
* filled; a child that deliberately differs keeps what it has.
|
|
244614
|
+
*/
|
|
244615
|
+
async healSavedConfig(saved, allSaved) {
|
|
244616
|
+
const parent = findParentRow(saved, allSaved);
|
|
244617
|
+
if (parent === void 0) return;
|
|
244618
|
+
const { patch, missingKeys } = planChildConfigHeal(saved.config, parent.config);
|
|
244619
|
+
if (missingKeys.length === 0) return;
|
|
244620
|
+
await this.ctx.kernel.devices?.persistInitialConfig(saved.stableId, {
|
|
244621
|
+
...saved.config,
|
|
244622
|
+
...patch
|
|
244623
|
+
});
|
|
244624
|
+
this.ctx.logger.warn("Reolink child config healed from its parent before restore", {
|
|
244625
|
+
tags: {
|
|
244626
|
+
deviceId: saved.id,
|
|
244627
|
+
stableId: saved.stableId
|
|
244628
|
+
},
|
|
244629
|
+
meta: {
|
|
244630
|
+
parentDeviceId: parent.id,
|
|
244631
|
+
restoredKeys: [...missingKeys]
|
|
244632
|
+
}
|
|
244633
|
+
});
|
|
244634
|
+
}
|
|
244635
|
+
listOnboardedForDiscovery() {
|
|
244636
|
+
const all = this.ctx.kernel.deviceRegistry?.getAll() ?? [];
|
|
244637
|
+
const onboarded = [];
|
|
244638
|
+
for (const d of all) {
|
|
244639
|
+
if (!(d instanceof ReolinkCamera) && !(d instanceof ReolinkHub)) continue;
|
|
244640
|
+
const values = d.config.values;
|
|
244641
|
+
const host = typeof values.host === "string" && values.host !== "" ? values.host : void 0;
|
|
244642
|
+
const uid = typeof values.uid === "string" && values.uid !== "" ? values.uid : void 0;
|
|
244643
|
+
if (host === void 0 && uid === void 0) continue;
|
|
244644
|
+
onboarded.push({
|
|
244645
|
+
deviceId: d.id,
|
|
244646
|
+
name: d.name,
|
|
244647
|
+
...host !== void 0 ? { host } : {},
|
|
244648
|
+
...uid !== void 0 ? { uid } : {}
|
|
244649
|
+
});
|
|
244650
|
+
}
|
|
244651
|
+
return onboarded;
|
|
244652
|
+
}
|
|
244653
|
+
/**
|
|
244393
244654
|
* Enumerate this addon's live `ReolinkCamera` instances from the
|
|
244394
244655
|
* global device registry. Used by the email-push server to resolve
|
|
244395
244656
|
* recipients + route inbound motion. Returns `[]` when the registry
|
|
@@ -244672,10 +244933,17 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
244672
244933
|
networkCidr: networkCidr || "local",
|
|
244673
244934
|
enableOnvif
|
|
244674
244935
|
} });
|
|
244675
|
-
|
|
244936
|
+
const onboarded = this.listOnboardedForDiscovery();
|
|
244937
|
+
const candidates = mapReolinkDiscoveryToCandidates(devices, {
|
|
244676
244938
|
username,
|
|
244677
244939
|
password
|
|
244678
|
-
});
|
|
244940
|
+
}, onboarded);
|
|
244941
|
+
const owned = candidates.filter((c) => c.alreadyOnboarded === true).length;
|
|
244942
|
+
if (owned > 0) this.ctx.logger.info("Reolink discovery: candidates already owned by this provider", { meta: {
|
|
244943
|
+
owned,
|
|
244944
|
+
total: candidates.length
|
|
244945
|
+
} });
|
|
244946
|
+
return candidates;
|
|
244679
244947
|
}
|
|
244680
244948
|
async adoptDiscoveredDevice(input) {
|
|
244681
244949
|
return this.createDevice({
|