Skip to content

Commit 70fd84f

Browse files
authored
Merge pull request #2061 from Jixabon/feature/add-api-datapoints
Feature/add api datapoints
2 parents 560b4c2 + f253c57 commit 70fd84f

File tree

5 files changed

+160
-9
lines changed

5 files changed

+160
-9
lines changed

api/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DNSMASQ_LEASES = '/var/lib/misc/dnsmasq.leases'

api/main.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,16 @@ async def get_system(api_key: APIKey = Depends(auth.get_api_key)):
3535
'uptime': system.uptime(),
3636
'systime': system.systime(),
3737
'usedMemory': system.usedMemory(),
38+
'usedDisk': system.usedDisk(),
3839
'processorCount': system.processorCount(),
3940
'LoadAvg1Min': system.LoadAvg1Min(),
4041
'systemLoadPercentage': system.systemLoadPercentage(),
4142
'systemTemperature': system.systemTemperature(),
4243
'hostapdStatus': system.hostapdStatus(),
4344
'operatingSystem': system.operatingSystem(),
4445
'kernelVersion': system.kernelVersion(),
45-
'rpiRevision': system.rpiRevision()
46+
'rpiRevision': system.rpiRevision(),
47+
'raspapVersion': system.raspapVersion()
4648
}
4749

4850
@app.get("/ap", tags=["accesspoint/hotspot"])
@@ -58,6 +60,7 @@ async def get_ap(api_key: APIKey = Depends(auth.get_api_key)):
5860
'channel': ap.channel(),
5961
'hw_mode': ap.hw_mode(),
6062
'ieee80211n': ap.ieee80211n(),
63+
'frequency_band': ap.frequency_band(),
6164
'wpa_passphrase': ap.wpa_passphrase(),
6265
'interface': ap.interface(),
6366
'wpa': ap.wpa(),
@@ -66,11 +69,20 @@ async def get_ap(api_key: APIKey = Depends(auth.get_api_key)):
6669
'ignore_broadcast_ssid': ap.ignore_broadcast_ssid()
6770
}
6871

72+
@app.get("/clients", tags=["Clients"])
73+
async def get_clients(api_key: APIKey = Depends(auth.get_api_key)):
74+
return{
75+
'active_clients_amount': client.get_active_clients_amount(),
76+
'active_wireless_clients_amount': client.get_active_wireless_clients_amount(),
77+
'active_ethernet_clients_amount': client.get_active_ethernet_clients_amount(),
78+
'active_clients': json.loads(client.get_active_clients())
79+
}
80+
6981
@app.get("/clients/{wireless_interface}", tags=["Clients"])
7082
async def get_clients(wireless_interface, api_key: APIKey = Depends(auth.get_api_key)):
7183
return{
72-
'active_clients_amount': client.get_active_clients_amount(wireless_interface),
73-
'active_clients': json.loads(client.get_active_clients(wireless_interface))
84+
'active_clients_amount': client.get_active_clients_amount_by_interface(wireless_interface),
85+
'active_clients': json.loads(client.get_active_clients_by_interface(wireless_interface))
7486
}
7587

7688
@app.get("/dhcp", tags=["DHCP"])

api/modules/ap.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import subprocess
2+
import re
23
import json
34

45
def driver():
@@ -31,6 +32,21 @@ def hw_mode():
3132
def ieee80211n():
3233
return subprocess.run("cat /etc/hostapd/hostapd.conf | grep ieee80211n= | cut -d'=' -f2", shell=True, capture_output=True, text=True).stdout.strip()
3334

