@camstack/addon-provider-reolink 1.2.215 → 1.2.216
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/CHANGELOG.md +6 -0
- package/dist/addon.js +171 -4
- package/dist/addon.mjs +171 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.216] - 2026-10-31
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The NVR / Home Hub can be added as a Gateway device (reboot, web interface link) next to its channel cameras.
|
|
15
|
+
|
|
10
16
|
## [1.2.215] - 2026-10-31
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/dist/addon.js
CHANGED
|
@@ -203955,6 +203955,14 @@ var ReolinkHubSession = class {
|
|
|
203955
203955
|
get isOnline() {
|
|
203956
203956
|
return this.#online;
|
|
203957
203957
|
}
|
|
203958
|
+
/** The integration's display name. */
|
|
203959
|
+
get name() {
|
|
203960
|
+
return this.#deps.name;
|
|
203961
|
+
}
|
|
203962
|
+
/** Restart the hub itself (not a camera behind it). */
|
|
203963
|
+
async reboot() {
|
|
203964
|
+
await (await this.getApi()).reboot();
|
|
203965
|
+
}
|
|
203958
203966
|
/** The last channel list, for the adoption surface. */
|
|
203959
203967
|
channelSnapshot() {
|
|
203960
203968
|
return this.#snapshot;
|
|
@@ -204413,6 +204421,28 @@ function toDiscoveredChild(channel, adoptedDeviceId) {
|
|
|
204413
204421
|
adoptedDeviceId
|
|
204414
204422
|
};
|
|
204415
204423
|
}
|
|
204424
|
+
var GATEWAY_SUFFIX = ":gateway";
|
|
204425
|
+
/** The `childNativeId` of the hub box itself, one per integration. */
|
|
204426
|
+
function hubGatewayNativeId(integrationId) {
|
|
204427
|
+
return `${integrationId}${GATEWAY_SUFFIX}`;
|
|
204428
|
+
}
|
|
204429
|
+
/**
|
|
204430
|
+
* The hub (NVR / Home Hub) as an adoption candidate: a {@link DeviceType.Gateway} that is a
|
|
204431
|
+
* sibling of its channel cameras, never their parent. Only what the library really exposes:
|
|
204432
|
+
* `reboot` (Baichuan `Reboot`) and the unit's web page.
|
|
204433
|
+
*/
|
|
204434
|
+
function toDiscoveredGateway(input) {
|
|
204435
|
+
return {
|
|
204436
|
+
childNativeId: hubGatewayNativeId(input.integrationId),
|
|
204437
|
+
name: input.name,
|
|
204438
|
+
type: DeviceType.Gateway,
|
|
204439
|
+
status: input.online ? "online" : "offline",
|
|
204440
|
+
metadata: { manufacturer: "Reolink" },
|
|
204441
|
+
alreadyAdopted: input.adoptedDeviceId !== null,
|
|
204442
|
+
adoptedDeviceId: input.adoptedDeviceId,
|
|
204443
|
+
capabilities: [rebootCapability.name, deviceAdminLinkCapability.name]
|
|
204444
|
+
};
|
|
204445
|
+
}
|
|
204416
204446
|
/**
|
|
204417
204447
|
* The per-integration registry the cameras reach their session through (the shape of
|
|
204418
204448
|
* `veluxGateways` / `enpalConnections`). It also holds the children, so a child that activates
|
|
@@ -217687,7 +217717,13 @@ function buildReolinkAdoptionProvider(deps) {
|
|
|
217687
217717
|
const session = getSession(integrationId);
|
|
217688
217718
|
if (session === null) return [];
|
|
217689
217719
|
const adopted = await listAdopted();
|
|
217690
|
-
|
|
217720
|
+
const gateways = await deps.listAdoptedGateways();
|
|
217721
|
+
return [toDiscoveredGateway({
|
|
217722
|
+
integrationId,
|
|
217723
|
+
name: session.name,
|
|
217724
|
+
online: session.isOnline,
|
|
217725
|
+
adoptedDeviceId: gateways.find((g) => g.integrationId === integrationId)?.id ?? null
|
|
217726
|
+
}), ...session.channelSnapshot().channels.map((channel) => toDiscoveredChild(channel, findAdoptedForChannel(adopted, integrationId, channel)))];
|
|
217691
217727
|
}
|
|
217692
217728
|
async function status(integrationId) {
|
|
217693
217729
|
const all = await candidates(integrationId);
|
|
@@ -217748,6 +217784,19 @@ function buildReolinkAdoptionProvider(deps) {
|
|
|
217748
217784
|
const adopted = [];
|
|
217749
217785
|
let failures = 0;
|
|
217750
217786
|
for (const childNativeId of childNativeIds) try {
|
|
217787
|
+
if (childNativeId === hubGatewayNativeId(integrationId)) {
|
|
217788
|
+
const known = (await deps.listAdoptedGateways()).find((g) => g.integrationId === integrationId);
|
|
217789
|
+
const { deviceId } = known !== void 0 ? { deviceId: known.id } : await deps.adoptGateway({
|
|
217790
|
+
integrationId,
|
|
217791
|
+
name: perCandidate?.[childNativeId]?.name ?? session.name
|
|
217792
|
+
});
|
|
217793
|
+
adopted.push({
|
|
217794
|
+
childNativeId,
|
|
217795
|
+
parentDeviceId: deviceId,
|
|
217796
|
+
accessoryDeviceIds: []
|
|
217797
|
+
});
|
|
217798
|
+
continue;
|
|
217799
|
+
}
|
|
217751
217800
|
const channel = session.channelSnapshot().channels.find((c) => c.childNativeId === childNativeId);
|
|
217752
217801
|
if (channel === void 0) {
|
|
217753
217802
|
logger.warn("reolink adopt: the hub lists no such channel — skipping", { meta: {
|
|
@@ -217794,7 +217843,7 @@ function buildReolinkAdoptionProvider(deps) {
|
|
|
217794
217843
|
},
|
|
217795
217844
|
resync: async ({ camDeviceId }) => {
|
|
217796
217845
|
const cfg = await deps.findDeviceConfig(camDeviceId);
|
|
217797
|
-
if (cfg === null || typeof cfg["integrationId"] !== "string") throw new Error(`reolink resync: device ${camDeviceId} is not an NVR-adopted
|
|
217846
|
+
if (cfg === null || typeof cfg["integrationId"] !== "string") throw new Error(`reolink resync: device ${camDeviceId} is not an NVR-adopted device`);
|
|
217798
217847
|
return {
|
|
217799
217848
|
changed: false,
|
|
217800
217849
|
rebuiltChildren: 0
|
|
@@ -218032,6 +218081,86 @@ async function retireMigratedHubs(retirements, ports) {
|
|
|
218032
218081
|
return retired;
|
|
218033
218082
|
}
|
|
218034
218083
|
//#endregion
|
|
218084
|
+
//#region src/reolink-gateway-device.ts
|
|
218085
|
+
var reolinkGatewayDeviceSchema = object({
|
|
218086
|
+
integrationId: string().min(1).describe("Integration (hub session) the device belongs to"),
|
|
218087
|
+
/** The adoption candidate id (`<integration>:gateway`). */
|
|
218088
|
+
nativeId: string().min(1)
|
|
218089
|
+
});
|
|
218090
|
+
/**
|
|
218091
|
+
* The physical NVR / Home Hub, as a {@link DeviceType.Gateway}. Its connection and password
|
|
218092
|
+
* live on the INTEGRATION; this device is the box's presence in the device list, and carries
|
|
218093
|
+
* the actions the library really supports through existing caps:
|
|
218094
|
+
*
|
|
218095
|
+
* - `reboot` (Baichuan `Reboot`, no channel), advertised as {@link DeviceFeature.Rebootable};
|
|
218096
|
+
* - `device-admin-link`, the unit's own web page.
|
|
218097
|
+
*
|
|
218098
|
+
* Cameras are NOT children of it: they are adopted as their own top-level devices of the
|
|
218099
|
+
* same integration. The addon flips `online` when the hub socket changes.
|
|
218100
|
+
*/
|
|
218101
|
+
var ReolinkGatewayDevice = class extends BaseDevice {
|
|
218102
|
+
features = [DeviceFeature.Rebootable];
|
|
218103
|
+
integrationId;
|
|
218104
|
+
constructor(ctx) {
|
|
218105
|
+
const cfg = reolinkGatewayDeviceSchema.parse(ctx.persistedConfig ?? {});
|
|
218106
|
+
super(ctx, reolinkGatewayDeviceSchema, { type: DeviceType.Gateway });
|
|
218107
|
+
this.integrationId = cfg.integrationId;
|
|
218108
|
+
this.online = reolinkHubSessions.get(this.integrationId)?.isOnline === true;
|
|
218109
|
+
this.registerRebootCap();
|
|
218110
|
+
this.registerAdminLinkCap();
|
|
218111
|
+
this.updateSourceInfo({
|
|
218112
|
+
id: cfg.nativeId,
|
|
218113
|
+
system: "reolink"
|
|
218114
|
+
});
|
|
218115
|
+
}
|
|
218116
|
+
registerRebootCap() {
|
|
218117
|
+
this.ctx.registerNativeCap(rebootCapability, { reboot: async ({ deviceId }) => {
|
|
218118
|
+
if (deviceId !== this.id) throw new Error(`ReolinkGatewayDevice: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
218119
|
+
const session = reolinkHubSessions.get(this.integrationId);
|
|
218120
|
+
if (session === null) {
|
|
218121
|
+
this.ctx.logger.warn("Reolink hub reboot refused: no live session", { tags: { deviceId: this.id } });
|
|
218122
|
+
throw new Error("The hub session is down, so it cannot be told to reboot.");
|
|
218123
|
+
}
|
|
218124
|
+
await session.reboot();
|
|
218125
|
+
this.ctx.logger.info("Reolink hub reboot requested", { tags: { deviceId: this.id } });
|
|
218126
|
+
return { success: true };
|
|
218127
|
+
} });
|
|
218128
|
+
}
|
|
218129
|
+
/**
|
|
218130
|
+
* The hub's web interface, on the hub host. The password is NOT placed in the URL; the page
|
|
218131
|
+
* asks for it.
|
|
218132
|
+
*/
|
|
218133
|
+
registerAdminLinkCap() {
|
|
218134
|
+
this.ctx.registerNativeCap(deviceAdminLinkCapability, { getAdminLink: async ({ deviceId }) => {
|
|
218135
|
+
if (deviceId !== this.id) throw new Error(`ReolinkGatewayDevice: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
218136
|
+
const host = reolinkHubSessions.get(this.integrationId)?.relayEndpoint().host;
|
|
218137
|
+
if (host === void 0) {
|
|
218138
|
+
this.ctx.logger.debug("reolink admin link unavailable: integration not running", { tags: { deviceId: this.id } });
|
|
218139
|
+
return null;
|
|
218140
|
+
}
|
|
218141
|
+
const built = buildDeviceAdminUrl({
|
|
218142
|
+
host,
|
|
218143
|
+
port: null,
|
|
218144
|
+
scheme: "https",
|
|
218145
|
+
path: "/"
|
|
218146
|
+
});
|
|
218147
|
+
if (!built.ok) {
|
|
218148
|
+
this.ctx.logger.debug("reolink admin link unavailable", {
|
|
218149
|
+
tags: { deviceId: this.id },
|
|
218150
|
+
meta: { reason: built.reason }
|
|
218151
|
+
});
|
|
218152
|
+
return null;
|
|
218153
|
+
}
|
|
218154
|
+
return {
|
|
218155
|
+
url: built.url,
|
|
218156
|
+
label: "Reolink web interface",
|
|
218157
|
+
target: "integration",
|
|
218158
|
+
host
|
|
218159
|
+
};
|
|
218160
|
+
} });
|
|
218161
|
+
}
|
|
218162
|
+
};
|
|
218163
|
+
//#endregion
|
|
218035
218164
|
//#region src/addon.ts
|
|
218036
218165
|
/**
|
|
218037
218166
|
* Adapt a camstack scoped logger to the lib's `Logger` shape (just
|
|
@@ -218195,7 +218324,10 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
218195
218324
|
secretStatePaths = ["emailPushAuthPassword"];
|
|
218196
218325
|
addonId = REOLINK_ADDON_ID;
|
|
218197
218326
|
providerName = "Reolink";
|
|
218198
|
-
deviceClasses = {
|
|
218327
|
+
deviceClasses = {
|
|
218328
|
+
[DeviceType.Camera]: ReolinkCamera,
|
|
218329
|
+
[DeviceType.Gateway]: ReolinkGatewayDevice
|
|
218330
|
+
};
|
|
218199
218331
|
/**
|
|
218200
218332
|
* Short-lived cache that bridges `testCreationField` and
|
|
218201
218333
|
* `onCreateDevice` — Slice 15 socket reuse. Both code paths key off
|
|
@@ -218410,6 +218542,15 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
218410
218542
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
|
|
218411
218543
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
|
|
218412
218544
|
}
|
|
218545
|
+
/** The hub box follows its integration's socket. */
|
|
218546
|
+
markGatewaysOnline(integrationId, online) {
|
|
218547
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
218548
|
+
if (!reg) return;
|
|
218549
|
+
for (const d of reg.getAllForAddon(this.addonId)) {
|
|
218550
|
+
if (!(d instanceof ReolinkGatewayDevice) || d.integrationId !== integrationId) continue;
|
|
218551
|
+
if (d.online !== online) d.markOnline(online);
|
|
218552
|
+
}
|
|
218553
|
+
}
|
|
218413
218554
|
buildAdoptionProvider() {
|
|
218414
218555
|
return buildReolinkAdoptionProvider({
|
|
218415
218556
|
getSession: (integrationId) => this.hubManager?.get(integrationId) ?? null,
|
|
@@ -218455,6 +218596,29 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
218455
218596
|
accessoryDeviceIds: children.map((c) => c.id)
|
|
218456
218597
|
};
|
|
218457
218598
|
},
|
|
218599
|
+
listAdoptedGateways: async () => {
|
|
218600
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
218601
|
+
if (!reg) return [];
|
|
218602
|
+
const out = [];
|
|
218603
|
+
for (const d of reg.getAllForAddon(this.addonId)) if (d instanceof ReolinkGatewayDevice) out.push({
|
|
218604
|
+
id: d.id,
|
|
218605
|
+
integrationId: d.integrationId
|
|
218606
|
+
});
|
|
218607
|
+
return out;
|
|
218608
|
+
},
|
|
218609
|
+
adoptGateway: async ({ integrationId, name }) => {
|
|
218610
|
+
const devices = this.ctx.kernel.devices;
|
|
218611
|
+
if (!devices) throw new Error("reolink adopt: kernel.devices unavailable");
|
|
218612
|
+
const nativeId = hubGatewayNativeId(integrationId);
|
|
218613
|
+
return { deviceId: (await devices.create(`${integrationId}-${nativeId}`, ReolinkGatewayDevice, {
|
|
218614
|
+
integrationId,
|
|
218615
|
+
nativeId
|
|
218616
|
+
}, null, {
|
|
218617
|
+
type: DeviceType.Gateway,
|
|
218618
|
+
name,
|
|
218619
|
+
integrationId
|
|
218620
|
+
})).id };
|
|
218621
|
+
},
|
|
218458
218622
|
removeDevice: async (id) => {
|
|
218459
218623
|
await this.ctx.kernel.devices?.remove(id);
|
|
218460
218624
|
},
|
|
@@ -218660,7 +218824,10 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
218660
218824
|
this.listReolinkCameras();
|
|
218661
218825
|
this.hubManager = createReolinkHubManager({
|
|
218662
218826
|
logger: this.ctx.logger,
|
|
218663
|
-
onConnectionChange: (integrationId, online) =>
|
|
218827
|
+
onConnectionChange: (integrationId, online) => {
|
|
218828
|
+
this.ctx.logger.info(`Reolink NVR socket ${online ? "up" : "down"}`, { meta: { integrationId } });
|
|
218829
|
+
this.markGatewaysOnline(integrationId, online);
|
|
218830
|
+
}
|
|
218664
218831
|
}, reolinkHubSessions);
|
|
218665
218832
|
await this.reconcileIntegrations();
|
|
218666
218833
|
this.subscribeIntegrationLifecycle();
|
package/dist/addon.mjs
CHANGED
|
@@ -203947,6 +203947,14 @@ var ReolinkHubSession = class {
|
|
|
203947
203947
|
get isOnline() {
|
|
203948
203948
|
return this.#online;
|
|
203949
203949
|
}
|
|
203950
|
+
/** The integration's display name. */
|
|
203951
|
+
get name() {
|
|
203952
|
+
return this.#deps.name;
|
|
203953
|
+
}
|
|
203954
|
+
/** Restart the hub itself (not a camera behind it). */
|
|
203955
|
+
async reboot() {
|
|
203956
|
+
await (await this.getApi()).reboot();
|
|
203957
|
+
}
|
|
203950
203958
|
/** The last channel list, for the adoption surface. */
|
|
203951
203959
|
channelSnapshot() {
|
|
203952
203960
|
return this.#snapshot;
|
|
@@ -204405,6 +204413,28 @@ function toDiscoveredChild(channel, adoptedDeviceId) {
|
|
|
204405
204413
|
adoptedDeviceId
|
|
204406
204414
|
};
|
|
204407
204415
|
}
|
|
204416
|
+
var GATEWAY_SUFFIX = ":gateway";
|
|
204417
|
+
/** The `childNativeId` of the hub box itself, one per integration. */
|
|
204418
|
+
function hubGatewayNativeId(integrationId) {
|
|
204419
|
+
return `${integrationId}${GATEWAY_SUFFIX}`;
|
|
204420
|
+
}
|
|
204421
|
+
/**
|
|
204422
|
+
* The hub (NVR / Home Hub) as an adoption candidate: a {@link DeviceType.Gateway} that is a
|
|
204423
|
+
* sibling of its channel cameras, never their parent. Only what the library really exposes:
|
|
204424
|
+
* `reboot` (Baichuan `Reboot`) and the unit's web page.
|
|
204425
|
+
*/
|
|
204426
|
+
function toDiscoveredGateway(input) {
|
|
204427
|
+
return {
|
|
204428
|
+
childNativeId: hubGatewayNativeId(input.integrationId),
|
|
204429
|
+
name: input.name,
|
|
204430
|
+
type: DeviceType.Gateway,
|
|
204431
|
+
status: input.online ? "online" : "offline",
|
|
204432
|
+
metadata: { manufacturer: "Reolink" },
|
|
204433
|
+
alreadyAdopted: input.adoptedDeviceId !== null,
|
|
204434
|
+
adoptedDeviceId: input.adoptedDeviceId,
|
|
204435
|
+
capabilities: [rebootCapability.name, deviceAdminLinkCapability.name]
|
|
204436
|
+
};
|
|
204437
|
+
}
|
|
204408
204438
|
/**
|
|
204409
204439
|
* The per-integration registry the cameras reach their session through (the shape of
|
|
204410
204440
|
* `veluxGateways` / `enpalConnections`). It also holds the children, so a child that activates
|
|
@@ -217679,7 +217709,13 @@ function buildReolinkAdoptionProvider(deps) {
|
|
|
217679
217709
|
const session = getSession(integrationId);
|
|
217680
217710
|
if (session === null) return [];
|
|
217681
217711
|
const adopted = await listAdopted();
|
|
217682
|
-
|
|
217712
|
+
const gateways = await deps.listAdoptedGateways();
|
|
217713
|
+
return [toDiscoveredGateway({
|
|
217714
|
+
integrationId,
|
|
217715
|
+
name: session.name,
|
|
217716
|
+
online: session.isOnline,
|
|
217717
|
+
adoptedDeviceId: gateways.find((g) => g.integrationId === integrationId)?.id ?? null
|
|
217718
|
+
}), ...session.channelSnapshot().channels.map((channel) => toDiscoveredChild(channel, findAdoptedForChannel(adopted, integrationId, channel)))];
|
|
217683
217719
|
}
|
|
217684
217720
|
async function status(integrationId) {
|
|
217685
217721
|
const all = await candidates(integrationId);
|
|
@@ -217740,6 +217776,19 @@ function buildReolinkAdoptionProvider(deps) {
|
|
|
217740
217776
|
const adopted = [];
|
|
217741
217777
|
let failures = 0;
|
|
217742
217778
|
for (const childNativeId of childNativeIds) try {
|
|
217779
|
+
if (childNativeId === hubGatewayNativeId(integrationId)) {
|
|
217780
|
+
const known = (await deps.listAdoptedGateways()).find((g) => g.integrationId === integrationId);
|
|
217781
|
+
const { deviceId } = known !== void 0 ? { deviceId: known.id } : await deps.adoptGateway({
|
|
217782
|
+
integrationId,
|
|
217783
|
+
name: perCandidate?.[childNativeId]?.name ?? session.name
|
|
217784
|
+
});
|
|
217785
|
+
adopted.push({
|
|
217786
|
+
childNativeId,
|
|
217787
|
+
parentDeviceId: deviceId,
|
|
217788
|
+
accessoryDeviceIds: []
|
|
217789
|
+
});
|
|
217790
|
+
continue;
|
|
217791
|
+
}
|
|
217743
217792
|
const channel = session.channelSnapshot().channels.find((c) => c.childNativeId === childNativeId);
|
|
217744
217793
|
if (channel === void 0) {
|
|
217745
217794
|
logger.warn("reolink adopt: the hub lists no such channel — skipping", { meta: {
|
|
@@ -217786,7 +217835,7 @@ function buildReolinkAdoptionProvider(deps) {
|
|
|
217786
217835
|
},
|
|
217787
217836
|
resync: async ({ camDeviceId }) => {
|
|
217788
217837
|
const cfg = await deps.findDeviceConfig(camDeviceId);
|
|
217789
|
-
if (cfg === null || typeof cfg["integrationId"] !== "string") throw new Error(`reolink resync: device ${camDeviceId} is not an NVR-adopted
|
|
217838
|
+
if (cfg === null || typeof cfg["integrationId"] !== "string") throw new Error(`reolink resync: device ${camDeviceId} is not an NVR-adopted device`);
|
|
217790
217839
|
return {
|
|
217791
217840
|
changed: false,
|
|
217792
217841
|
rebuiltChildren: 0
|
|
@@ -218024,6 +218073,86 @@ async function retireMigratedHubs(retirements, ports) {
|
|
|
218024
218073
|
return retired;
|
|
218025
218074
|
}
|
|
218026
218075
|
//#endregion
|
|
218076
|
+
//#region src/reolink-gateway-device.ts
|
|
218077
|
+
var reolinkGatewayDeviceSchema = object({
|
|
218078
|
+
integrationId: string().min(1).describe("Integration (hub session) the device belongs to"),
|
|
218079
|
+
/** The adoption candidate id (`<integration>:gateway`). */
|
|
218080
|
+
nativeId: string().min(1)
|
|
218081
|
+
});
|
|
218082
|
+
/**
|
|
218083
|
+
* The physical NVR / Home Hub, as a {@link DeviceType.Gateway}. Its connection and password
|
|
218084
|
+
* live on the INTEGRATION; this device is the box's presence in the device list, and carries
|
|
218085
|
+
* the actions the library really supports through existing caps:
|
|
218086
|
+
*
|
|
218087
|
+
* - `reboot` (Baichuan `Reboot`, no channel), advertised as {@link DeviceFeature.Rebootable};
|
|
218088
|
+
* - `device-admin-link`, the unit's own web page.
|
|
218089
|
+
*
|
|
218090
|
+
* Cameras are NOT children of it: they are adopted as their own top-level devices of the
|
|
218091
|
+
* same integration. The addon flips `online` when the hub socket changes.
|
|
218092
|
+
*/
|
|
218093
|
+
var ReolinkGatewayDevice = class extends BaseDevice {
|
|
218094
|
+
features = [DeviceFeature.Rebootable];
|
|
218095
|
+
integrationId;
|
|
218096
|
+
constructor(ctx) {
|
|
218097
|
+
const cfg = reolinkGatewayDeviceSchema.parse(ctx.persistedConfig ?? {});
|
|
218098
|
+
super(ctx, reolinkGatewayDeviceSchema, { type: DeviceType.Gateway });
|
|
218099
|
+
this.integrationId = cfg.integrationId;
|
|
218100
|
+
this.online = reolinkHubSessions.get(this.integrationId)?.isOnline === true;
|
|
218101
|
+
this.registerRebootCap();
|
|
218102
|
+
this.registerAdminLinkCap();
|
|
218103
|
+
this.updateSourceInfo({
|
|
218104
|
+
id: cfg.nativeId,
|
|
218105
|
+
system: "reolink"
|
|
218106
|
+
});
|
|
218107
|
+
}
|
|
218108
|
+
registerRebootCap() {
|
|
218109
|
+
this.ctx.registerNativeCap(rebootCapability, { reboot: async ({ deviceId }) => {
|
|
218110
|
+
if (deviceId !== this.id) throw new Error(`ReolinkGatewayDevice: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
218111
|
+
const session = reolinkHubSessions.get(this.integrationId);
|
|
218112
|
+
if (session === null) {
|
|
218113
|
+
this.ctx.logger.warn("Reolink hub reboot refused: no live session", { tags: { deviceId: this.id } });
|
|
218114
|
+
throw new Error("The hub session is down, so it cannot be told to reboot.");
|
|
218115
|
+
}
|
|
218116
|
+
await session.reboot();
|
|
218117
|
+
this.ctx.logger.info("Reolink hub reboot requested", { tags: { deviceId: this.id } });
|
|
218118
|
+
return { success: true };
|
|
218119
|
+
} });
|
|
218120
|
+
}
|
|
218121
|
+
/**
|
|
218122
|
+
* The hub's web interface, on the hub host. The password is NOT placed in the URL; the page
|
|
218123
|
+
* asks for it.
|
|
218124
|
+
*/
|
|
218125
|
+
registerAdminLinkCap() {
|
|
218126
|
+
this.ctx.registerNativeCap(deviceAdminLinkCapability, { getAdminLink: async ({ deviceId }) => {
|
|
218127
|
+
if (deviceId !== this.id) throw new Error(`ReolinkGatewayDevice: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
218128
|
+
const host = reolinkHubSessions.get(this.integrationId)?.relayEndpoint().host;
|
|
218129
|
+
if (host === void 0) {
|
|
218130
|
+
this.ctx.logger.debug("reolink admin link unavailable: integration not running", { tags: { deviceId: this.id } });
|
|
218131
|
+
return null;
|
|
218132
|
+
}
|
|
218133
|
+
const built = buildDeviceAdminUrl({
|
|
218134
|
+
host,
|
|
218135
|
+
port: null,
|
|
218136
|
+
scheme: "https",
|
|
218137
|
+
path: "/"
|
|
218138
|
+
});
|
|
218139
|
+
if (!built.ok) {
|
|
218140
|
+
this.ctx.logger.debug("reolink admin link unavailable", {
|
|
218141
|
+
tags: { deviceId: this.id },
|
|
218142
|
+
meta: { reason: built.reason }
|
|
218143
|
+
});
|
|
218144
|
+
return null;
|
|
218145
|
+
}
|
|
218146
|
+
return {
|
|
218147
|
+
url: built.url,
|
|
218148
|
+
label: "Reolink web interface",
|
|
218149
|
+
target: "integration",
|
|
218150
|
+
host
|
|
218151
|
+
};
|
|
218152
|
+
} });
|
|
218153
|
+
}
|
|
218154
|
+
};
|
|
218155
|
+
//#endregion
|
|
218027
218156
|
//#region src/addon.ts
|
|
218028
218157
|
/**
|
|
218029
218158
|
* Adapt a camstack scoped logger to the lib's `Logger` shape (just
|
|
@@ -218187,7 +218316,10 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
218187
218316
|
secretStatePaths = ["emailPushAuthPassword"];
|
|
218188
218317
|
addonId = REOLINK_ADDON_ID;
|
|
218189
218318
|
providerName = "Reolink";
|
|
218190
|
-
deviceClasses = {
|
|
218319
|
+
deviceClasses = {
|
|
218320
|
+
[DeviceType.Camera]: ReolinkCamera,
|
|
218321
|
+
[DeviceType.Gateway]: ReolinkGatewayDevice
|
|
218322
|
+
};
|
|
218191
218323
|
/**
|
|
218192
218324
|
* Short-lived cache that bridges `testCreationField` and
|
|
218193
218325
|
* `onCreateDevice` — Slice 15 socket reuse. Both code paths key off
|
|
@@ -218402,6 +218534,15 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
218402
218534
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
|
|
218403
218535
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
|
|
218404
218536
|
}
|
|
218537
|
+
/** The hub box follows its integration's socket. */
|
|
218538
|
+
markGatewaysOnline(integrationId, online) {
|
|
218539
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
218540
|
+
if (!reg) return;
|
|
218541
|
+
for (const d of reg.getAllForAddon(this.addonId)) {
|
|
218542
|
+
if (!(d instanceof ReolinkGatewayDevice) || d.integrationId !== integrationId) continue;
|
|
218543
|
+
if (d.online !== online) d.markOnline(online);
|
|
218544
|
+
}
|
|
218545
|
+
}
|
|
218405
218546
|
buildAdoptionProvider() {
|
|
218406
218547
|
return buildReolinkAdoptionProvider({
|
|
218407
218548
|
getSession: (integrationId) => this.hubManager?.get(integrationId) ?? null,
|
|
@@ -218447,6 +218588,29 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
218447
218588
|
accessoryDeviceIds: children.map((c) => c.id)
|
|
218448
218589
|
};
|
|
218449
218590
|
},
|
|
218591
|
+
listAdoptedGateways: async () => {
|
|
218592
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
218593
|
+
if (!reg) return [];
|
|
218594
|
+
const out = [];
|
|
218595
|
+
for (const d of reg.getAllForAddon(this.addonId)) if (d instanceof ReolinkGatewayDevice) out.push({
|
|
218596
|
+
id: d.id,
|
|
218597
|
+
integrationId: d.integrationId
|
|
218598
|
+
});
|
|
218599
|
+
return out;
|
|
218600
|
+
},
|
|
218601
|
+
adoptGateway: async ({ integrationId, name }) => {
|
|
218602
|
+
const devices = this.ctx.kernel.devices;
|
|
218603
|
+
if (!devices) throw new Error("reolink adopt: kernel.devices unavailable");
|
|
218604
|
+
const nativeId = hubGatewayNativeId(integrationId);
|
|
218605
|
+
return { deviceId: (await devices.create(`${integrationId}-${nativeId}`, ReolinkGatewayDevice, {
|
|
218606
|
+
integrationId,
|
|
218607
|
+
nativeId
|
|
218608
|
+
}, null, {
|
|
218609
|
+
type: DeviceType.Gateway,
|
|
218610
|
+
name,
|
|
218611
|
+
integrationId
|
|
218612
|
+
})).id };
|
|
218613
|
+
},
|
|
218450
218614
|
removeDevice: async (id) => {
|
|
218451
218615
|
await this.ctx.kernel.devices?.remove(id);
|
|
218452
218616
|
},
|
|
@@ -218652,7 +218816,10 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
218652
218816
|
this.listReolinkCameras();
|
|
218653
218817
|
this.hubManager = createReolinkHubManager({
|
|
218654
218818
|
logger: this.ctx.logger,
|
|
218655
|
-
onConnectionChange: (integrationId, online) =>
|
|
218819
|
+
onConnectionChange: (integrationId, online) => {
|
|
218820
|
+
this.ctx.logger.info(`Reolink NVR socket ${online ? "up" : "down"}`, { meta: { integrationId } });
|
|
218821
|
+
this.markGatewaysOnline(integrationId, online);
|
|
218822
|
+
}
|
|
218656
218823
|
}, reolinkHubSessions);
|
|
218657
218824
|
await this.reconcileIntegrations();
|
|
218658
218825
|
this.subscribeIntegrationLifecycle();
|