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
+
+
+
+
+ Network:
+
+ Hosts:
+
+ Timeout (ms):
+
+ Scan
+ Stop
+
+
+ Scanning: {{ currentScanIP }} ({{ scanProgress }}/{{ scanTargetIPs.length }})
+
+
+
@@ -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';