35+
def frequency_band():
36+
ap_interface = interface()
37+
result = subprocess.run(["iw", "dev", ap_interface, "info"], capture_output=True, text=True)
38+
match = re.search(r"channel\s+\d+\s+\((\d+)\s+MHz\)", result.stdout)
39+
40+
if match:
41+
frequency = int(match.group(1))
42+
43+
if 2400 <= frequency < 2500:
44+
return "2.4"
45+
elif 5000 <= frequency < 6000:
46+
return "5"
47+
48+
return None
49+
3450
def wpa_passphrase():
3551
return subprocess.run("sed -En 's/wpa_passphrase=(.*)/\\1/p' /etc/hostapd/hostapd.conf", shell=True, capture_output=True, text=True).stdout.strip()
3652

api/modules/client.py

Lines changed: 121 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,138 @@
11
import subprocess
2+
import re
3+
import os
24
import json
35

4-
def get_active_clients_amount(interface):
6+
import modules.ap as ap
7+
import config
8+
9+
def get_active_wireless_clients_mac(interface):
10+
station_dump = subprocess.run(["iw", "dev", interface, "station", "dump"], capture_output=True, text=True)
11+
12+
macs = []
13+
for line in station_dump.stdout.splitlines():
14+
if line.startswith("Station "):
15+
# Typical format: Station aa:bb:cc:dd:ee:ff (on wlan0)
16+
parts = line.split()
17+
if len(parts) >= 2:
18+
mac = parts[1].strip()
19+
# Optional: basic validation
20+
if len(mac) == 17 and mac.count(":") == 5:
21+
macs.append(mac.upper())
22+
23+
return macs
24+
25+
def get_active_wireless_clients_amount():
26+
ap_interface = ap.interface()
27+
macs = get_active_wireless_clients_mac(ap_interface)
28+
29+
return len(macs)
30+
31+
def get_active_ethernet_clients_mac():
32+
arp_macs = []
33+
34+
arp_output = subprocess.run(['ip', 'neight', 'show'], capture_output=True, text=True)
35+
if arp_output.stdout:
36+
for line in arp_output.stdout.splitlines():
37+
line = line.strip()
38+
if not line:
39+
continue
40+
41+
# Matches lines like:
42+
# 192.168.100.45 dev enp3s0 lladdr 3c:97:0e:12:34:56 REACHABLE
43+
# 192.168.1.120 dev eth0 lladdr 00:1a:2b:3c:4d:5e DELAY
44+
match = re.match(
45+
r'^(\S+)\s+dev\s+(eth[0-9]+|en\w+)\s+lladdr\s+(\S+)\s+(REACHABLE|DELAY|PROBE)',
46+
line
47+
)
48+
if match:
49+
mac = match.group(3).upper()
50+
arp_macs.append(mac)
51+
52+
lease_macs = []
53+
54+
if os.path.isfile(config.DNSMASQ_LEASES):
55+
try:
56+
with open(config.DNSMASQ_LEASES, encoding="utf-8", errors="ignore") as f:
57+
for line in f:
58+
line = line.strip()
59+
if not line or line.startswith("#"):
60+
continue
61+
fields = line.split()
62+
if len(fields) >= 3:
63+
# format: expiry MAC IP hostname ...
64+
mac = fields[1].upper()
65+
lease_macs.append(mac)
66+
except Exception:
67+
pass
68+
69+
active_ethernet_macs = []
70+
for mac in arp_macs:
71+
if mac in lease_macs and mac not in active_ethernet_macs:
72+
active_ethernet_macs.append(mac)
73+
74+
75+
return active_ethernet_macs
76+
77+
def get_active_ethernet_clients_amount():
78+
eth_macs = get_active_ethernet_clients_mac()
79+
return len(eth_macs)
80+
81+
def get_active_clients_amount():
82+
wireless_clients_count = get_active_wireless_clients_amount()
83+
ethernet_clients_count = get_active_ethernet_clients_amount()
84+
85+
return wireless_clients_count + ethernet_clients_count
86+
87+
def get_active_clients():
88+
ap_interface = ap.interface()
89+
wireless_macs = get_active_wireless_clients_mac(ap_interface)
90+
ethernet_macs = get_active_ethernet_clients_mac()
91+
92+
arp_output = subprocess.run(['arp', '-i', ap_interface], capture_output=True, text=True)
93+
arp_mac_addresses = set(line.split()[2] for line in arp_output.stdout.splitlines()[1:])
94+
95+
dnsmasq_output = subprocess.run(['cat', config.DNSMASQ_LEASES], capture_output=True, text=True)
96+
active_clients = []
97+
98+
for line in dnsmasq_output.stdout.splitlines():
99+
fields = line.split()
100+
mac_address = fields[1]
101+
102+
if mac_address in arp_mac_addresses:
103+
normalized_mac = mac_address.upper()
104+
is_wireless = True if normalized_mac in wireless_macs else False
105+
is_ethernet = True if normalized_mac in ethernet_macs else False
106+
107+
client_data = {
108+
"timestamp": int(fields[0]),
109+
"mac_address": fields[1],
110+
"ip_address": fields[2],
111+
"hostname": fields[3],
112+
"client_id": fields[4],
113+
"connection_type": 'wireless' if is_wireless else ('ethernet' if is_ethernet else 'unknown')
114+
}
115+
active_clients.append(client_data)
116+
117+
json_output = json.dumps(active_clients, indent=2)
118+
return json_output
119+
120+
def get_active_clients_amount_by_interface(interface):
5121
arp_output = subprocess.run(['arp', '-i', interface], capture_output=True, text=True)
6-
mac_addresses = mac_addresses = set(line.split()[2] for line in arp_output.stdout.splitlines()[1:])
122+
mac_addresses = set(line.split()[2] for line in arp_output.stdout.splitlines()[1:])
7123

