@camstack/addon-provider-reolink 1.1.9 → 1.1.11
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 +759 -7
- package/dist/addon.mjs +760 -8
- package/package.json +2 -1
package/dist/addon.js
CHANGED
|
@@ -4655,7 +4655,7 @@ function _instanceof(cls, params = {}) {
|
|
|
4655
4655
|
return inst;
|
|
4656
4656
|
}
|
|
4657
4657
|
//#endregion
|
|
4658
|
-
//#region ../types/dist/sleep-
|
|
4658
|
+
//#region ../types/dist/sleep-C2M2zF7x.mjs
|
|
4659
4659
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4660
4660
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4661
4661
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -6223,6 +6223,12 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
|
6223
6223
|
DeviceType["Switch"] = "switch";
|
|
6224
6224
|
DeviceType["Sensor"] = "sensor";
|
|
6225
6225
|
DeviceType["Thermostat"] = "thermostat";
|
|
6226
|
+
/** Air-conditioner / heat-pump climate device (HVAC) — shares the
|
|
6227
|
+
* `climate-control` cap surface with `Thermostat` but renders a
|
|
6228
|
+
* dedicated AC-appropriate control UI (mode chips, fan speed,
|
|
6229
|
+
* independent vertical/horizontal swing). Sources: native Gree, and
|
|
6230
|
+
* reusable by other AC integrations. */
|
|
6231
|
+
DeviceType["Climate"] = "climate";
|
|
6226
6232
|
DeviceType["Button"] = "button";
|
|
6227
6233
|
/** Generic stateless event emitter — carries a device's EXACT declared
|
|
6228
6234
|
* event vocabulary verbatim (no normalization). Installed with the
|
|
@@ -9244,7 +9250,7 @@ var climateControlCapability = {
|
|
|
9244
9250
|
scope: "device",
|
|
9245
9251
|
deviceNative: true,
|
|
9246
9252
|
mode: "singleton",
|
|
9247
|
-
deviceTypes: [DeviceType.Thermostat],
|
|
9253
|
+
deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
|
|
9248
9254
|
methods: {
|
|
9249
9255
|
setMode: method(object({
|
|
9250
9256
|
deviceId: number().int().nonnegative(),
|
|
@@ -13843,10 +13849,30 @@ var deviceProviderCapability = {
|
|
|
13843
13849
|
type: string()
|
|
13844
13850
|
}))),
|
|
13845
13851
|
supportsDiscovery: method(object({}), boolean()),
|
|
13846
|
-
|
|
13852
|
+
/**
|
|
13853
|
+
* Run a network scan. `params` carries optional provider-specific scan
|
|
13854
|
+
* inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
|
|
13855
|
+
* shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
|
|
13856
|
+
* (provider uses its local-network default).
|
|
13857
|
+
*/
|
|
13858
|
+
discoverDevices: method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
|
|
13847
13859
|
kind: "mutation",
|
|
13848
13860
|
auth: "admin"
|
|
13849
13861
|
}),
|
|
13862
|
+
/**
|
|
13863
|
+
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
13864
|
+
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
13865
|
+
* `null` when the provider takes no extra scan params — the generic
|
|
13866
|
+
* aggregated scan never renders this; the per-integration scan does.
|
|
13867
|
+
*/
|
|
13868
|
+
getDiscoveryParamsSchema: method(object({}), CreationSchemaOutputSchema),
|
|
13869
|
+
/**
|
|
13870
|
+
* The DeviceType this provider creates via manual add (Camera for
|
|
13871
|
+
* Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
|
|
13872
|
+
* provider does not support manual creation. Lets the Add-Device dialog
|
|
13873
|
+
* pick the right type instead of assuming Camera.
|
|
13874
|
+
*/
|
|
13875
|
+
getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
|
|
13850
13876
|
adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
13851
13877
|
kind: "mutation",
|
|
13852
13878
|
auth: "admin"
|
|
@@ -13970,9 +13996,23 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
13970
13996
|
async supportsDiscovery() {
|
|
13971
13997
|
return false;
|
|
13972
13998
|
}
|
|
13973
|
-
async discoverDevices() {
|
|
13999
|
+
async discoverDevices(_input) {
|
|
13974
14000
|
return [];
|
|
13975
14001
|
}
|
|
14002
|
+
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
14003
|
+
* Null = no extra params. Override in providers that support scoped scans. */
|
|
14004
|
+
async getDiscoveryParamsSchema() {
|
|
14005
|
+
return null;
|
|
14006
|
+
}
|
|
14007
|
+
/**
|
|
14008
|
+
* The DeviceType this provider creates via manual add — derived from the
|
|
14009
|
+
* `deviceClasses` map (first registered type). `null` when manual creation is
|
|
14010
|
+
* unsupported. Lets the Add-Device dialog pick the right type per provider.
|
|
14011
|
+
*/
|
|
14012
|
+
async getManualCreationType() {
|
|
14013
|
+
if (!await this.supportsManualCreation()) return { deviceType: null };
|
|
14014
|
+
return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
|
|
14015
|
+
}
|
|
13976
14016
|
async adoptDiscoveredDevice(_input) {
|
|
13977
14017
|
throw new Error(`${this.providerName} provider does not support discovery-based adoption`);
|
|
13978
14018
|
}
|
|
@@ -15829,7 +15869,10 @@ method(object({
|
|
|
15829
15869
|
}), FieldProbeResultSchema, {
|
|
15830
15870
|
kind: "mutation",
|
|
15831
15871
|
auth: "admin"
|
|
15832
|
-
}), method(
|
|
15872
|
+
}), method(object({
|
|
15873
|
+
addonId: string(),
|
|
15874
|
+
integrationId: string()
|
|
15875
|
+
}), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
|
|
15833
15876
|
addonId: string(),
|
|
15834
15877
|
integrationId: string()
|
|
15835
15878
|
}), AdoptionStatusSchema, {
|
|
@@ -15844,7 +15887,24 @@ method(object({
|
|
|
15844
15887
|
}), method(ResyncInputSchema, ResyncResultSchema, {
|
|
15845
15888
|
kind: "mutation",
|
|
15846
15889
|
auth: "admin"
|
|
15890
|
+
}), method(object({}), object({ providers: array(object({
|
|
15891
|
+
addonId: string(),
|
|
15892
|
+
label: string()
|
|
15893
|
+
})).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
|
|
15894
|
+
addonId: string(),
|
|
15895
|
+
label: string(),
|
|
15896
|
+
candidates: array(DiscoveryCandidateSchema).readonly(),
|
|
15897
|
+
error: string().nullable()
|
|
15898
|
+
})).readonly() }), {
|
|
15899
|
+
kind: "mutation",
|
|
15900
|
+
auth: "admin"
|
|
15847
15901
|
}), method(object({
|
|
15902
|
+
addonId: string(),
|
|
15903
|
+
params: record(string(), unknown()).optional()
|
|
15904
|
+
}), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
|
|
15905
|
+
kind: "mutation",
|
|
15906
|
+
auth: "admin"
|
|
15907
|
+
}), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
|
|
15848
15908
|
deviceId: number(),
|
|
15849
15909
|
key: string(),
|
|
15850
15910
|
value: unknown()
|
|
@@ -18539,6 +18599,17 @@ var AvailableIntegrationTypeSchema = object({
|
|
|
18539
18599
|
iconUrl: string().nullable(),
|
|
18540
18600
|
color: string(),
|
|
18541
18601
|
instanceMode: string(),
|
|
18602
|
+
/**
|
|
18603
|
+
* Integration wizard `mode` (LOCKED MODEL): `standalone` (create
|
|
18604
|
+
* immediately then add devices, no config step/button), `account` (config
|
|
18605
|
+
* step), or `broker` (broker step). Derived server-side by
|
|
18606
|
+
* `getAvailableTypes` when the addon manifest omits an explicit `mode`.
|
|
18607
|
+
*/
|
|
18608
|
+
mode: _enum([
|
|
18609
|
+
"standalone",
|
|
18610
|
+
"account",
|
|
18611
|
+
"broker"
|
|
18612
|
+
]),
|
|
18542
18613
|
discoveryMode: string(),
|
|
18543
18614
|
/**
|
|
18544
18615
|
* Which integration-marker cap the addon declared, so the wizard can
|
|
@@ -21328,6 +21399,12 @@ Object.freeze({
|
|
|
21328
21399
|
addonId: null,
|
|
21329
21400
|
access: "create"
|
|
21330
21401
|
},
|
|
21402
|
+
"deviceManager.adoptionListCandidateFilters": {
|
|
21403
|
+
capName: "device-manager",
|
|
21404
|
+
capScope: "system",
|
|
21405
|
+
addonId: null,
|
|
21406
|
+
access: "view"
|
|
21407
|
+
},
|
|
21331
21408
|
"deviceManager.adoptionListCandidates": {
|
|
21332
21409
|
capName: "device-manager",
|
|
21333
21410
|
capScope: "system",
|
|
@@ -21376,12 +21453,30 @@ Object.freeze({
|
|
|
21376
21453
|
addonId: null,
|
|
21377
21454
|
access: "create"
|
|
21378
21455
|
},
|
|
21456
|
+
"deviceManager.discoverAllProviders": {
|
|
21457
|
+
capName: "device-manager",
|
|
21458
|
+
capScope: "system",
|
|
21459
|
+
addonId: null,
|
|
21460
|
+
access: "create"
|
|
21461
|
+
},
|
|
21379
21462
|
"deviceManager.discoverDevices": {
|
|
21380
21463
|
capName: "device-manager",
|
|
21381
21464
|
capScope: "system",
|
|
21382
21465
|
addonId: null,
|
|
21383
21466
|
access: "create"
|
|
21384
21467
|
},
|
|
21468
|
+
"deviceManager.discoverProvider": {
|
|
21469
|
+
capName: "device-manager",
|
|
21470
|
+
capScope: "system",
|
|
21471
|
+
addonId: null,
|
|
21472
|
+
access: "create"
|
|
21473
|
+
},
|
|
21474
|
+
"deviceManager.discoveryProviders": {
|
|
21475
|
+
capName: "device-manager",
|
|
21476
|
+
capScope: "system",
|
|
21477
|
+
addonId: null,
|
|
21478
|
+
access: "view"
|
|
21479
|
+
},
|
|
21385
21480
|
"deviceManager.enable": {
|
|
21386
21481
|
capName: "device-manager",
|
|
21387
21482
|
capScope: "system",
|
|
@@ -21532,6 +21627,18 @@ Object.freeze({
|
|
|
21532
21627
|
addonId: null,
|
|
21533
21628
|
access: "create"
|
|
21534
21629
|
},
|
|
21630
|
+
"deviceManager.providerCreationType": {
|
|
21631
|
+
capName: "device-manager",
|
|
21632
|
+
capScope: "system",
|
|
21633
|
+
addonId: null,
|
|
21634
|
+
access: "view"
|
|
21635
|
+
},
|
|
21636
|
+
"deviceManager.providerDiscoveryParamsSchema": {
|
|
21637
|
+
capName: "device-manager",
|
|
21638
|
+
capScope: "system",
|
|
21639
|
+
addonId: null,
|
|
21640
|
+
access: "view"
|
|
21641
|
+
},
|
|
21535
21642
|
"deviceManager.registerDevice": {
|
|
21536
21643
|
capName: "device-manager",
|
|
21537
21644
|
capScope: "system",
|
|
@@ -21748,6 +21855,18 @@ Object.freeze({
|
|
|
21748
21855
|
addonId: null,
|
|
21749
21856
|
access: "view"
|
|
21750
21857
|
},
|
|
21858
|
+
"deviceProvider.getDiscoveryParamsSchema": {
|
|
21859
|
+
capName: "device-provider",
|
|
21860
|
+
capScope: "system",
|
|
21861
|
+
addonId: null,
|
|
21862
|
+
access: "view"
|
|
21863
|
+
},
|
|
21864
|
+
"deviceProvider.getManualCreationType": {
|
|
21865
|
+
capName: "device-provider",
|
|
21866
|
+
capScope: "system",
|
|
21867
|
+
addonId: null,
|
|
21868
|
+
access: "view"
|
|
21869
|
+
},
|
|
21751
21870
|
"deviceProvider.getStatus": {
|
|
21752
21871
|
capName: "device-provider",
|
|
21753
21872
|
capScope: "system",
|
|
@@ -164360,7 +164479,7 @@ ${scheduleItems}
|
|
|
164360
164479
|
}), chimeId);
|
|
164361
164480
|
}
|
|
164362
164481
|
};
|
|
164363
|
-
(0, util.promisify)(child_process.execFile);
|
|
164482
|
+
var execFileAsync = (0, util.promisify)(child_process.execFile);
|
|
164364
164483
|
async function discoverViaUdpDirect(host, options) {
|
|
164365
164484
|
if (!options.enableUdpDiscovery) return [];
|
|
164366
164485
|
const logger = options.logger;
|
|
@@ -164430,6 +164549,170 @@ async function discoverViaUdpDirect(host, options) {
|
|
|
164430
164549
|
});
|
|
164431
164550
|
});
|
|
164432
164551
|
}
|
|
164552
|
+
function getLocalNetworks() {
|
|
164553
|
+
const networks = [];
|
|
164554
|
+
const interfaces = (0, os.networkInterfaces)();
|
|
164555
|
+
for (const ifaceName of Object.keys(interfaces)) {
|
|
164556
|
+
const iface = interfaces[ifaceName];
|
|
164557
|
+
if (!iface) continue;
|
|
164558
|
+
for (const addr of iface) {
|
|
164559
|
+
if (addr.internal || addr.family !== "IPv4" || !addr.netmask) continue;
|
|
164560
|
+
addr.address.split(".").map(Number);
|
|
164561
|
+
const maskParts = addr.netmask.split(".").map(Number);
|
|
164562
|
+
let cidr = 0;
|
|
164563
|
+
for (let i = 0; i < 4; i++) {
|
|
164564
|
+
const maskValue = maskParts[i];
|
|
164565
|
+
if (maskValue === void 0 || !Number.isFinite(maskValue)) break;
|
|
164566
|
+
if (maskValue === 255) cidr += 8;
|
|
164567
|
+
else if (maskValue === 0) break;
|
|
164568
|
+
else {
|
|
164569
|
+
let bits = 0;
|
|
164570
|
+
let m = maskValue;
|
|
164571
|
+
while (m > 0) {
|
|
164572
|
+
if (m & 1) bits++;
|
|
164573
|
+
m = m >> 1;
|
|
164574
|
+
}
|
|
164575
|
+
cidr += bits;
|
|
164576
|
+
break;
|
|
164577
|
+
}
|
|
164578
|
+
}
|
|
164579
|
+
const networkCidr = `${addr.address.split(".").slice(0, 3).join(".")}.0/${cidr}`;
|
|
164580
|
+
if (!networks.includes(networkCidr)) networks.push(networkCidr);
|
|
164581
|
+
}
|
|
164582
|
+
}
|
|
164583
|
+
return networks;
|
|
164584
|
+
}
|
|
164585
|
+
function parseCidr(cidr) {
|
|
164586
|
+
const parts = cidr.split("/");
|
|
164587
|
+
const network = parts[0];
|
|
164588
|
+
const prefixStr = parts[1];
|
|
164589
|
+
if (!network) return null;
|
|
164590
|
+
const prefix = Number.parseInt(prefixStr ?? "24", 10);
|
|
164591
|
+
if (!Number.isFinite(prefix) || prefix < 0 || prefix > 32) return null;
|
|
164592
|
+
const ipParts = network.split(".").map(Number);
|
|
164593
|
+
if (ipParts.length !== 4 || ipParts.some((p) => !Number.isFinite(p) || p < 0 || p > 255)) return null;
|
|
164594
|
+
const networkBits = prefix;
|
|
164595
|
+
const hostBits = 32 - networkBits;
|
|
164596
|
+
let networkAddr = 0;
|
|
164597
|
+
for (let i = 0; i < 4; i++) {
|
|
164598
|
+
const part = ipParts[i];
|
|
164599
|
+
if (part === void 0 || !Number.isFinite(part)) return null;
|
|
164600
|
+
networkAddr = networkAddr << 8 | part & 255;
|
|
164601
|
+
}
|
|
164602
|
+
const mask = (1 << networkBits) - 1 << hostBits;
|
|
164603
|
+
networkAddr &= mask;
|
|
164604
|
+
const hostCount = 1 << hostBits;
|
|
164605
|
+
const start = prefix >= 24 ? networkAddr + 1 : networkAddr;
|
|
164606
|
+
const end = prefix >= 24 ? networkAddr + hostCount - 2 : networkAddr + hostCount - 1;
|
|
164607
|
+
return {
|
|
164608
|
+
start,
|
|
164609
|
+
end,
|
|
164610
|
+
count: end - start + 1
|
|
164611
|
+
};
|
|
164612
|
+
}
|
|
164613
|
+
function ipNumberToString(ip) {
|
|
164614
|
+
return `${ip >>> 24 & 255}.${ip >>> 16 & 255}.${ip >>> 8 & 255}.${ip & 255}`;
|
|
164615
|
+
}
|
|
164616
|
+
async function probeHttpDevice(ip, port, options) {
|
|
164617
|
+
const { username, password, timeoutMs, logger, useHttps } = options;
|
|
164618
|
+
try {
|
|
164619
|
+
const cgi = new ReolinkCgiApi({
|
|
164620
|
+
host: ip,
|
|
164621
|
+
port,
|
|
164622
|
+
useHttps: useHttps ?? false,
|
|
164623
|
+
username: username ?? "admin",
|
|
164624
|
+
password: password ?? "",
|
|
164625
|
+
timeoutMs
|
|
164626
|
+
});
|
|
164627
|
+
try {
|
|
164628
|
+
const info = await cgi.getInfo();
|
|
164629
|
+
if (info?.type) {
|
|
164630
|
+
logger?.log?.(`[Discovery] Found Reolink device at ${ip}:${port} (${useHttps ? "HTTPS" : "HTTP"}) - ${info.type}`);
|
|
164631
|
+
const result = {
|
|
164632
|
+
host: ip,
|
|
164633
|
+
discoveryMethod: "http_probe",
|
|
164634
|
+
supportsHttps: useHttps ?? false,
|
|
164635
|
+
httpAccessible: !useHttps
|
|
164636
|
+
};
|
|
164637
|
+
if (port !== void 0) if (useHttps) result.httpsPort = port;
|
|
164638
|
+
else result.httpPort = port;
|
|
164639
|
+
if (info.type) result.model = info.type.trim();
|
|
164640
|
+
if (info.name) result.name = info.name.trim();
|
|
164641
|
+
if (info.firmwareVersion) result.firmwareVersion = info.firmwareVersion.trim();
|
|
164642
|
+
return result;
|
|
164643
|
+
}
|
|
164644
|
+
} catch {
|
|
164645
|
+
if (username && password) try {
|
|
164646
|
+
await cgi.login();
|
|
164647
|
+
const info = await cgi.getInfo();
|
|
164648
|
+
if (info?.type) {
|
|
164649
|
+
logger?.log?.(`[Discovery] Found authenticated Reolink device at ${ip}:${port} (${useHttps ? "HTTPS" : "HTTP"}) - ${info.type}`);
|
|
164650
|
+
const result = {
|
|
164651
|
+
host: ip,
|
|
164652
|
+
discoveryMethod: "http_probe",
|
|
164653
|
+
supportsHttps: useHttps ?? false,
|
|
164654
|
+
httpAccessible: !useHttps
|
|
164655
|
+
};
|
|
164656
|
+
if (port !== void 0) if (useHttps) result.httpsPort = port;
|
|
164657
|
+
else result.httpPort = port;
|
|
164658
|
+
if (info.type) result.model = info.type.trim();
|
|
164659
|
+
if (info.name) result.name = info.name.trim();
|
|
164660
|
+
if (info.firmwareVersion) result.firmwareVersion = info.firmwareVersion.trim();
|
|
164661
|
+
return result;
|
|
164662
|
+
}
|
|
164663
|
+
} catch {}
|
|
164664
|
+
}
|
|
164665
|
+
} catch (err) {
|
|
164666
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
164667
|
+
if (!msg.includes("ECONNREFUSED") && !msg.includes("ETIMEDOUT")) logger?.warn?.(`[Discovery] Error probing ${ip}:${port}: ${msg}`);
|
|
164668
|
+
}
|
|
164669
|
+
return null;
|
|
164670
|
+
}
|
|
164671
|
+
async function discoverViaHttpScan(options) {
|
|
164672
|
+
if (!options.enableHttpScanning) return [];
|
|
164673
|
+
const logger = options.logger;
|
|
164674
|
+
const networkCidr = options.networkCidr ?? getLocalNetworks()[0];
|
|
164675
|
+
const httpPorts = options.httpPorts ?? [80, 443];
|
|
164676
|
+
const timeoutMs = options.httpProbeTimeoutMs ?? 2e3;
|
|
164677
|
+
const maxConcurrent = options.maxConcurrentProbes ?? 50;
|
|
164678
|
+
if (!networkCidr) {
|
|
164679
|
+
logger?.warn?.("[Discovery] No network CIDR available for HTTP scanning");
|
|
164680
|
+
return [];
|
|
164681
|
+
}
|
|
164682
|
+
logger?.log?.(`[Discovery] Starting HTTP scan on network ${networkCidr}...`);
|
|
164683
|
+
const ipRange = parseCidr(networkCidr);
|
|
164684
|
+
if (!ipRange) {
|
|
164685
|
+
logger?.warn?.(`[Discovery] Invalid CIDR: ${networkCidr}`);
|
|
164686
|
+
return [];
|
|
164687
|
+
}
|
|
164688
|
+
const discovered = [];
|
|
164689
|
+
const ipAddresses = [];
|
|
164690
|
+
for (let ipNum = ipRange.start; ipNum <= ipRange.end && ipNum <= ipRange.start + 254; ipNum++) {
|
|
164691
|
+
const ip = ipNumberToString(ipNum);
|
|
164692
|
+
for (const port of httpPorts) ipAddresses.push({
|
|
164693
|
+
ip,
|
|
164694
|
+
port,
|
|
164695
|
+
useHttps: port === 443
|
|
164696
|
+
});
|
|
164697
|
+
}
|
|
164698
|
+
logger?.log?.(`[Discovery] Scanning ${ipAddresses.length} IP:port combinations...`);
|
|
164699
|
+
for (let i = 0; i < ipAddresses.length; i += maxConcurrent) {
|
|
164700
|
+
const batch = ipAddresses.slice(i, i + maxConcurrent);
|
|
164701
|
+
const batchResults = await Promise.allSettled(batch.map(({ ip, port, useHttps }) => {
|
|
164702
|
+
const probeOptions = {
|
|
164703
|
+
timeoutMs,
|
|
164704
|
+
useHttps
|
|
164705
|
+
};
|
|
164706
|
+
if (options.username !== void 0) probeOptions.username = options.username;
|
|
164707
|
+
if (options.password !== void 0) probeOptions.password = options.password;
|
|
164708
|
+
if (logger !== void 0) probeOptions.logger = logger;
|
|
164709
|
+
return probeHttpDevice(ip, port, probeOptions);
|
|
164710
|
+
}));
|
|
164711
|
+
for (const result of batchResults) if (result.status === "fulfilled" && result.value) discovered.push(result.value);
|
|
164712
|
+
}
|
|
164713
|
+
logger?.log?.(`[Discovery] HTTP scan complete. Found ${discovered.length} device(s).`);
|
|
164714
|
+
return discovered;
|
|
164715
|
+
}
|
|
164433
164716
|
async function discoverViaUdpBroadcast(options) {
|
|
164434
164717
|
if (!options.enableUdpDiscovery) return [];
|
|
164435
164718
|
const logger = options.logger;
|
|
@@ -164517,6 +164800,359 @@ async function discoverViaUdpBroadcast(options) {
|
|
|
164517
164800
|
});
|
|
164518
164801
|
});
|
|
164519
164802
|
}
|
|
164803
|
+
var REOLINK_MAC_PREFIXES = [
|
|
164804
|
+
"EC:71:DB",
|
|
164805
|
+
"2C:1B:3A",
|
|
164806
|
+
"18:2C:65",
|
|
164807
|
+
"DC:E5:37",
|
|
164808
|
+
"9C:8E:CD",
|
|
164809
|
+
"B4:4B:D6",
|
|
164810
|
+
"E4:3D:1A"
|
|
164811
|
+
];
|
|
164812
|
+
async function discoverViaArpTable(options) {
|
|
164813
|
+
if (!options.enableArpLookup) return [];
|
|
164814
|
+
const logger = options.logger;
|
|
164815
|
+
logger?.log?.("[Discovery] Starting ARP table lookup for Reolink MAC prefix...");
|
|
164816
|
+
const discovered = [];
|
|
164817
|
+
try {
|
|
164818
|
+
let entries = [];
|
|
164819
|
+
if ((0, os.platform)() === "linux") try {
|
|
164820
|
+
const { readFile } = await import("fs/promises");
|
|
164821
|
+
const content = await readFile("/proc/net/arp", "utf8");
|
|
164822
|
+
for (const line of content.split("\n").slice(1)) {
|
|
164823
|
+
const parts = line.trim().split(/\s+/);
|
|
164824
|
+
if (parts.length >= 4 && parts[0] && parts[3] && parts[3] !== "00:00:00:00:00:00") entries.push({
|
|
164825
|
+
ip: parts[0],
|
|
164826
|
+
mac: parts[3].toUpperCase()
|
|
164827
|
+
});
|
|
164828
|
+
}
|
|
164829
|
+
} catch {
|
|
164830
|
+
const { stdout } = await runArpCommand();
|
|
164831
|
+
entries = parseArpOutput(stdout);
|
|
164832
|
+
}
|
|
164833
|
+
else {
|
|
164834
|
+
const { stdout } = await runArpCommand();
|
|
164835
|
+
entries = parseArpOutput(stdout);
|
|
164836
|
+
}
|
|
164837
|
+
logger?.log?.(`[Discovery] ARP table has ${entries.length} entries`);
|
|
164838
|
+
for (const { ip, mac } of entries) if (REOLINK_MAC_PREFIXES.some((prefix) => mac.startsWith(prefix))) {
|
|
164839
|
+
logger?.log?.(`[Discovery] Found Reolink device via ARP: ${ip} (MAC: ${mac})`);
|
|
164840
|
+
discovered.push({
|
|
164841
|
+
host: ip,
|
|
164842
|
+
discoveryMethod: "arp"
|
|
164843
|
+
});
|
|
164844
|
+
}
|
|
164845
|
+
} catch (err) {
|
|
164846
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
164847
|
+
logger?.warn?.(`[Discovery] ARP table lookup failed: ${msg}`);
|
|
164848
|
+
}
|
|
164849
|
+
logger?.log?.(`[Discovery] ARP lookup complete. Found ${discovered.length} device(s).`);
|
|
164850
|
+
return discovered;
|
|
164851
|
+
}
|
|
164852
|
+
async function runArpCommand() {
|
|
164853
|
+
for (const arpPath of [
|
|
164854
|
+
"/usr/sbin/arp",
|
|
164855
|
+
"/sbin/arp",
|
|
164856
|
+
"/usr/bin/arp",
|
|
164857
|
+
"arp"
|
|
164858
|
+
]) try {
|
|
164859
|
+
return await execFileAsync(arpPath, ["-an"], { timeout: 5e3 });
|
|
164860
|
+
} catch {}
|
|
164861
|
+
throw new Error("arp command not found");
|
|
164862
|
+
}
|
|
164863
|
+
function parseArpOutput(stdout) {
|
|
164864
|
+
const results = [];
|
|
164865
|
+
for (const line of stdout.split("\n")) {
|
|
164866
|
+
const match = /\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([0-9a-fA-F:]+)/i.exec(line);
|
|
164867
|
+
if (match && match[1] && match[2] && match[2] !== "(incomplete)") results.push({
|
|
164868
|
+
ip: match[1],
|
|
164869
|
+
mac: match[2].toUpperCase()
|
|
164870
|
+
});
|
|
164871
|
+
}
|
|
164872
|
+
return results;
|
|
164873
|
+
}
|
|
164874
|
+
async function discoverViaDhcpListener(options) {
|
|
164875
|
+
if (!options.enableDhcpListener) return [];
|
|
164876
|
+
const logger = options.logger;
|
|
164877
|
+
const timeoutMs = options.dhcpListenerTimeoutMs ?? 1e4;
|
|
164878
|
+
logger?.log?.(`[Discovery] Starting passive DHCP listener (${timeoutMs}ms)...`);
|
|
164879
|
+
const discovered = /* @__PURE__ */ new Map();
|
|
164880
|
+
return new Promise((resolve) => {
|
|
164881
|
+
let socket;
|
|
164882
|
+
let timeout;
|
|
164883
|
+
try {
|
|
164884
|
+
socket = dgram.default.createSocket({
|
|
164885
|
+
type: "udp4",
|
|
164886
|
+
reuseAddr: true
|
|
164887
|
+
});
|
|
164888
|
+
} catch (err) {
|
|
164889
|
+
logger?.warn?.(`[Discovery] DHCP: failed to create socket: ${err instanceof Error ? err.message : String(err)}`);
|
|
164890
|
+
resolve([]);
|
|
164891
|
+
return;
|
|
164892
|
+
}
|
|
164893
|
+
socket.on("message", (msg) => {
|
|
164894
|
+
try {
|
|
164895
|
+
if (msg.length < 240) return;
|
|
164896
|
+
const op = msg[0];
|
|
164897
|
+
if (msg[2] !== 6) return;
|
|
164898
|
+
const mac = [
|
|
164899
|
+
msg[28]?.toString(16).padStart(2, "0"),
|
|
164900
|
+
msg[29]?.toString(16).padStart(2, "0"),
|
|
164901
|
+
msg[30]?.toString(16).padStart(2, "0"),
|
|
164902
|
+
msg[31]?.toString(16).padStart(2, "0"),
|
|
164903
|
+
msg[32]?.toString(16).padStart(2, "0"),
|
|
164904
|
+
msg[33]?.toString(16).padStart(2, "0")
|
|
164905
|
+
].join(":").toUpperCase();
|
|
164906
|
+
const isReolinkMac = REOLINK_MAC_PREFIXES.some((p) => mac.startsWith(p));
|
|
164907
|
+
let hostname = "";
|
|
164908
|
+
let i = 240;
|
|
164909
|
+
while (i < msg.length - 1) {
|
|
164910
|
+
const optType = msg[i];
|
|
164911
|
+
if (optType === 255) break;
|
|
164912
|
+
if (optType === 0) {
|
|
164913
|
+
i++;
|
|
164914
|
+
continue;
|
|
164915
|
+
}
|
|
164916
|
+
const optLen = msg[i + 1] ?? 0;
|
|
164917
|
+
if (optType === 12 && optLen > 0) hostname = msg.subarray(i + 2, i + 2 + optLen).toString("ascii").toLowerCase();
|
|
164918
|
+
i += 2 + optLen;
|
|
164919
|
+
}
|
|
164920
|
+
const isReolinkHostname = hostname.startsWith("reolink");
|
|
164921
|
+
if (!isReolinkMac && !isReolinkHostname) return;
|
|
164922
|
+
const yiaddr = `${msg[16]}.${msg[17]}.${msg[18]}.${msg[19]}`;
|
|
164923
|
+
const ciaddr = `${msg[12]}.${msg[13]}.${msg[14]}.${msg[15]}`;
|
|
164924
|
+
const ip = yiaddr !== "0.0.0.0" ? yiaddr : ciaddr;
|
|
164925
|
+
if (ip === "0.0.0.0" || !ip) return;
|
|
164926
|
+
if (!discovered.has(ip)) {
|
|
164927
|
+
logger?.log?.(`[Discovery] DHCP: found Reolink device ${ip} (MAC: ${mac}, hostname: ${hostname || "n/a"}, op: ${op === 1 ? "request" : "reply"})`);
|
|
164928
|
+
const device = {
|
|
164929
|
+
host: ip,
|
|
164930
|
+
discoveryMethod: "dhcp"
|
|
164931
|
+
};
|
|
164932
|
+
if (hostname) device.name = hostname;
|
|
164933
|
+
discovered.set(ip, device);
|
|
164934
|
+
}
|
|
164935
|
+
} catch {}
|
|
164936
|
+
});
|
|
164937
|
+
socket.on("error", (err) => {
|
|
164938
|
+
logger?.warn?.(`[Discovery] DHCP socket error: ${err.message}`);
|
|
164939
|
+
clearTimeout(timeout);
|
|
164940
|
+
socket.close();
|
|
164941
|
+
resolve(Array.from(discovered.values()));
|
|
164942
|
+
});
|
|
164943
|
+
socket.bind(67, "0.0.0.0", () => {
|
|
164944
|
+
logger?.log?.("[Discovery] DHCP listener bound on port 67");
|
|
164945
|
+
timeout = setTimeout(() => {
|
|
164946
|
+
socket.close();
|
|
164947
|
+
logger?.log?.(`[Discovery] DHCP listener complete. Found ${discovered.size} device(s).`);
|
|
164948
|
+
resolve(Array.from(discovered.values()));
|
|
164949
|
+
}, timeoutMs);
|
|
164950
|
+
});
|
|
164951
|
+
});
|
|
164952
|
+
}
|
|
164953
|
+
function probeTcpPort(ip, port, timeoutMs) {
|
|
164954
|
+
return new Promise((resolve) => {
|
|
164955
|
+
const socket = new net.Socket();
|
|
164956
|
+
let settled = false;
|
|
164957
|
+
const done = (result) => {
|
|
164958
|
+
if (settled) return;
|
|
164959
|
+
settled = true;
|
|
164960
|
+
socket.destroy();
|
|
164961
|
+
resolve(result);
|
|
164962
|
+
};
|
|
164963
|
+
socket.setTimeout(timeoutMs);
|
|
164964
|
+
socket.on("connect", () => done(true));
|
|
164965
|
+
socket.on("timeout", () => done(false));
|
|
164966
|
+
socket.on("error", () => done(false));
|
|
164967
|
+
socket.connect(port, ip);
|
|
164968
|
+
});
|
|
164969
|
+
}
|
|
164970
|
+
async function discoverViaTcpPortScan(options) {
|
|
164971
|
+
if (!options.enableTcpPortScan) return [];
|
|
164972
|
+
const logger = options.logger;
|
|
164973
|
+
const networkCidr = options.networkCidr ?? getLocalNetworks()[0];
|
|
164974
|
+
const timeoutMs = options.tcpProbeTimeoutMs ?? 1500;
|
|
164975
|
+
const maxConcurrent = options.maxConcurrentProbes ?? 80;
|
|
164976
|
+
if (!networkCidr) {
|
|
164977
|
+
logger?.warn?.("[Discovery] No network CIDR available for TCP port scan");
|
|
164978
|
+
return [];
|
|
164979
|
+
}
|
|
164980
|
+
logger?.log?.(`[Discovery] Starting TCP port 9000 scan on network ${networkCidr}...`);
|
|
164981
|
+
const ipRange = parseCidr(networkCidr);
|
|
164982
|
+
if (!ipRange) {
|
|
164983
|
+
logger?.warn?.(`[Discovery] Invalid CIDR: ${networkCidr}`);
|
|
164984
|
+
return [];
|
|
164985
|
+
}
|
|
164986
|
+
const discovered = [];
|
|
164987
|
+
const ipAddresses = [];
|
|
164988
|
+
for (let ipNum = ipRange.start; ipNum <= ipRange.end && ipNum <= ipRange.start + 254; ipNum++) ipAddresses.push(ipNumberToString(ipNum));
|
|
164989
|
+
logger?.log?.(`[Discovery] Scanning ${ipAddresses.length} IPs on port 9000...`);
|
|
164990
|
+
for (let i = 0; i < ipAddresses.length; i += maxConcurrent) {
|
|
164991
|
+
const batch = ipAddresses.slice(i, i + maxConcurrent);
|
|
164992
|
+
const batchResults = await Promise.allSettled(batch.map(async (ip) => {
|
|
164993
|
+
if (await probeTcpPort(ip, 9e3, timeoutMs)) {
|
|
164994
|
+
logger?.log?.(`[Discovery] Found Baichuan device at ${ip}:9000`);
|
|
164995
|
+
return {
|
|
164996
|
+
host: ip,
|
|
164997
|
+
discoveryMethod: "tcp_port_scan"
|
|
164998
|
+
};
|
|
164999
|
+
}
|
|
165000
|
+
return null;
|
|
165001
|
+
}));
|
|
165002
|
+
for (const result of batchResults) if (result.status === "fulfilled" && result.value) discovered.push(result.value);
|
|
165003
|
+
}
|
|
165004
|
+
logger?.log?.(`[Discovery] TCP port scan complete. Found ${discovered.length} device(s).`);
|
|
165005
|
+
return discovered;
|
|
165006
|
+
}
|
|
165007
|
+
async function discoverViaOnvif(options) {
|
|
165008
|
+
if (!options.enableOnvifDiscovery) return [];
|
|
165009
|
+
const logger = options.logger;
|
|
165010
|
+
const timeoutMs = options.onvifDiscoveryTimeoutMs ?? 5e3;
|
|
165011
|
+
logger?.log?.(`[Discovery] Starting ONVIF WS-Discovery (${timeoutMs}ms)...`);
|
|
165012
|
+
const discovered = /* @__PURE__ */ new Map();
|
|
165013
|
+
const MULTICAST_ADDR = "239.255.255.250";
|
|
165014
|
+
const MULTICAST_PORT = 3702;
|
|
165015
|
+
const probeMessage = [
|
|
165016
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
|
|
165017
|
+
"<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\"",
|
|
165018
|
+
" xmlns:a=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\"",
|
|
165019
|
+
" xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\"",
|
|
165020
|
+
" xmlns:dn=\"http://www.onvif.org/ver10/network/wsdl\">",
|
|
165021
|
+
" <s:Header>",
|
|
165022
|
+
` <a:MessageID>${`uuid:${(0, crypto$1.randomUUID)()}`}</a:MessageID>`,
|
|
165023
|
+
" <a:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>",
|
|
165024
|
+
" <a:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>",
|
|
165025
|
+
" </s:Header>",
|
|
165026
|
+
" <s:Body>",
|
|
165027
|
+
" <d:Probe>",
|
|
165028
|
+
" <d:Types>dn:NetworkVideoTransmitter</d:Types>",
|
|
165029
|
+
" </d:Probe>",
|
|
165030
|
+
" </s:Body>",
|
|
165031
|
+
"</s:Envelope>"
|
|
165032
|
+
].join("\n");
|
|
165033
|
+
return new Promise((resolve) => {
|
|
165034
|
+
const socket = dgram.default.createSocket({
|
|
165035
|
+
type: "udp4",
|
|
165036
|
+
reuseAddr: true
|
|
165037
|
+
});
|
|
165038
|
+
let timeout;
|
|
165039
|
+
socket.on("message", (msg, rinfo) => {
|
|
165040
|
+
try {
|
|
165041
|
+
const xml = msg.toString("utf8");
|
|
165042
|
+
const xaddrsMatch = /<[^:]*:?XAddrs>([^<]+)<\/[^:]*:?XAddrs>/i.exec(xml);
|
|
165043
|
+
const scopesMatch = /<[^:]*:?Scopes>([^<]+)<\/[^:]*:?Scopes>/i.exec(xml);
|
|
165044
|
+
let host = rinfo.address;
|
|
165045
|
+
let httpPort;
|
|
165046
|
+
if (xaddrsMatch?.[1]) {
|
|
165047
|
+
const urls = xaddrsMatch[1].trim().split(/\s+/);
|
|
165048
|
+
for (const url of urls) try {
|
|
165049
|
+
const parsed = new URL(url);
|
|
165050
|
+
if (parsed.hostname) {
|
|
165051
|
+
host = parsed.hostname;
|
|
165052
|
+
const p = Number.parseInt(parsed.port, 10);
|
|
165053
|
+
if (p && p !== 80) httpPort = p;
|
|
165054
|
+
break;
|
|
165055
|
+
}
|
|
165056
|
+
} catch {}
|
|
165057
|
+
}
|
|
165058
|
+
if (discovered.has(host)) return;
|
|
165059
|
+
let model;
|
|
165060
|
+
let name;
|
|
165061
|
+
let manufacturer;
|
|
165062
|
+
if (scopesMatch?.[1]) {
|
|
165063
|
+
const scopes = scopesMatch[1].trim().split(/\s+/);
|
|
165064
|
+
for (const scope of scopes) {
|
|
165065
|
+
const hwMatch = /\/hardware\/(.+)$/i.exec(scope);
|
|
165066
|
+
if (hwMatch?.[1]) model = decodeURIComponent(hwMatch[1]);
|
|
165067
|
+
const nameMatch = /\/name\/(.+)$/i.exec(scope);
|
|
165068
|
+
if (nameMatch?.[1]) name = decodeURIComponent(nameMatch[1]);
|
|
165069
|
+
const mfgMatch = /\/manufacturer\/(.+)$/i.exec(scope);
|
|
165070
|
+
if (mfgMatch?.[1]) manufacturer = decodeURIComponent(mfgMatch[1]);
|
|
165071
|
+
}
|
|
165072
|
+
}
|
|
165073
|
+
const hasReolinkText = `${manufacturer ?? ""} ${model ?? ""} ${xaddrsMatch?.[1] ?? ""}`.toLowerCase().includes("reolink");
|
|
165074
|
+
const hasReolinkModel = /^(rlc|rln|rl[ncb]|e1|cw|cx|duo|trackmix|argus|lumus|go|video doorbell|reolink)/i.test(model ?? "");
|
|
165075
|
+
if (!(hasReolinkText || hasReolinkModel)) {
|
|
165076
|
+
logger?.debug?.(`[Discovery] ONVIF: skipping non-Reolink device at ${host} (${model ?? "unknown"}, manufacturer: ${manufacturer ?? "unknown"})`);
|
|
165077
|
+
return;
|
|
165078
|
+
}
|
|
165079
|
+
logger?.log?.(`[Discovery] ONVIF: found Reolink device at ${host}${model ? ` (${model})` : ""}${name ? ` name="${name}"` : ""}`);
|
|
165080
|
+
const device = {
|
|
165081
|
+
host,
|
|
165082
|
+
discoveryMethod: "onvif"
|
|
165083
|
+
};
|
|
165084
|
+
if (model) device.model = model;
|
|
165085
|
+
if (name && name !== "IPC") device.name = name;
|
|
165086
|
+
else if (model) device.name = model;
|
|
165087
|
+
if (httpPort) device.httpPort = httpPort;
|
|
165088
|
+
discovered.set(host, device);
|
|
165089
|
+
} catch {}
|
|
165090
|
+
});
|
|
165091
|
+
socket.on("error", (err) => {
|
|
165092
|
+
logger?.warn?.(`[Discovery] ONVIF socket error: ${err.message}`);
|
|
165093
|
+
});
|
|
165094
|
+
socket.bind(0, "0.0.0.0", () => {
|
|
165095
|
+
const buf = Buffer.from(probeMessage, "utf8");
|
|
165096
|
+
socket.send(buf, 0, buf.length, MULTICAST_PORT, MULTICAST_ADDR, (err) => {
|
|
165097
|
+
if (err) logger?.warn?.(`[Discovery] ONVIF: failed to send probe: ${err.message}`);
|
|
165098
|
+
});
|
|
165099
|
+
setTimeout(() => {
|
|
165100
|
+
try {
|
|
165101
|
+
socket.send(buf, 0, buf.length, MULTICAST_PORT, MULTICAST_ADDR);
|
|
165102
|
+
} catch {}
|
|
165103
|
+
}, 500);
|
|
165104
|
+
timeout = setTimeout(() => {
|
|
165105
|
+
try {
|
|
165106
|
+
socket.close();
|
|
165107
|
+
} catch {}
|
|
165108
|
+
logger?.log?.(`[Discovery] ONVIF WS-Discovery complete. Found ${discovered.size} device(s).`);
|
|
165109
|
+
resolve(Array.from(discovered.values()));
|
|
165110
|
+
}, timeoutMs);
|
|
165111
|
+
});
|
|
165112
|
+
socket.on("close", () => {
|
|
165113
|
+
if (timeout) clearTimeout(timeout);
|
|
165114
|
+
});
|
|
165115
|
+
});
|
|
165116
|
+
}
|
|
165117
|
+
async function discoverReolinkDevices(options = {}) {
|
|
165118
|
+
const logger = options.logger;
|
|
165119
|
+
logger?.log?.("[Discovery] Starting Reolink device discovery...");
|
|
165120
|
+
const results = [];
|
|
165121
|
+
const seenDevices = /* @__PURE__ */ new Map();
|
|
165122
|
+
const mergeDevice = (device) => {
|
|
165123
|
+
const key = device.host;
|
|
165124
|
+
const existing = seenDevices.get(key);
|
|
165125
|
+
if (existing) {
|
|
165126
|
+
if (!existing.model && device.model) existing.model = device.model;
|
|
165127
|
+
if (!existing.uid && device.uid) existing.uid = device.uid;
|
|
165128
|
+
if (!existing.name && device.name) existing.name = device.name;
|
|
165129
|
+
if (!existing.firmwareVersion && device.firmwareVersion) existing.firmwareVersion = device.firmwareVersion;
|
|
165130
|
+
if (device.httpPort && !existing.httpPort) existing.httpPort = device.httpPort;
|
|
165131
|
+
if (device.httpsPort && !existing.httpsPort) existing.httpsPort = device.httpsPort;
|
|
165132
|
+
if (device.supportsHttps !== void 0) existing.supportsHttps = device.supportsHttps;
|
|
165133
|
+
if (device.httpAccessible !== void 0) existing.httpAccessible = device.httpAccessible;
|
|
165134
|
+
} else {
|
|
165135
|
+
seenDevices.set(key, { ...device });
|
|
165136
|
+
results.push(seenDevices.get(key));
|
|
165137
|
+
}
|
|
165138
|
+
};
|
|
165139
|
+
const [httpDevices, udpDevices, tcpDevices, arpDevices, dhcpDevices, onvifDevices] = await Promise.all([
|
|
165140
|
+
discoverViaHttpScan(options),
|
|
165141
|
+
discoverViaUdpBroadcast(options),
|
|
165142
|
+
discoverViaTcpPortScan(options),
|
|
165143
|
+
discoverViaArpTable(options),
|
|
165144
|
+
discoverViaDhcpListener(options),
|
|
165145
|
+
discoverViaOnvif(options)
|
|
165146
|
+
]);
|
|
165147
|
+
for (const device of dhcpDevices) mergeDevice(device);
|
|
165148
|
+
for (const device of arpDevices) mergeDevice(device);
|
|
165149
|
+
for (const device of tcpDevices) mergeDevice(device);
|
|
165150
|
+
for (const device of onvifDevices) mergeDevice(device);
|
|
165151
|
+
for (const device of httpDevices) mergeDevice(device);
|
|
165152
|
+
for (const device of udpDevices) mergeDevice(device);
|
|
165153
|
+
logger?.log?.(`[Discovery] Discovery complete. Found ${results.length} unique device(s).`);
|
|
165154
|
+
return results;
|
|
165155
|
+
}
|
|
164520
165156
|
var ALL_UDP_DISCOVERY_METHODS = [
|
|
164521
165157
|
"local-direct",
|
|
164522
165158
|
"local-broadcast",
|
|
@@ -222168,6 +222804,62 @@ function discoveredStatusFor(d) {
|
|
|
222168
222804
|
* - NVR devices expose every channel via `ch{N}-main` / `ch{N}-sub`
|
|
222169
222805
|
* stream IDs (Slice 8); no operator choice is required
|
|
222170
222806
|
*/
|
|
222807
|
+
/**
|
|
222808
|
+
* Extra per-scan inputs for Reolink network discovery. Unlike Gree/Ecowitt, a Reolink camera needs
|
|
222809
|
+
* CREDENTIALS to be created — so the scan form collects them: the library uses them to enrich the
|
|
222810
|
+
* probe (model/UID via the authenticated API) and they are baked into each candidate so a one-click
|
|
222811
|
+
* adopt yields a working camera. `networkCidr` reaches cameras on another subnet (auto-detects the
|
|
222812
|
+
* local subnet when blank); ONVIF WS-Discovery is on by default since most Reolink cameras support it.
|
|
222813
|
+
*/
|
|
222814
|
+
function buildDiscoveryParamsFormSchema() {
|
|
222815
|
+
return { sections: [{
|
|
222816
|
+
id: "credentials",
|
|
222817
|
+
title: "Credentials",
|
|
222818
|
+
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.",
|
|
222819
|
+
columns: 2,
|
|
222820
|
+
fields: [{
|
|
222821
|
+
type: "text",
|
|
222822
|
+
key: "username",
|
|
222823
|
+
label: "Username",
|
|
222824
|
+
default: "admin",
|
|
222825
|
+
required: true
|
|
222826
|
+
}, {
|
|
222827
|
+
type: "password",
|
|
222828
|
+
key: "password",
|
|
222829
|
+
label: "Password",
|
|
222830
|
+
required: true,
|
|
222831
|
+
showToggle: true
|
|
222832
|
+
}]
|
|
222833
|
+
}, {
|
|
222834
|
+
id: "scan",
|
|
222835
|
+
title: "Scan options",
|
|
222836
|
+
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).",
|
|
222837
|
+
columns: 1,
|
|
222838
|
+
fields: [
|
|
222839
|
+
{
|
|
222840
|
+
type: "text",
|
|
222841
|
+
key: "networkCidr",
|
|
222842
|
+
label: "Network (CIDR)",
|
|
222843
|
+
required: false,
|
|
222844
|
+
placeholder: "192.168.20.0/24"
|
|
222845
|
+
},
|
|
222846
|
+
{
|
|
222847
|
+
type: "boolean",
|
|
222848
|
+
key: "enableOnvif",
|
|
222849
|
+
label: "Include ONVIF discovery",
|
|
222850
|
+
default: true
|
|
222851
|
+
},
|
|
222852
|
+
{
|
|
222853
|
+
type: "number",
|
|
222854
|
+
key: "timeoutMs",
|
|
222855
|
+
label: "Scan timeout (ms)",
|
|
222856
|
+
min: 1e3,
|
|
222857
|
+
max: 3e4,
|
|
222858
|
+
default: 5e3
|
|
222859
|
+
}
|
|
222860
|
+
]
|
|
222861
|
+
}] };
|
|
222862
|
+
}
|
|
222171
222863
|
function buildCreationFormSchema() {
|
|
222172
222864
|
return { sections: [
|
|
222173
222865
|
{
|
|
@@ -222779,6 +223471,11 @@ function isMeaningfulIdentifier(s) {
|
|
|
222779
223471
|
if (!s || s.length < 6) return false;
|
|
222780
223472
|
return new Set(s.toLowerCase().split("").filter((c) => c !== "0" && c !== "f")).size >= 1;
|
|
222781
223473
|
}
|
|
223474
|
+
/** Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
|
|
223475
|
+
* discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan. */
|
|
223476
|
+
function slugifyReolinkHost(host) {
|
|
223477
|
+
return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
223478
|
+
}
|
|
222782
223479
|
/**
|
|
222783
223480
|
* Patch `detection.deviceInfo` + `hostNetworkInfo` in-place with the
|
|
222784
223481
|
* post-login HOST identifiers. Two distinct gaps to fill:
|
|
@@ -222930,7 +223627,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
222930
223627
|
if (isMeaningfulIdentifier(mac)) return `mac-${mac}`;
|
|
222931
223628
|
const host = typeof cfg["host"] === "string" ? cfg["host"].trim() : "";
|
|
222932
223629
|
if (host.length > 0) {
|
|
222933
|
-
const slug = host
|
|
223630
|
+
const slug = slugifyReolinkHost(host);
|
|
222934
223631
|
if (slug.length > 0) return `host-${slug}`;
|
|
222935
223632
|
}
|
|
222936
223633
|
const hostLabel = host || "(unknown host)";
|
|
@@ -223044,6 +223741,61 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
223044
223741
|
});
|
|
223045
223742
|
await dev.materializeStreamSocket(camStreamId);
|
|
223046
223743
|
}
|
|
223744
|
+
async supportsDiscovery() {
|
|
223745
|
+
return true;
|
|
223746
|
+
}
|
|
223747
|
+
async getDiscoveryParamsSchema() {
|
|
223748
|
+
return buildDiscoveryParamsFormSchema();
|
|
223749
|
+
}
|
|
223750
|
+
async discoverDevices(input) {
|
|
223751
|
+
const p = input?.params ?? {};
|
|
223752
|
+
const username = typeof p["username"] === "string" ? p["username"].trim() : "";
|
|
223753
|
+
const password = typeof p["password"] === "string" ? p["password"] : "";
|
|
223754
|
+
const networkCidr = typeof p["networkCidr"] === "string" ? p["networkCidr"].trim() : "";
|
|
223755
|
+
const enableOnvif = p["enableOnvif"] !== false;
|
|
223756
|
+
const timeoutMs = typeof p["timeoutMs"] === "number" ? p["timeoutMs"] : void 0;
|
|
223757
|
+
const devices = await discoverReolinkDevices({
|
|
223758
|
+
logger: buildAutodetectLibLogger(this.ctx.logger),
|
|
223759
|
+
enableOnvifDiscovery: enableOnvif,
|
|
223760
|
+
...username ? { username } : {},
|
|
223761
|
+
...password ? { password } : {},
|
|
223762
|
+
...networkCidr ? { networkCidr } : {},
|
|
223763
|
+
...timeoutMs !== void 0 ? {
|
|
223764
|
+
udpBroadcastTimeoutMs: timeoutMs,
|
|
223765
|
+
onvifDiscoveryTimeoutMs: timeoutMs
|
|
223766
|
+
} : {}
|
|
223767
|
+
});
|
|
223768
|
+
this.ctx.logger.info("Reolink discovery complete", { meta: {
|
|
223769
|
+
count: devices.length,
|
|
223770
|
+
networkCidr: networkCidr || "local",
|
|
223771
|
+
enableOnvif
|
|
223772
|
+
} });
|
|
223773
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
223774
|
+
for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
|
|
223775
|
+
return [...byHost.values()].map((d) => {
|
|
223776
|
+
const displayName = d.name ?? d.model ?? d.host;
|
|
223777
|
+
return {
|
|
223778
|
+
stableId: `host-${slugifyReolinkHost(d.host)}`,
|
|
223779
|
+
type: DeviceType.Camera,
|
|
223780
|
+
suggestedName: displayName,
|
|
223781
|
+
prefilledConfig: {
|
|
223782
|
+
name: displayName,
|
|
223783
|
+
host: d.host,
|
|
223784
|
+
transport: "auto",
|
|
223785
|
+
...d.httpPort !== void 0 ? { port: d.httpPort } : {},
|
|
223786
|
+
...d.uid ? { uid: d.uid } : {},
|
|
223787
|
+
...username ? { username } : {},
|
|
223788
|
+
...password ? { password } : {}
|
|
223789
|
+
}
|
|
223790
|
+
};
|
|
223791
|
+
});
|
|
223792
|
+
}
|
|
223793
|
+
async adoptDiscoveredDevice(input) {
|
|
223794
|
+
return this.createDevice({
|
|
223795
|
+
type: DeviceType.Camera,
|
|
223796
|
+
config: input.candidate.prefilledConfig
|
|
223797
|
+
});
|
|
223798
|
+
}
|
|
223047
223799
|
async onGetCreationSchema(type) {
|
|
223048
223800
|
if (type !== DeviceType.Camera && type !== DeviceType.Hub) return null;
|
|
223049
223801
|
return buildCreationFormSchema();
|