@camstack/addon-provider-reolink 1.1.10 → 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 CHANGED
@@ -4655,7 +4655,7 @@ function _instanceof(cls, params = {}) {
4655
4655
  return inst;
4656
4656
  }
4657
4657
  //#endregion
4658
- //#region ../types/dist/sleep-B3AOslwX.mjs
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
- discoverDevices: method(object({}), array(DiscoveryCandidateSchema), {
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(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
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()
@@ -21339,6 +21399,12 @@ Object.freeze({
21339
21399
  addonId: null,
21340
21400
  access: "create"
21341
21401
  },
21402
+ "deviceManager.adoptionListCandidateFilters": {
21403
+ capName: "device-manager",
21404
+ capScope: "system",
21405
+ addonId: null,
21406
+ access: "view"
21407
+ },
21342
21408
  "deviceManager.adoptionListCandidates": {
21343
21409
  capName: "device-manager",
21344
21410
  capScope: "system",
@@ -21387,12 +21453,30 @@ Object.freeze({
21387
21453
  addonId: null,
21388
21454
  access: "create"
21389
21455
  },
21456
+ "deviceManager.discoverAllProviders": {
21457
+ capName: "device-manager",
21458
+ capScope: "system",
21459
+ addonId: null,
21460
+ access: "create"
21461
+ },
21390
21462
  "deviceManager.discoverDevices": {
21391
21463
  capName: "device-manager",
21392
21464
  capScope: "system",
21393
21465
  addonId: null,
21394
21466
  access: "create"
21395
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
+ },
21396
21480
  "deviceManager.enable": {
21397
21481
  capName: "device-manager",
21398
21482
  capScope: "system",
@@ -21543,6 +21627,18 @@ Object.freeze({
21543
21627
  addonId: null,
21544
21628
  access: "create"
21545
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
+ },
21546
21642
  "deviceManager.registerDevice": {
21547
21643
  capName: "device-manager",
21548
21644
  capScope: "system",
@@ -21759,6 +21855,18 @@ Object.freeze({
21759
21855
  addonId: null,
21760
21856
  access: "view"
21761
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
+ },
21762
21870
  "deviceProvider.getStatus": {
21763
21871
  capName: "device-provider",
21764
21872
  capScope: "system",
@@ -164371,7 +164479,7 @@ ${scheduleItems}
164371
164479
  }), chimeId);
164372
164480
  }
164373
164481
  };
164374
- (0, util.promisify)(child_process.execFile);
164482
+ var execFileAsync = (0, util.promisify)(child_process.execFile);
164375
164483
  async function discoverViaUdpDirect(host, options) {
164376
164484
  if (!options.enableUdpDiscovery) return [];
164377
164485
  const logger = options.logger;
@@ -164441,6 +164549,170 @@ async function discoverViaUdpDirect(host, options) {
164441
164549
  });
164442
164550
  });
164443
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
+ }
164444
164716
  async function discoverViaUdpBroadcast(options) {
164445
164717
  if (!options.enableUdpDiscovery) return [];
164446
164718
  const logger = options.logger;
@@ -164528,6 +164800,359 @@ async function discoverViaUdpBroadcast(options) {
164528
164800
  });
164529
164801
  });
164530
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
+ }
164531
165156
  var ALL_UDP_DISCOVERY_METHODS = [
164532
165157
  "local-direct",
164533
165158
  "local-broadcast",
@@ -222179,6 +222804,62 @@ function discoveredStatusFor(d) {
222179
222804
  * - NVR devices expose every channel via `ch{N}-main` / `ch{N}-sub`
222180
222805
  * stream IDs (Slice 8); no operator choice is required
222181
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
+ }
222182
222863
  function buildCreationFormSchema() {
222183
222864
  return { sections: [
222184
222865
  {
@@ -222790,6 +223471,11 @@ function isMeaningfulIdentifier(s) {
222790
223471
  if (!s || s.length < 6) return false;
222791
223472
  return new Set(s.toLowerCase().split("").filter((c) => c !== "0" && c !== "f")).size >= 1;
222792
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
+ }
222793
223479
  /**
222794
223480
  * Patch `detection.deviceInfo` + `hostNetworkInfo` in-place with the
222795
223481
  * post-login HOST identifiers. Two distinct gaps to fill:
@@ -222941,7 +223627,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
222941
223627
  if (isMeaningfulIdentifier(mac)) return `mac-${mac}`;
222942
223628
  const host = typeof cfg["host"] === "string" ? cfg["host"].trim() : "";
222943
223629
  if (host.length > 0) {
222944
- const slug = host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
223630
+ const slug = slugifyReolinkHost(host);
222945
223631
  if (slug.length > 0) return `host-${slug}`;
222946
223632
  }
222947
223633
  const hostLabel = host || "(unknown host)";
@@ -223055,6 +223741,61 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
223055
223741
  });
223056
223742
  await dev.materializeStreamSocket(camStreamId);
223057
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
+ }
223058
223799
  async onGetCreationSchema(type) {
223059
223800
  if (type !== DeviceType.Camera && type !== DeviceType.Hub) return null;
223060
223801
  return buildCreationFormSchema();