@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.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-B3AOslwX.mjs
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
- discoverDevices: method(object({}), array(DiscoveryCandidateSchema), {
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(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
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()
@@ -18534,6 +18594,17 @@ var AvailableIntegrationTypeSchema = object({
18534
18594
  iconUrl: string().nullable(),
18535
18595
  color: string(),
18536
18596
  instanceMode: string(),
18597
+ /**
18598
+ * Integration wizard `mode` (LOCKED MODEL): `standalone` (create
18599
+ * immediately then add devices, no config step/button), `account` (config
18600
+ * step), or `broker` (broker step). Derived server-side by
18601
+ * `getAvailableTypes` when the addon manifest omits an explicit `mode`.
18602
+ */
18603
+ mode: _enum([
18604
+ "standalone",
18605
+ "account",
18606
+ "broker"
18607
+ ]),
18537
18608
  discoveryMode: string(),
18538
18609
  /**
18539
18610
  * Which integration-marker cap the addon declared, so the wizard can
@@ -21323,6 +21394,12 @@ Object.freeze({
21323
21394
  addonId: null,
21324
21395
  access: "create"
21325
21396
  },
21397
+ "deviceManager.adoptionListCandidateFilters": {
21398
+ capName: "device-manager",
21399
+ capScope: "system",
21400
+ addonId: null,
21401
+ access: "view"
21402
+ },
21326
21403
  "deviceManager.adoptionListCandidates": {
21327
21404
  capName: "device-manager",
21328
21405
  capScope: "system",
@@ -21371,12 +21448,30 @@ Object.freeze({
21371
21448
  addonId: null,
21372
21449
  access: "create"
21373
21450
  },
21451
+ "deviceManager.discoverAllProviders": {
21452
+ capName: "device-manager",
21453
+ capScope: "system",
21454
+ addonId: null,
21455
+ access: "create"
21456
+ },
21374
21457
  "deviceManager.discoverDevices": {
21375
21458
  capName: "device-manager",
21376
21459
  capScope: "system",
21377
21460
  addonId: null,
21378
21461
  access: "create"
21379
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
+ },
21380
21475
  "deviceManager.enable": {
21381
21476
  capName: "device-manager",
21382
21477
  capScope: "system",
@@ -21527,6 +21622,18 @@ Object.freeze({
21527
21622
  addonId: null,
21528
21623
  access: "create"
21529
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
+ },
21530
21637
  "deviceManager.registerDevice": {
21531
21638
  capName: "device-manager",
21532
21639
  capScope: "system",
@@ -21743,6 +21850,18 @@ Object.freeze({
21743
21850
  addonId: null,
21744
21851
  access: "view"
21745
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
+ },
21746
21865
  "deviceProvider.getStatus": {
21747
21866
  capName: "device-provider",
21748
21867
  capScope: "system",
@@ -164340,7 +164459,7 @@ ${scheduleItems}
164340
164459
  }), chimeId);
164341
164460
  }
164342
164461
  };
164343
- promisify(execFile);
164462
+ var execFileAsync = promisify(execFile);
164344
164463
  async function discoverViaUdpDirect(host, options) {
164345
164464
  if (!options.enableUdpDiscovery) return [];
164346
164465
  const logger = options.logger;
@@ -164410,6 +164529,170 @@ async function discoverViaUdpDirect(host, options) {
164410
164529
  });
164411
164530
  });
164412
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
+ }
164413
164696
  async function discoverViaUdpBroadcast(options) {
164414
164697
  if (!options.enableUdpDiscovery) return [];
164415
164698
  const logger = options.logger;
@@ -164497,6 +164780,359 @@ async function discoverViaUdpBroadcast(options) {
164497
164780
  });
164498
164781
  });
164499
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
+ }
164500
165136
  var ALL_UDP_DISCOVERY_METHODS = [
164501
165137
  "local-direct",
164502
165138
  "local-broadcast",
@@ -222148,6 +222784,62 @@ function discoveredStatusFor(d) {
222148
222784
  * - NVR devices expose every channel via `ch{N}-main` / `ch{N}-sub`
222149
222785
  * stream IDs (Slice 8); no operator choice is required
222150
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
+ }
222151
222843
  function buildCreationFormSchema() {
222152
222844
  return { sections: [
222153
222845
  {
@@ -222759,6 +223451,11 @@ function isMeaningfulIdentifier(s) {
222759
223451
  if (!s || s.length < 6) return false;
222760
223452
  return new Set(s.toLowerCase().split("").filter((c) => c !== "0" && c !== "f")).size >= 1;
222761
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
+ }
222762
223459
  /**
222763
223460
  * Patch `detection.deviceInfo` + `hostNetworkInfo` in-place with the
222764
223461
  * post-login HOST identifiers. Two distinct gaps to fill:
@@ -222910,7 +223607,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
222910
223607
  if (isMeaningfulIdentifier(mac)) return `mac-${mac}`;
222911
223608
  const host = typeof cfg["host"] === "string" ? cfg["host"].trim() : "";
222912
223609
  if (host.length > 0) {
222913
- const slug = host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
223610
+ const slug = slugifyReolinkHost(host);
222914
223611
  if (slug.length > 0) return `host-${slug}`;
222915
223612
  }
222916
223613
  const hostLabel = host || "(unknown host)";
@@ -223024,6 +223721,61 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
223024
223721
  });
223025
223722
  await dev.materializeStreamSocket(camStreamId);
223026
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
+ }
223027
223779
  async onGetCreationSchema(type) {
223028
223780
  if (type !== DeviceType.Camera && type !== DeviceType.Hub) return null;
223029
223781
  return buildCreationFormSchema();