8124
if mac_addresses:
9125
grep_pattern = '|'.join(mac_addresses)
10-
output = subprocess.run(['grep', '-iwE', grep_pattern, '/var/lib/misc/dnsmasq.leases'], capture_output=True, text=True)
126+
output = subprocess.run(['grep', '-iwE', grep_pattern, config.DNSMASQ_LEASES], capture_output=True, text=True)
11127
return len(output.stdout.splitlines())
12128
else:
13129
return 0
14130

15-
def get_active_clients(interface):
131+
def get_active_clients_by_interface(interface):
16132
arp_output = subprocess.run(['arp', '-i', interface], capture_output=True, text=True)
17133
arp_mac_addresses = set(line.split()[2] for line in arp_output.stdout.splitlines()[1:])
18134

19-
dnsmasq_output = subprocess.run(['cat', '/var/lib/misc/dnsmasq.leases'], capture_output=True, text=True)
135+
dnsmasq_output = subprocess.run(['cat', config.DNSMASQ_LEASES], capture_output=True, text=True)
20136
active_clients = []
21137

22138
for line in dnsmasq_output.stdout.splitlines():

api/modules/system.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ def systime():
5353
def usedMemory():
5454
return round(float(subprocess.run("free -m | awk 'NR==2{total=$2 ; used=$3 } END { print used/total*100}'", shell=True, capture_output=True, text=True).stdout.strip()),2)
5555

56+
def usedDisk():
57+
return float(subprocess.run("df -h / | awk 'NR==2 {print $5}' | sed 's/%$//'", shell=True, capture_output=True, text=True).stdout.strip())
58+
5659
def processorCount():
5760
return int(subprocess.run("nproc --all", shell=True, capture_output=True, text=True).stdout.strip())
5861

@@ -83,4 +86,7 @@ def rpiRevision():
8386
try:
8487
return revisions[output]
8588
except KeyError:
86-
return 'Unknown Device'
89+
return 'Unknown Device'
90+
91+
def raspapVersion():
92+
return subprocess.run("grep 'RASPI_VERSION' /var/www/html/includes/defaults.php | awk -F\"'\" '{print $4}'", shell=True, capture_output=True, text=True).stdout.strip()

0 commit comments

Comments
 (0)