diff --git a/vue/tools.vue b/vue/tools.vue index 9526c192..37497768 100644 --- a/vue/tools.vue +++ b/vue/tools.vue @@ -97,6 +97,46 @@ +
+

Network Device Scan

+

+ Scan your local subnet for OBK/Tasmota devices.
+

+ How to use + + Enter the base network (e.g. 192.168.0) and in the second field + the IP or IPs to scan:
+ - a single octet (e.g. 20)
   --> 192.168.0.20
+ - ranges using - (e.g. 20-25)
   --> 192.168.0.20 - .25
+ - octets separated by commas (like 20,23,25)
   --> hosts .20, .23, .25
+ - both ranges and single IPs separted by commas
+  (e.g. 20-23,27,30-32)
+    --> hosts .20, .21, .22, .23, .27, .30, .31, .32
+
+
+

+
+ +
+ +
+ + + + +
+
+ Scanning: {{ currentScanIP }} ({{ scanProgress }}/{{ scanTargetIPs.length }}) +
+
+
Found Devices:
+ +
+
@@ -126,6 +166,15 @@ C:'0', W:'0', ExternalGroupName:'SomeRandomGroup', + scanNet: '192.168.0', + scanList: '20-25', + scanTimeout: 500, + scanResults: [], + scanInProgress: false, + scanProgress: 0, + currentScanIP: "", + scanTargetIPs: [], + stopScanRequested: false, } }, methods:{ @@ -303,6 +352,115 @@ }); // Never forget the final catch! }, + async scanNetwork() { + this.scanResults = []; + this.scanInProgress = true; + this.scanProgress = 0; + this.currentScanIP = ""; + this.stopScanRequested = false; + this.scanTargetIPs = this.parseIPTargets(this.scanNet, this.scanList); + let found = 0; + for (let i = 0; i < this.scanTargetIPs.length; ++i) { + if (this.stopScanRequested) break; + let ip = this.scanTargetIPs[i]; + this.currentScanIP = ip; + try { + let url = `http://${ip}/cm?cmnd=STATUS%205`; + let res = await this.fetchWithTimeout(url, { timeout: this.scanTimeout }); + let data = null; + try { + data = await res.json(); + } catch (e) { + data = null; + } + if (data && data.StatusNET) { + // filter out most "useless" information - to identify even only Name, IP and MAC should be fine + const { DNSServer1, DNSServer2, Webserver, HTTP_API, WifiConfig, WifiPower, ...helper } = data.StatusNET; + this.scanResults.push({ + ip: ip, + statusNet: helper, + }); + } + } catch (err) { + // Ignore errors, do not push non-success + } + this.scanProgress = i + 1; + // Yield to UI and allow for stop + await new Promise(resolve => setTimeout(resolve, 30)); + } + this.scanInProgress = false; + this.currentScanIP = ""; + this.stopScanRequested = false; + }, + stopScan() { + this.stopScanRequested = true; + }, + + + // examples for possible IPs and ranges: + // + // + // single range, e.g. 192.168.0.10 - 192.168.0.20 + // Net = "192.168.0" + // Hosts = "10-20" + // + // single IPs, e.g. 192.168.0.10, 192.168.0.20, 192.168.0.25 and 192.168.0.30 + // Net = "192.168.0" + // Hosts = "10,20,25,30" + // + // single IPs and ranges, e.g. 192.168.0.10, 192.168.0.20 - 192.168.0.25, 192.168.0.30 and 192.168.0.40 - 192.168.0.50 + // Net = "192.168.0" + // Hosts = "10,20-25,30,40-50" + parseIPTargets(Net, List) { + // Net: e.g. "192.168.0" + // endList: e.g. "20-25,27-30,45" + Net = (Net || '').trim(); + List = (List || '').trim(); + if (!Net) return []; + let parts = Net.split('.'); + if (parts.length !== 3) return []; + let base = `${parts[0]}.${parts[1]}.${parts[2]}`; + let octetSet = new Set(); + + if (List) { + let items = List.split(','); + items.forEach(item => { + item = item.trim(); + if (item.includes('-')) { + // Handle range + console.log('item includes - ' + item); + let [startStr, endStr] = item.split('-').map(s => s.trim()); + let rangeStart = parseInt(startStr); + let rangeEnd = parseInt(endStr); + if (!isNaN(rangeStart) && !isNaN(rangeEnd) && rangeEnd >= rangeStart) { + for (let i = rangeStart; i <= rangeEnd; ++i) { + octetSet.add(i); + } + } + } else { + // Single number + let val = parseInt(item); + if (!isNaN(val)) { + octetSet.add(val); + } + } + }); + } + + // Filter valid octets (1..255), sort them + let allOctets = Array.from(octetSet).filter(o => o >= 1 && o <= 255).sort((a, b) => a - b); + + return allOctets.map(octet => `${base}.${octet}`); + }, + fetchWithTimeout(resource, options = {}) { + const { timeout = 1500 } = options; + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + return fetch(resource, { + ...options, + signal: controller.signal + }).finally(() => clearTimeout(id)); + } }, mounted (){ this.msg = 'fred';