@camstack/addon-provider-reolink 1.1.10 → 1.1.12
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 +748 -7
- package/dist/addon.mjs +749 -8
- package/package.json +4 -2
package/dist/addon.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { format, promisify } from "util";
|
|
|
14
14
|
import * as dgram2 from "dgram";
|
|
15
15
|
import dgram from "dgram";
|
|
16
16
|
import dns from "dns/promises";
|
|
17
|
-
import { networkInterfaces } from "os";
|
|
17
|
+
import { networkInterfaces, platform } from "os";
|
|
18
18
|
import { setInterval as setInterval$1 } from "timers";
|
|
19
19
|
import * as net2 from "net";
|
|
20
20
|
import netImpl from "net";
|
|
@@ -4650,7 +4650,7 @@ function _instanceof(cls, params = {}) {
|
|
|
4650
4650
|
return inst;
|
|
4651
4651
|
}
|
|
4652
4652
|
//#endregion
|
|
4653
|
-
//#region ../types/dist/sleep-
|
|
4653
|
+
//#region ../types/dist/sleep-C2M2zF7x.mjs
|
|
4654
4654
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4655
4655
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4656
4656
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -6218,6 +6218,12 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
|
6218
6218
|
DeviceType["Switch"] = "switch";
|
|
6219
6219
|
DeviceType["Sensor"] = "sensor";
|
|
6220
6220
|
DeviceType["Thermostat"] = "thermostat";
|
|
6221
|
+
/** Air-conditioner / heat-pump climate device (HVAC) — shares the
|
|
6222
|
+
* `climate-control` cap surface with `Thermostat` but renders a
|
|
6223
|
+
* dedicated AC-appropriate control UI (mode chips, fan speed,
|
|
6224
|
+
* independent vertical/horizontal swing). Sources: native Gree, and
|
|
6225
|
+
* reusable by other AC integrations. */
|
|
6226
|
+
DeviceType["Climate"] = "climate";
|
|
6221
6227
|
DeviceType["Button"] = "button";
|
|
6222
6228
|
/** Generic stateless event emitter — carries a device's EXACT declared
|
|
6223
6229
|
* event vocabulary verbatim (no normalization). Installed with the
|
|
@@ -9239,7 +9245,7 @@ var climateControlCapability = {
|
|
|
9239
9245
|
scope: "device",
|
|
9240
9246
|
deviceNative: true,
|
|
9241
9247
|
mode: "singleton",
|
|
9242
|
-
deviceTypes: [DeviceType.Thermostat],
|
|
9248
|
+
deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
|
|
9243
9249
|
methods: {
|
|
9244
9250
|
setMode: method(object({
|
|
9245
9251
|
deviceId: number().int().nonnegative(),
|
|
@@ -13838,10 +13844,30 @@ var deviceProviderCapability = {
|
|
|
13838
13844
|
type: string()
|
|
13839
13845
|
}))),
|
|
13840
13846
|
supportsDiscovery: method(object({}), boolean()),
|
|
13841
|
-
|
|
13847
|
+
/**
|
|
13848
|
+
* Run a network scan. `params` carries optional provider-specific scan
|
|
13849
|
+
* inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
|
|
13850
|
+
* shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
|
|
13851
|
+
* (provider uses its local-network default).
|
|
13852
|
+
*/
|
|
13853
|
+
discoverDevices: method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
|
|
13842
13854
|
kind: "mutation",
|
|
13843
13855
|
auth: "admin"
|
|
13844
13856
|
}),
|
|
13857
|
+
/**
|
|
13858
|
+
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
13859
|
+
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
13860
|
+
* `null` when the provider takes no extra scan params — the generic
|
|
13861
|
+
* aggregated scan never renders this; the per-integration scan does.
|
|
13862
|
+
*/
|
|
13863
|
+
getDiscoveryParamsSchema: method(object({}), CreationSchemaOutputSchema),
|
|
13864
|
+
/**
|
|
13865
|
+
* The DeviceType this provider creates via manual add (Camera for
|
|
13866
|
+
* Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
|
|
13867
|
+
* provider does not support manual creation. Lets the Add-Device dialog
|
|
13868
|
+
* pick the right type instead of assuming Camera.
|
|
13869
|
+
*/
|
|
13870
|
+
getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
|
|
13845
13871
|
adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
13846
13872
|
kind: "mutation",
|
|
13847
13873
|
auth: "admin"
|
|
@@ -13965,9 +13991,23 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
13965
13991
|
async supportsDiscovery() {
|
|
13966
13992
|
return false;
|
|
13967
13993
|
}
|
|
13968
|
-
async discoverDevices() {
|
|
13994
|
+
async discoverDevices(_input) {
|
|
13969
13995
|
return [];
|
|
13970
13996
|
}
|
|
13997
|
+
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
13998
|
+
* Null = no extra params. Override in providers that support scoped scans. */
|
|
13999
|
+
async getDiscoveryParamsSchema() {
|
|
14000
|
+
return null;
|
|
14001
|
+
}
|
|
14002
|
+
/**
|
|
14003
|
+
* The DeviceType this provider creates via manual add — derived from the
|
|
14004
|
+
* `deviceClasses` map (first registered type). `null` when manual creation is
|
|
14005
|
+
* unsupported. Lets the Add-Device dialog pick the right type per provider.
|
|
14006
|
+
*/
|
|
14007
|
+
async getManualCreationType() {
|
|
14008
|
+
if (!await this.supportsManualCreation()) return { deviceType: null };
|
|
14009
|
+
return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
|
|
14010
|
+
}
|
|
13971
14011
|
async adoptDiscoveredDevice(_input) {
|
|
13972
14012
|
throw new Error(`${this.providerName} provider does not support discovery-based adoption`);
|
|
13973
14013
|
}
|
|
@@ -15824,7 +15864,10 @@ method(object({
|
|
|
15824
15864
|
}), FieldProbeResultSchema, {
|
|
15825
15865
|
kind: "mutation",
|
|
15826
15866
|
auth: "admin"
|
|
15827
|
-
}), method(
|
|
15867
|
+
}), method(object({
|
|
15868
|
+
addonId: string(),
|
|
15869
|
+
integrationId: string()
|
|
15870
|
+
}), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
|
|
15828
15871
|
addonId: string(),
|
|
15829
15872
|
integrationId: string()
|
|
15830
15873
|
}), AdoptionStatusSchema, {
|
|
@@ -15839,7 +15882,24 @@ method(object({
|
|
|
15839
15882
|
}), method(ResyncInputSchema, ResyncResultSchema, {
|
|
15840
15883
|
kind: "mutation",
|
|
15841
15884
|
auth: "admin"
|
|
15885
|
+
}), method(object({}), object({ providers: array(object({
|
|
15886
|
+
addonId: string(),
|
|
15887
|
+
label: string()
|
|
15888
|
+
})).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
|
|
15889
|
+
addonId: string(),
|
|
15890
|
+
label: string(),
|
|
15891
|
+
candidates: array(DiscoveryCandidateSchema).readonly(),
|
|
15892
|
+
error: string().nullable()
|
|
15893
|
+
})).readonly() }), {
|
|
15894
|
+
kind: "mutation",
|
|
15895
|
+
auth: "admin"
|
|
15842
15896
|
}), method(object({
|
|
15897
|
+
addonId: string(),
|
|
15898
|
+
params: record(string(), unknown()).optional()
|
|
15899
|
+
}), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
|
|
15900
|
+
kind: "mutation",
|
|
15901
|
+
auth: "admin"
|
|
15902
|
+
}), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
|
|
15843
15903
|
deviceId: number(),
|
|
15844
15904
|
key: string(),
|
|
15845
15905
|
value: unknown()
|
|
@@ -21334,6 +21394,12 @@ Object.freeze({
|
|
|
21334
21394
|
addonId: null,
|
|
21335
21395
|
access: "create"
|
|
21336
21396
|
},
|
|
21397
|
+
"deviceManager.adoptionListCandidateFilters": {
|
|
21398
|
+
capName: "device-manager",
|
|
21399
|
+
capScope: "system",
|
|
21400
|
+
addonId: null,
|
|
21401
|
+
access: "view"
|
|
21402
|
+
},
|
|
21337
21403
|
"deviceManager.adoptionListCandidates": {
|
|
21338
21404
|
capName: "device-manager",
|
|
21339
21405
|
capScope: "system",
|
|
@@ -21382,12 +21448,30 @@ Object.freeze({
|
|
|
21382
21448
|
addonId: null,
|
|
21383
21449
|
access: "create"
|
|
21384
21450
|
},
|
|
21451
|
+
"deviceManager.discoverAllProviders": {
|
|
21452
|
+
capName: "device-manager",
|
|
21453
|
+
capScope: "system",
|
|
21454
|
+
addonId: null,
|
|
21455
|
+
access: "create"
|
|
21456
|
+
},
|
|
21385
21457
|
"deviceManager.discoverDevices": {
|
|
21386
21458
|
capName: "device-manager",
|
|
21387
21459
|
capScope: "system",
|
|
21388
21460
|
addonId: null,
|
|
21389
21461
|
access: "create"
|
|
21390
21462
|
},
|
|
21463
|
+
"deviceManager.discoverProvider": {
|
|
21464
|
+
capName: "device-manager",
|
|
21465
|
+
capScope: "system",
|
|
21466
|
+
addonId: null,
|
|
21467
|
+
access: "create"
|
|
21468
|
+
},
|
|
21469
|
+
"deviceManager.discoveryProviders": {
|
|
21470
|
+
capName: "device-manager",
|
|
21471
|
+
capScope: "system",
|
|
21472
|
+
addonId: null,
|
|
21473
|
+
access: "view"
|
|
21474
|
+
},
|
|
21391
21475
|
"deviceManager.enable": {
|
|
21392
21476
|
capName: "device-manager",
|
|
21393
21477
|
capScope: "system",
|
|
@@ -21538,6 +21622,18 @@ Object.freeze({
|
|
|
21538
21622
|
addonId: null,
|
|
21539
21623
|
access: "create"
|
|
21540
21624
|
},
|
|
21625
|
+
"deviceManager.providerCreationType": {
|
|
21626
|
+
capName: "device-manager",
|
|
21627
|
+
capScope: "system",
|
|
21628
|
+
addonId: null,
|
|
21629
|
+
access: "view"
|
|
21630
|
+
},
|
|
21631
|
+
"deviceManager.providerDiscoveryParamsSchema": {
|
|
21632
|
+
capName: "device-manager",
|
|
21633
|
+
capScope: "system",
|
|
21634
|
+
addonId: null,
|
|
21635
|
+
access: "view"
|
|
21636
|
+
},
|
|
21541
21637
|
"deviceManager.registerDevice": {
|
|
21542
21638
|
capName: "device-manager",
|
|
21543
21639
|
capScope: "system",
|
|
@@ -21754,6 +21850,18 @@ Object.freeze({
|
|
|
21754
21850
|
addonId: null,
|
|
21755
21851
|
access: "view"
|
|
21756
21852
|
},
|
|
21853
|
+
"deviceProvider.getDiscoveryParamsSchema": {
|
|
21854
|
+
capName: "device-provider",
|
|
21855
|
+
capScope: "system",
|
|
21856
|
+
addonId: null,
|
|
21857
|
+
access: "view"
|
|
21858
|
+
},
|
|
21859
|
+
"deviceProvider.getManualCreationType": {
|
|
21860
|
+
capName: "device-provider",
|
|
21861
|
+
capScope: "system",
|
|
21862
|
+
addonId: null,
|
|
21863
|
+
access: "view"
|
|
21864
|
+
},
|
|
21757
21865
|
"deviceProvider.getStatus": {
|
|
21758
21866
|
capName: "device-provider",
|
|
21759
21867
|
capScope: "system",
|
|
@@ -164351,7 +164459,7 @@ ${scheduleItems}
|
|
|
164351
164459
|
}), chimeId);
|
|
164352
164460
|
}
|
|
164353
164461
|
};
|
|
164354
|
-
promisify(execFile);
|
|
164462
|
+
var execFileAsync = promisify(execFile);
|
|
164355
164463
|
async function discoverViaUdpDirect(host, options) {
|
|
164356
164464
|
if (!options.enableUdpDiscovery) return [];
|
|
164357
164465
|
const logger = options.logger;
|
|
@@ -164421,6 +164529,170 @@ async function discoverViaUdpDirect(host, options) {
|
|
|
164421
164529
|
});
|
|
164422
164530
|
});
|
|
164423
164531
|
}
|
|
164532
|
+
function getLocalNetworks() {
|
|
164533
|
+
const networks = [];
|
|
164534
|
+
const interfaces = networkInterfaces();
|
|
164535
|
+
for (const ifaceName of Object.keys(interfaces)) {
|
|
164536
|
+
const iface = interfaces[ifaceName];
|
|
164537
|
+
if (!iface) continue;
|
|
164538
|
+
for (const addr of iface) {
|
|
164539
|
+
if (addr.internal || addr.family !== "IPv4" || !addr.netmask) continue;
|
|
164540
|
+
addr.address.split(".").map(Number);
|
|
164541
|
+
const maskParts = addr.netmask.split(".").map(Number);
|
|
164542
|
+
let cidr = 0;
|
|
164543
|
+
for (let i = 0; i < 4; i++) {
|
|
164544
|
+
const maskValue = maskParts[i];
|
|
164545
|
+
if (maskValue === void 0 || !Number.isFinite(maskValue)) break;
|
|
164546
|
+
if (maskValue === 255) cidr += 8;
|
|
164547
|
+
else if (maskValue === 0) break;
|
|
164548
|
+
else {
|
|
164549
|
+
let bits = 0;
|
|
164550
|
+
let m = maskValue;
|
|
164551
|
+
while (m > 0) {
|
|
164552
|
+
if (m & 1) bits++;
|
|
164553
|
+
m = m >> 1;
|
|
164554
|
+
}
|
|
164555
|
+
cidr += bits;
|
|
164556
|
+
break;
|
|
164557
|
+
}
|
|
164558
|
+
}
|
|
164559
|
+
const networkCidr = `${addr.address.split(".").slice(0, 3).join(".")}.0/${cidr}`;
|
|
164560
|
+
if (!networks.includes(networkCidr)) networks.push(networkCidr);
|
|
164561
|
+
}
|
|
164562
|
+
}
|
|
164563
|
+
return networks;
|
|
164564
|
+
}
|
|
164565
|
+
function parseCidr(cidr) {
|
|
164566
|
+
const parts = cidr.split("/");
|
|
164567
|
+
const network = parts[0];
|
|
164568
|
+
const prefixStr = parts[1];
|
|
164569
|
+
if (!network) return null;
|
|
164570
|
+
const prefix = Number.parseInt(prefixStr ?? "24", 10);
|
|
164571
|
+
if (!Number.isFinite(prefix) || prefix < 0 || prefix > 32) return null;
|
|
164572
|
+
const ipParts = network.split(".").map(Number);
|
|
164573
|
+
if (ipParts.length !== 4 || ipParts.some((p) => !Number.isFinite(p) || p < 0 || p > 255)) return null;
|
|
164574
|
+
const networkBits = prefix;
|
|
164575
|
+
const hostBits = 32 - networkBits;
|
|
164576
|
+
let networkAddr = 0;
|
|
164577
|
+
for (let i = 0; i < 4; i++) {
|
|
164578
|
+
const part = ipParts[i];
|
|
164579
|
+
if (part === void 0 || !Number.isFinite(part)) return null;
|
|
164580
|
+
networkAddr = networkAddr << 8 | part & 255;
|
|
164581
|
+
}
|
|
164582
|
+
const mask = (1 << networkBits) - 1 << hostBits;
|
|
164583
|
+
networkAddr &= mask;
|
|
164584
|
+
const hostCount = 1 << hostBits;
|
|
164585
|
+
const start = prefix >= 24 ? networkAddr + 1 : networkAddr;
|
|
164586
|
+
const end = prefix >= 24 ? networkAddr + hostCount - 2 : networkAddr + hostCount - 1;
|
|
164587
|
+
return {
|
|
164588
|
+
start,
|
|
164589
|
+
end,
|
|
164590
|
+
count: end - start + 1
|
|
164591
|
+
};
|
|
164592
|
+
}
|
|
164593
|
+
function ipNumberToString(ip) {
|
|
164594
|
+
return `${ip >>> 24 & 255}.${ip >>> 16 & 255}.${ip >>> 8 & 255}.${ip & 255}`;
|
|
164595
|
+
}
|
|
164596
|
+
async function probeHttpDevice(ip, port, options) {
|
|
164597
|
+
const { username, password, timeoutMs, logger, useHttps } = options;
|
|
164598
|
+
try {
|
|
164599
|
+
const cgi = new ReolinkCgiApi({
|
|
164600
|
+
host: ip,
|
|
164601
|
+
port,
|
|
164602
|
+
useHttps: useHttps ?? false,
|
|
164603
|
+
username: username ?? "admin",
|
|
164604
|
+
password: password ?? "",
|
|
164605
|
+
timeoutMs
|
|
164606
|
+
});
|
|
164607
|
+
try {
|
|
164608
|
+
const info = await cgi.getInfo();
|
|
164609
|
+
if (info?.type) {
|
|
164610
|
+
logger?.log?.(`[Discovery] Found Reolink device at ${ip}:${port} (${useHttps ? "HTTPS" : "HTTP"}) - ${info.type}`);
|
|
164611
|
+
const result = {
|
|
164612
|
+
host: ip,
|
|
164613
|
+
discoveryMethod: "http_probe",
|
|
164614
|
+
supportsHttps: useHttps ?? false,
|
|
164615
|
+
httpAccessible: !useHttps
|
|
164616
|
+
};
|
|
164617
|
+
if (port !== void 0) if (useHttps) result.httpsPort = port;
|
|
164618
|
+
else result.httpPort = port;
|
|
164619
|
+
if (info.type) result.model = info.type.trim();
|
|
164620
|
+
if (info.name) result.name = info.name.trim();
|
|
164621
|
+
if (info.firmwareVersion) result.firmwareVersion = info.firmwareVersion.trim();
|
|
164622
|
+
return result;
|
|
164623
|
+
}
|
|
164624
|
+
} catch {
|
|
164625
|
+
if (username && password) try {
|
|
164626
|
+
await cgi.login();
|
|
164627
|
+
const info = await cgi.getInfo();
|
|
164628
|
+
if (info?.type) {
|
|
164629
|
+
logger?.log?.(`[Discovery] Found authenticated Reolink device at ${ip}:${port} (${useHttps ? "HTTPS" : "HTTP"}) - ${info.type}`);
|
|
164630
|
+
const result = {
|
|
164631
|
+
host: ip,
|
|
164632
|
+
discoveryMethod: "http_probe",
|
|
164633
|
+
supportsHttps: useHttps ?? false,
|
|
164634
|
+
httpAccessible: !useHttps
|
|
164635
|
+
};
|
|
164636
|
+
if (port !== void 0) if (useHttps) result.httpsPort = port;
|
|
164637
|
+
else result.httpPort = port;
|
|
164638
|
+
if (info.type) result.model = info.type.trim();
|
|
164639
|
+
if (info.name) result.name = info.name.trim();
|
|
164640
|
+
if (info.firmwareVersion) result.firmwareVersion = info.firmwareVersion.trim();
|
|
164641
|
+
return result;
|
|
164642
|
+
}
|
|
164643
|
+
} catch {}
|
|
164644
|
+
}
|
|
164645
|
+
} catch (err) {
|
|
164646
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
164647
|
+
if (!msg.includes("ECONNREFUSED") && !msg.includes("ETIMEDOUT")) logger?.warn?.(`[Discovery] Error probing ${ip}:${port}: ${msg}`);
|
|
164648
|
+
}
|
|
164649
|
+
return null;
|
|
164650
|
+
}
|
|
164651
|
+
async function discoverViaHttpScan(options) {
|
|
164652
|
+
if (!options.enableHttpScanning) return [];
|
|
164653
|
+
const logger = options.logger;
|
|
164654
|
+
const networkCidr = options.networkCidr ?? getLocalNetworks()[0];
|
|
164655
|
+
const httpPorts = options.httpPorts ?? [80, 443];
|
|
164656
|
+
const timeoutMs = options.httpProbeTimeoutMs ?? 2e3;
|
|
164657
|
+
const maxConcurrent = options.maxConcurrentProbes ?? 50;
|
|
164658
|
+
if (!networkCidr) {
|
|
164659
|
+
logger?.warn?.("[Discovery] No network CIDR available for HTTP scanning");
|
|
164660
|
+
return [];
|
|
164661
|
+
}
|
|
164662
|
+
logger?.log?.(`[Discovery] Starting HTTP scan on network ${networkCidr}...`);
|
|
164663
|
+
const ipRange = parseCidr(networkCidr);
|
|
164664
|
+
if (!ipRange) {
|
|
164665
|
+
logger?.warn?.(`[Discovery] Invalid CIDR: ${networkCidr}`);
|
|
164666
|
+
return [];
|
|
164667
|
+
}
|
|
164668
|
+
const discovered = [];
|
|
164669
|
+
const ipAddresses = [];
|
|
164670
|
+
for (let ipNum = ipRange.start; ipNum <= ipRange.end && ipNum <= ipRange.start + 254; ipNum++) {
|
|
164671
|
+
const ip = ipNumberToString(ipNum);
|
|
164672
|
+
for (const port of httpPorts) ipAddresses.push({
|
|
164673
|
+
ip,
|
|
164674
|
+
port,
|
|
164675
|
+
useHttps: port === 443
|
|
164676
|
+
});
|
|
164677
|
+
}
|
|
164678
|
+
logger?.log?.(`[Discovery] Scanning ${ipAddresses.length} IP:port combinations...`);
|
|
164679
|
+
for (let i = 0; i < ipAddresses.length; i += maxConcurrent) {
|
|
164680
|
+
const batch = ipAddresses.slice(i, i + maxConcurrent);
|
|
164681
|
+
const batchResults = await Promise.allSettled(batch.map(({ ip, port, useHttps }) => {
|
|
164682
|
+
const probeOptions = {
|
|
164683
|
+
timeoutMs,
|
|
164684
|
+
useHttps
|
|
164685
|
+
};
|
|
164686
|
+
if (options.username !== void 0) probeOptions.username = options.username;
|
|
164687
|
+
if (options.password !== void 0) probeOptions.password = options.password;
|
|
164688
|
+
if (logger !== void 0) probeOptions.logger = logger;
|
|
164689
|
+
return probeHttpDevice(ip, port, probeOptions);
|
|
164690
|
+
}));
|
|
164691
|
+
for (const result of batchResults) if (result.status === "fulfilled" && result.value) discovered.push(result.value);
|
|
164692
|
+
}
|
|
164693
|
+
logger?.log?.(`[Discovery] HTTP scan complete. Found ${discovered.length} device(s).`);
|
|
164694
|
+
return discovered;
|
|
164695
|
+
}
|
|
164424
164696
|
async function discoverViaUdpBroadcast(options) {
|
|
164425
164697
|
if (!options.enableUdpDiscovery) return [];
|
|
164426
164698
|
const logger = options.logger;
|
|
@@ -164508,6 +164780,359 @@ async function discoverViaUdpBroadcast(options) {
|
|
|
164508
164780
|
});
|
|
164509
164781
|
});
|
|
164510
164782
|
}
|
|
164783
|
+
var REOLINK_MAC_PREFIXES = [
|
|
164784
|
+
"EC:71:DB",
|
|
164785
|
+
"2C:1B:3A",
|
|
164786
|
+
"18:2C:65",
|
|
164787
|
+
"DC:E5:37",
|
|
164788
|
+
"9C:8E:CD",
|
|
164789
|
+
"B4:4B:D6",
|
|
164790
|
+
"E4:3D:1A"
|
|
164791
|
+
];
|
|
164792
|
+
async function discoverViaArpTable(options) {
|
|
164793
|
+
if (!options.enableArpLookup) return [];
|
|
164794
|
+
const logger = options.logger;
|
|
164795
|
+
logger?.log?.("[Discovery] Starting ARP table lookup for Reolink MAC prefix...");
|
|
164796
|
+
const discovered = [];
|
|
164797
|
+
try {
|
|
164798
|
+
let entries = [];
|
|
164799
|
+
if (platform() === "linux") try {
|
|
164800
|
+
const { readFile } = await import("fs/promises");
|
|
164801
|
+
const content = await readFile("/proc/net/arp", "utf8");
|
|
164802
|
+
for (const line of content.split("\n").slice(1)) {
|
|
164803
|
+
const parts = line.trim().split(/\s+/);
|
|
164804
|
+
if (parts.length >= 4 && parts[0] && parts[3] && parts[3] !== "00:00:00:00:00:00") entries.push({
|
|
164805
|
+
ip: parts[0],
|
|
164806
|
+
mac: parts[3].toUpperCase()
|
|
164807
|
+
});
|
|
164808
|
+
}
|
|
164809
|
+
} catch {
|
|
164810
|
+
const { stdout } = await runArpCommand();
|
|
164811
|
+
entries = parseArpOutput(stdout);
|
|
164812
|
+
}
|
|
164813
|
+
else {
|
|
164814
|
+
const { stdout } = await runArpCommand();
|
|
164815
|
+
entries = parseArpOutput(stdout);
|
|
164816
|
+
}
|
|
164817
|
+
logger?.log?.(`[Discovery] ARP table has ${entries.length} entries`);
|
|
164818
|
+
for (const { ip, mac } of entries) if (REOLINK_MAC_PREFIXES.some((prefix) => mac.startsWith(prefix))) {
|
|
164819
|
+
logger?.log?.(`[Discovery] Found Reolink device via ARP: ${ip} (MAC: ${mac})`);
|
|
164820
|
+
discovered.push({
|
|
164821
|
+
host: ip,
|
|
164822
|
+
discoveryMethod: "arp"
|
|
164823
|
+
});
|
|
164824
|
+
}
|
|
164825
|
+
} catch (err) {
|
|
164826
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
164827
|
+
logger?.warn?.(`[Discovery] ARP table lookup failed: ${msg}`);
|
|
164828
|
+
}
|
|
164829
|
+
logger?.log?.(`[Discovery] ARP lookup complete. Found ${discovered.length} device(s).`);
|
|
164830
|
+
return discovered;
|
|
164831
|
+
}
|
|
164832
|
+
async function runArpCommand() {
|
|
164833
|
+
for (const arpPath of [
|
|
164834
|
+
"/usr/sbin/arp",
|
|
164835
|
+
"/sbin/arp",
|
|
164836
|
+
"/usr/bin/arp",
|
|
164837
|
+
"arp"
|
|
164838
|
+
]) try {
|
|
164839
|
+
return await execFileAsync(arpPath, ["-an"], { timeout: 5e3 });
|
|
164840
|
+
} catch {}
|
|
164841
|
+
throw new Error("arp command not found");
|
|
164842
|
+
}
|
|
164843
|
+
function parseArpOutput(stdout) {
|
|
164844
|
+
const results = [];
|
|
164845
|
+
for (const line of stdout.split("\n")) {
|
|
164846
|
+
const match = /\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([0-9a-fA-F:]+)/i.exec(line);
|
|
164847
|
+
if (match && match[1] && match[2] && match[2] !== "(incomplete)") results.push({
|
|
164848
|
+
ip: match[1],
|
|
164849
|
+
mac: match[2].toUpperCase()
|
|
164850
|
+
});
|
|
164851
|
+
}
|
|
164852
|
+
return results;
|
|
164853
|
+
}
|
|
164854
|
+
async function discoverViaDhcpListener(options) {
|
|
164855
|
+
if (!options.enableDhcpListener) return [];
|
|
164856
|
+
const logger = options.logger;
|
|
164857
|
+
const timeoutMs = options.dhcpListenerTimeoutMs ?? 1e4;
|
|
164858
|
+
logger?.log?.(`[Discovery] Starting passive DHCP listener (${timeoutMs}ms)...`);
|
|
164859
|
+
const discovered = /* @__PURE__ */ new Map();
|
|
164860
|
+
return new Promise((resolve) => {
|
|
164861
|
+
let socket;
|
|
164862
|
+
let timeout;
|
|
164863
|
+
try {
|
|
164864
|
+
socket = dgram.createSocket({
|
|
164865
|
+
type: "udp4",
|
|
164866
|
+
reuseAddr: true
|
|
164867
|
+
});
|
|
164868
|
+
} catch (err) {
|
|
164869
|
+
logger?.warn?.(`[Discovery] DHCP: failed to create socket: ${err instanceof Error ? err.message : String(err)}`);
|
|
164870
|
+
resolve([]);
|
|
164871
|
+
return;
|
|
164872
|
+
}
|
|
164873
|
+
socket.on("message", (msg) => {
|
|
164874
|
+
try {
|
|
164875
|
+
if (msg.length < 240) return;
|
|
164876
|
+
const op = msg[0];
|
|
164877
|
+
if (msg[2] !== 6) return;
|
|
164878
|
+
const mac = [
|
|
164879
|
+
msg[28]?.toString(16).padStart(2, "0"),
|
|
164880
|
+
msg[29]?.toString(16).padStart(2, "0"),
|
|
164881
|
+
msg[30]?.toString(16).padStart(2, "0"),
|
|
164882
|
+
msg[31]?.toString(16).padStart(2, "0"),
|
|
164883
|
+
msg[32]?.toString(16).padStart(2, "0"),
|
|
164884
|
+
msg[33]?.toString(16).padStart(2, "0")
|
|
164885
|
+
].join(":").toUpperCase();
|
|
164886
|
+
const isReolinkMac = REOLINK_MAC_PREFIXES.some((p) => mac.startsWith(p));
|
|
164887
|
+
let hostname = "";
|
|
164888
|
+
let i = 240;
|
|
164889
|
+
while (i < msg.length - 1) {
|
|
164890
|
+
const optType = msg[i];
|
|
164891
|
+
if (optType === 255) break;
|
|
164892
|
+
if (optType === 0) {
|
|
164893
|
+
i++;
|
|
164894
|
+
continue;
|
|
164895
|
+
}
|
|
164896
|
+
const optLen = msg[i + 1] ?? 0;
|
|
164897
|
+
if (optType === 12 && optLen > 0) hostname = msg.subarray(i + 2, i + 2 + optLen).toString("ascii").toLowerCase();
|
|
164898
|
+
i += 2 + optLen;
|
|
164899
|
+
}
|
|
164900
|
+
const isReolinkHostname = hostname.startsWith("reolink");
|
|
164901
|
+
if (!isReolinkMac && !isReolinkHostname) return;
|
|
164902
|
+
const yiaddr = `${msg[16]}.${msg[17]}.${msg[18]}.${msg[19]}`;
|
|
164903
|
+
const ciaddr = `${msg[12]}.${msg[13]}.${msg[14]}.${msg[15]}`;
|
|
164904
|
+
const ip = yiaddr !== "0.0.0.0" ? yiaddr : ciaddr;
|
|
164905
|
+
if (ip === "0.0.0.0" || !ip) return;
|
|
164906
|
+
if (!discovered.has(ip)) {
|
|
164907
|
+
logger?.log?.(`[Discovery] DHCP: found Reolink device ${ip} (MAC: ${mac}, hostname: ${hostname || "n/a"}, op: ${op === 1 ? "request" : "reply"})`);
|
|
164908
|
+
const device = {
|
|
164909
|
+
host: ip,
|
|
164910
|
+
discoveryMethod: "dhcp"
|
|
164911
|
+
};
|
|
164912
|
+
if (hostname) device.name = hostname;
|
|
164913
|
+
discovered.set(ip, device);
|
|
164914
|
+
}
|
|
164915
|
+
} catch {}
|
|
164916
|
+
});
|
|
164917
|
+
socket.on("error", (err) => {
|
|
164918
|
+
logger?.warn?.(`[Discovery] DHCP socket error: ${err.message}`);
|
|
164919
|
+
clearTimeout(timeout);
|
|
164920
|
+
socket.close();
|
|
164921
|
+
resolve(Array.from(discovered.values()));
|
|
164922
|
+
});
|
|
164923
|
+
socket.bind(67, "0.0.0.0", () => {
|
|
164924
|
+
logger?.log?.("[Discovery] DHCP listener bound on port 67");
|
|
164925
|
+
timeout = setTimeout(() => {
|
|
164926
|
+
socket.close();
|
|
164927
|
+
logger?.log?.(`[Discovery] DHCP listener complete. Found ${discovered.size} device(s).`);
|
|
164928
|
+
resolve(Array.from(discovered.values()));
|
|
164929
|
+
}, timeoutMs);
|
|
164930
|
+
});
|
|
164931
|
+
});
|
|
164932
|
+
}
|
|
164933
|
+
function probeTcpPort(ip, port, timeoutMs) {
|
|
164934
|
+
return new Promise((resolve) => {
|
|
164935
|
+
const socket = new net2.Socket();
|
|
164936
|
+
let settled = false;
|
|
164937
|
+
const done = (result) => {
|
|
164938
|
+
if (settled) return;
|
|
164939
|
+
settled = true;
|
|
164940
|
+
socket.destroy();
|
|
164941
|
+
resolve(result);
|
|
164942
|
+
};
|
|
164943
|
+
socket.setTimeout(timeoutMs);
|
|
164944
|
+
socket.on("connect", () => done(true));
|
|
164945
|
+
socket.on("timeout", () => done(false));
|
|
164946
|
+
socket.on("error", () => done(false));
|
|
164947
|
+
socket.connect(port, ip);
|
|
164948
|
+
});
|
|
164949
|
+
}
|
|
164950
|
+
async function discoverViaTcpPortScan(options) {
|
|
164951
|
+
if (!options.enableTcpPortScan) return [];
|
|
164952
|
+
const logger = options.logger;
|
|
164953
|
+
const networkCidr = options.networkCidr ?? getLocalNetworks()[0];
|
|
164954
|
+
const timeoutMs = options.tcpProbeTimeoutMs ?? 1500;
|
|
164955
|
+
const maxConcurrent = options.maxConcurrentProbes ?? 80;
|
|
164956
|
+
if (!networkCidr) {
|
|
164957
|
+
logger?.warn?.("[Discovery] No network CIDR available for TCP port scan");
|
|
164958
|
+
return [];
|
|
164959
|
+
}
|
|
164960
|
+
logger?.log?.(`[Discovery] Starting TCP port 9000 scan on network ${networkCidr}...`);
|
|
164961
|
+
const ipRange = parseCidr(networkCidr);
|
|
164962
|
+
if (!ipRange) {
|
|
164963
|
+
logger?.warn?.(`[Discovery] Invalid CIDR: ${networkCidr}`);
|
|
164964
|
+
return [];
|
|
164965
|
+
}
|
|
164966
|
+
const discovered = [];
|
|
164967
|
+
const ipAddresses = [];
|
|
164968
|
+
for (let ipNum = ipRange.start; ipNum <= ipRange.end && ipNum <= ipRange.start + 254; ipNum++) ipAddresses.push(ipNumberToString(ipNum));
|
|
164969
|
+
logger?.log?.(`[Discovery] Scanning ${ipAddresses.length} IPs on port 9000...`);
|
|
164970
|
+
for (let i = 0; i < ipAddresses.length; i += maxConcurrent) {
|
|
164971
|
+
const batch = ipAddresses.slice(i, i + maxConcurrent);
|
|
164972
|
+
const batchResults = await Promise.allSettled(batch.map(async (ip) => {
|
|
164973
|
+
if (await probeTcpPort(ip, 9e3, timeoutMs)) {
|
|
164974
|
+
logger?.log?.(`[Discovery] Found Baichuan device at ${ip}:9000`);
|
|
164975
|
+
return {
|
|
164976
|
+
host: ip,
|
|
164977
|
+
discoveryMethod: "tcp_port_scan"
|
|
164978
|
+
};
|
|
164979
|
+
}
|
|
164980
|
+
return null;
|
|
164981
|
+
}));
|
|
164982
|
+
for (const result of batchResults) if (result.status === "fulfilled" && result.value) discovered.push(result.value);
|
|
164983
|
+
}
|
|
164984
|
+
logger?.log?.(`[Discovery] TCP port scan complete. Found ${discovered.length} device(s).`);
|
|
164985
|
+
return discovered;
|
|
164986
|
+
}
|
|
164987
|
+
async function discoverViaOnvif(options) {
|
|
164988
|
+
if (!options.enableOnvifDiscovery) return [];
|
|
164989
|
+
const logger = options.logger;
|
|
164990
|
+
const timeoutMs = options.onvifDiscoveryTimeoutMs ?? 5e3;
|
|
164991
|
+
logger?.log?.(`[Discovery] Starting ONVIF WS-Discovery (${timeoutMs}ms)...`);
|
|
164992
|
+
const discovered = /* @__PURE__ */ new Map();
|
|
164993
|
+
const MULTICAST_ADDR = "239.255.255.250";
|
|
164994
|
+
const MULTICAST_PORT = 3702;
|
|
164995
|
+
const probeMessage = [
|
|
164996
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
|
164997
|
+
"<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\"",
|
|
164998
|
+
" xmlns:a=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\"",
|
|
164999
|
+
" xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\"",
|
|
165000
|
+
" xmlns:dn=\"http://www.onvif.org/ver10/network/wsdl\">",
|
|
165001
|
+
" <s:Header>",
|
|
165002
|
+
` <a:MessageID>${`uuid:${randomUUID()}`}</a:MessageID>`,
|
|
165003
|
+
" <a:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>",
|
|
165004
|
+
" <a:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>",
|
|
165005
|
+
" </s:Header>",
|
|
165006
|
+
" <s:Body>",
|
|
165007
|
+
" <d:Probe>",
|
|
165008
|
+
" <d:Types>dn:NetworkVideoTransmitter</d:Types>",
|
|
165009
|
+
" </d:Probe>",
|
|
165010
|
+
" </s:Body>",
|
|
165011
|
+
"</s:Envelope>"
|
|
165012
|
+
].join("\n");
|
|
165013
|
+
return new Promise((resolve) => {
|
|
165014
|
+
const socket = dgram.createSocket({
|
|
165015
|
+
type: "udp4",
|
|
165016
|
+
reuseAddr: true
|
|
165017
|
+
});
|
|
165018
|
+
let timeout;
|
|
165019
|
+
socket.on("message", (msg, rinfo) => {
|
|
165020
|
+
try {
|
|
165021
|
+
const xml = msg.toString("utf8");
|
|
165022
|
+
const xaddrsMatch = /<[^:]*:?XAddrs>([^<]+)<\/[^:]*:?XAddrs>/i.exec(xml);
|
|
165023
|
+
const scopesMatch = /<[^:]*:?Scopes>([^<]+)<\/[^:]*:?Scopes>/i.exec(xml);
|
|
165024
|
+
let host = rinfo.address;
|
|
165025
|
+
let httpPort;
|
|
165026
|
+
if (xaddrsMatch?.[1]) {
|
|
165027
|
+
const urls = xaddrsMatch[1].trim().split(/\s+/);
|
|
165028
|
+
for (const url of urls) try {
|
|
165029
|
+
const parsed = new URL(url);
|
|
165030
|
+
if (parsed.hostname) {
|
|
165031
|
+
host = parsed.hostname;
|
|
165032
|
+
const p = Number.parseInt(parsed.port, 10);
|
|
165033
|
+
if (p && p !== 80) httpPort = p;
|
|
165034
|
+
break;
|
|
165035
|
+
}
|
|
165036
|
+
} catch {}
|
|
165037
|
+
}
|
|
165038
|
+
if (discovered.has(host)) return;
|
|
165039
|
+
let model;
|
|
165040
|
+
let name;
|
|
165041
|
+
let manufacturer;
|
|
165042
|
+
if (scopesMatch?.[1]) {
|
|
165043
|
+
const scopes = scopesMatch[1].trim().split(/\s+/);
|
|
165044
|
+
for (const scope of scopes) {
|
|
165045
|
+
const hwMatch = /\/hardware\/(.+)$/i.exec(scope);
|
|
165046
|
+
if (hwMatch?.[1]) model = decodeURIComponent(hwMatch[1]);
|
|
165047
|
+
const nameMatch = /\/name\/(.+)$/i.exec(scope);
|
|
165048
|
+
if (nameMatch?.[1]) name = decodeURIComponent(nameMatch[1]);
|
|
165049
|
+
const mfgMatch = /\/manufacturer\/(.+)$/i.exec(scope);
|
|
165050
|
+
if (mfgMatch?.[1]) manufacturer = decodeURIComponent(mfgMatch[1]);
|
|
165051
|
+
}
|
|
165052
|
+
}
|
|
165053
|
+
const hasReolinkText = `${manufacturer ?? ""} ${model ?? ""} ${xaddrsMatch?.[1] ?? ""}`.toLowerCase().includes("reolink");
|
|
165054
|
+
const hasReolinkModel = /^(rlc|rln|rl[ncb]|e1|cw|cx|duo|trackmix|argus|lumus|go|video doorbell|reolink)/i.test(model ?? "");
|
|
165055
|
+
if (!(hasReolinkText || hasReolinkModel)) {
|
|
165056
|
+
logger?.debug?.(`[Discovery] ONVIF: skipping non-Reolink device at ${host} (${model ?? "unknown"}, manufacturer: ${manufacturer ?? "unknown"})`);
|
|
165057
|
+
return;
|
|
165058
|
+
}
|
|
165059
|
+
logger?.log?.(`[Discovery] ONVIF: found Reolink device at ${host}${model ? ` (${model})` : ""}${name ? ` name="${name}"` : ""}`);
|
|
165060
|
+
const device = {
|
|
165061
|
+
host,
|
|
165062
|
+
discoveryMethod: "onvif"
|
|
165063
|
+
};
|
|
165064
|
+
if (model) device.model = model;
|
|
165065
|
+
if (name && name !== "IPC") device.name = name;
|
|
165066
|
+
else if (model) device.name = model;
|
|
165067
|
+
if (httpPort) device.httpPort = httpPort;
|
|
165068
|
+
discovered.set(host, device);
|
|
165069
|
+
} catch {}
|
|
165070
|
+
});
|
|
165071
|
+
socket.on("error", (err) => {
|
|
165072
|
+
logger?.warn?.(`[Discovery] ONVIF socket error: ${err.message}`);
|
|
165073
|
+
});
|
|
165074
|
+
socket.bind(0, "0.0.0.0", () => {
|
|
165075
|
+
const buf = Buffer.from(probeMessage, "utf8");
|
|
165076
|
+
socket.send(buf, 0, buf.length, MULTICAST_PORT, MULTICAST_ADDR, (err) => {
|
|
165077
|
+
if (err) logger?.warn?.(`[Discovery] ONVIF: failed to send probe: ${err.message}`);
|
|
165078
|
+
});
|
|
165079
|
+
setTimeout(() => {
|
|
165080
|
+
try {
|
|
165081
|
+
socket.send(buf, 0, buf.length, MULTICAST_PORT, MULTICAST_ADDR);
|
|
165082
|
+
} catch {}
|
|
165083
|
+
}, 500);
|
|
165084
|
+
timeout = setTimeout(() => {
|
|
165085
|
+
try {
|
|
165086
|
+
socket.close();
|
|
165087
|
+
} catch {}
|
|
165088
|
+
logger?.log?.(`[Discovery] ONVIF WS-Discovery complete. Found ${discovered.size} device(s).`);
|
|
165089
|
+
resolve(Array.from(discovered.values()));
|
|
165090
|
+
}, timeoutMs);
|
|
165091
|
+
});
|
|
165092
|
+
socket.on("close", () => {
|
|
165093
|
+
if (timeout) clearTimeout(timeout);
|
|
165094
|
+
});
|
|
165095
|
+
});
|
|
165096
|
+
}
|
|
165097
|
+
async function discoverReolinkDevices(options = {}) {
|
|
165098
|
+
const logger = options.logger;
|
|
165099
|
+
logger?.log?.("[Discovery] Starting Reolink device discovery...");
|
|
165100
|
+
const results = [];
|
|
165101
|
+
const seenDevices = /* @__PURE__ */ new Map();
|
|
165102
|
+
const mergeDevice = (device) => {
|
|
165103
|
+
const key = device.host;
|
|
165104
|
+
const existing = seenDevices.get(key);
|
|
165105
|
+
if (existing) {
|
|
165106
|
+
if (!existing.model && device.model) existing.model = device.model;
|
|
165107
|
+
if (!existing.uid && device.uid) existing.uid = device.uid;
|
|
165108
|
+
if (!existing.name && device.name) existing.name = device.name;
|
|
165109
|
+
if (!existing.firmwareVersion && device.firmwareVersion) existing.firmwareVersion = device.firmwareVersion;
|
|
165110
|
+
if (device.httpPort && !existing.httpPort) existing.httpPort = device.httpPort;
|
|
165111
|
+
if (device.httpsPort && !existing.httpsPort) existing.httpsPort = device.httpsPort;
|
|
165112
|
+
if (device.supportsHttps !== void 0) existing.supportsHttps = device.supportsHttps;
|
|
165113
|
+
if (device.httpAccessible !== void 0) existing.httpAccessible = device.httpAccessible;
|
|
165114
|
+
} else {
|
|
165115
|
+
seenDevices.set(key, { ...device });
|
|
165116
|
+
results.push(seenDevices.get(key));
|
|
165117
|
+
}
|
|
165118
|
+
};
|
|
165119
|
+
const [httpDevices, udpDevices, tcpDevices, arpDevices, dhcpDevices, onvifDevices] = await Promise.all([
|
|
165120
|
+
discoverViaHttpScan(options),
|
|
165121
|
+
discoverViaUdpBroadcast(options),
|
|
165122
|
+
discoverViaTcpPortScan(options),
|
|
165123
|
+
discoverViaArpTable(options),
|
|
165124
|
+
discoverViaDhcpListener(options),
|
|
165125
|
+
discoverViaOnvif(options)
|
|
165126
|
+
]);
|
|
165127
|
+
for (const device of dhcpDevices) mergeDevice(device);
|
|
165128
|
+
for (const device of arpDevices) mergeDevice(device);
|
|
165129
|
+
for (const device of tcpDevices) mergeDevice(device);
|
|
165130
|
+
for (const device of onvifDevices) mergeDevice(device);
|
|
165131
|
+
for (const device of httpDevices) mergeDevice(device);
|
|
165132
|
+
for (const device of udpDevices) mergeDevice(device);
|
|
165133
|
+
logger?.log?.(`[Discovery] Discovery complete. Found ${results.length} unique device(s).`);
|
|
165134
|
+
return results;
|
|
165135
|
+
}
|
|
164511
165136
|
var ALL_UDP_DISCOVERY_METHODS = [
|
|
164512
165137
|
"local-direct",
|
|
164513
165138
|
"local-broadcast",
|
|
@@ -222159,6 +222784,62 @@ function discoveredStatusFor(d) {
|
|
|
222159
222784
|
* - NVR devices expose every channel via `ch{N}-main` / `ch{N}-sub`
|
|
222160
222785
|
* stream IDs (Slice 8); no operator choice is required
|
|
222161
222786
|
*/
|
|
222787
|
+
/**
|
|
222788
|
+
* Extra per-scan inputs for Reolink network discovery. Unlike Gree/Ecowitt, a Reolink camera needs
|
|
222789
|
+
* CREDENTIALS to be created — so the scan form collects them: the library uses them to enrich the
|
|
222790
|
+
* probe (model/UID via the authenticated API) and they are baked into each candidate so a one-click
|
|
222791
|
+
* adopt yields a working camera. `networkCidr` reaches cameras on another subnet (auto-detects the
|
|
222792
|
+
* local subnet when blank); ONVIF WS-Discovery is on by default since most Reolink cameras support it.
|
|
222793
|
+
*/
|
|
222794
|
+
function buildDiscoveryParamsFormSchema() {
|
|
222795
|
+
return { sections: [{
|
|
222796
|
+
id: "credentials",
|
|
222797
|
+
title: "Credentials",
|
|
222798
|
+
description: "Used to probe the cameras and pre-filled into each result so you can add it in one click. Assumes all cameras share these credentials.",
|
|
222799
|
+
columns: 2,
|
|
222800
|
+
fields: [{
|
|
222801
|
+
type: "text",
|
|
222802
|
+
key: "username",
|
|
222803
|
+
label: "Username",
|
|
222804
|
+
default: "admin",
|
|
222805
|
+
required: true
|
|
222806
|
+
}, {
|
|
222807
|
+
type: "password",
|
|
222808
|
+
key: "password",
|
|
222809
|
+
label: "Password",
|
|
222810
|
+
required: true,
|
|
222811
|
+
showToggle: true
|
|
222812
|
+
}]
|
|
222813
|
+
}, {
|
|
222814
|
+
id: "scan",
|
|
222815
|
+
title: "Scan options",
|
|
222816
|
+
description: "Leave the network blank to scan the local subnet. To find cameras on a different subnet, enter its CIDR (e.g. 192.168.20.0/24).",
|
|
222817
|
+
columns: 1,
|
|
222818
|
+
fields: [
|
|
222819
|
+
{
|
|
222820
|
+
type: "text",
|
|
222821
|
+
key: "networkCidr",
|
|
222822
|
+
label: "Network (CIDR)",
|
|
222823
|
+
required: false,
|
|
222824
|
+
placeholder: "192.168.20.0/24"
|
|
222825
|
+
},
|
|
222826
|
+
{
|
|
222827
|
+
type: "boolean",
|
|
222828
|
+
key: "enableOnvif",
|
|
222829
|
+
label: "Include ONVIF discovery",
|
|
222830
|
+
default: true
|
|
222831
|
+
},
|
|
222832
|
+
{
|
|
222833
|
+
type: "number",
|
|
222834
|
+
key: "timeoutMs",
|
|
222835
|
+
label: "Scan timeout (ms)",
|
|
222836
|
+
min: 1e3,
|
|
222837
|
+
max: 3e4,
|
|
222838
|
+
default: 5e3
|
|
222839
|
+
}
|
|
222840
|
+
]
|
|
222841
|
+
}] };
|
|
222842
|
+
}
|
|
222162
222843
|
function buildCreationFormSchema() {
|
|
222163
222844
|
return { sections: [
|
|
222164
222845
|
{
|
|
@@ -222770,6 +223451,11 @@ function isMeaningfulIdentifier(s) {
|
|
|
222770
223451
|
if (!s || s.length < 6) return false;
|
|
222771
223452
|
return new Set(s.toLowerCase().split("").filter((c) => c !== "0" && c !== "f")).size >= 1;
|
|
222772
223453
|
}
|
|
223454
|
+
/** Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
|
|
223455
|
+
* discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan. */
|
|
223456
|
+
function slugifyReolinkHost(host) {
|
|
223457
|
+
return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
223458
|
+
}
|
|
222773
223459
|
/**
|
|
222774
223460
|
* Patch `detection.deviceInfo` + `hostNetworkInfo` in-place with the
|
|
222775
223461
|
* post-login HOST identifiers. Two distinct gaps to fill:
|
|
@@ -222921,7 +223607,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
222921
223607
|
if (isMeaningfulIdentifier(mac)) return `mac-${mac}`;
|
|
222922
223608
|
const host = typeof cfg["host"] === "string" ? cfg["host"].trim() : "";
|
|
222923
223609
|
if (host.length > 0) {
|
|
222924
|
-
const slug = host
|
|
223610
|
+
const slug = slugifyReolinkHost(host);
|
|
222925
223611
|
if (slug.length > 0) return `host-${slug}`;
|
|
222926
223612
|
}
|
|
222927
223613
|
const hostLabel = host || "(unknown host)";
|
|
@@ -223035,6 +223721,61 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
223035
223721
|
});
|
|
223036
223722
|
await dev.materializeStreamSocket(camStreamId);
|
|
223037
223723
|
}
|
|
223724
|
+
async supportsDiscovery() {
|
|
223725
|
+
return true;
|
|
223726
|
+
}
|
|
223727
|
+
async getDiscoveryParamsSchema() {
|
|
223728
|
+
return buildDiscoveryParamsFormSchema();
|
|
223729
|
+
}
|
|
223730
|
+
async discoverDevices(input) {
|
|
223731
|
+
const p = input?.params ?? {};
|
|
223732
|
+
const username = typeof p["username"] === "string" ? p["username"].trim() : "";
|
|
223733
|
+
const password = typeof p["password"] === "string" ? p["password"] : "";
|
|
223734
|
+
const networkCidr = typeof p["networkCidr"] === "string" ? p["networkCidr"].trim() : "";
|
|
223735
|
+
const enableOnvif = p["enableOnvif"] !== false;
|
|
223736
|
+
const timeoutMs = typeof p["timeoutMs"] === "number" ? p["timeoutMs"] : void 0;
|
|
223737
|
+
const devices = await discoverReolinkDevices({
|
|
223738
|
+
logger: buildAutodetectLibLogger(this.ctx.logger),
|
|
223739
|
+
enableOnvifDiscovery: enableOnvif,
|
|
223740
|
+
...username ? { username } : {},
|
|
223741
|
+
...password ? { password } : {},
|
|
223742
|
+
...networkCidr ? { networkCidr } : {},
|
|
223743
|
+
...timeoutMs !== void 0 ? {
|
|
223744
|
+
udpBroadcastTimeoutMs: timeoutMs,
|
|
223745
|
+
onvifDiscoveryTimeoutMs: timeoutMs
|
|
223746
|
+
} : {}
|
|
223747
|
+
});
|
|
223748
|
+
this.ctx.logger.info("Reolink discovery complete", { meta: {
|
|
223749
|
+
count: devices.length,
|
|
223750
|
+
networkCidr: networkCidr || "local",
|
|
223751
|
+
enableOnvif
|
|
223752
|
+
} });
|
|
223753
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
223754
|
+
for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
|
|
223755
|
+
return [...byHost.values()].map((d) => {
|
|
223756
|
+
const displayName = d.name ?? d.model ?? d.host;
|
|
223757
|
+
return {
|
|
223758
|
+
stableId: `host-${slugifyReolinkHost(d.host)}`,
|
|
223759
|
+
type: DeviceType.Camera,
|
|
223760
|
+
suggestedName: displayName,
|
|
223761
|
+
prefilledConfig: {
|
|
223762
|
+
name: displayName,
|
|
223763
|
+
host: d.host,
|
|
223764
|
+
transport: "auto",
|
|
223765
|
+
...d.httpPort !== void 0 ? { port: d.httpPort } : {},
|
|
223766
|
+
...d.uid ? { uid: d.uid } : {},
|
|
223767
|
+
...username ? { username } : {},
|
|
223768
|
+
...password ? { password } : {}
|
|
223769
|
+
}
|
|
223770
|
+
};
|
|
223771
|
+
});
|
|
223772
|
+
}
|
|
223773
|
+
async adoptDiscoveredDevice(input) {
|
|
223774
|
+
return this.createDevice({
|
|
223775
|
+
type: DeviceType.Camera,
|
|
223776
|
+
config: input.candidate.prefilledConfig
|
|
223777
|
+
});
|
|
223778
|
+
}
|
|
223038
223779
|
async onGetCreationSchema(type) {
|
|
223039
223780
|
if (type !== DeviceType.Camera && type !== DeviceType.Hub) return null;
|
|
223040
223781
|
return buildCreationFormSchema();
|