86 lines
2.7 KiB
Python
86 lines
2.7 KiB
Python
import glob
|
|
import re
|
|
import shutil
|
|
import socket
|
|
import subprocess
|
|
import time
|
|
from typing import Any
|
|
|
|
import psutil
|
|
|
|
_wifi_cache: dict[str, tuple[float, str | None]] = {}
|
|
_WIFI_TTL = 15.0
|
|
_SSID_RE = re.compile(r"SSID:\s+(\S.*)")
|
|
|
|
|
|
def _wifi_ifaces() -> set[str]:
|
|
"""Return the names of interfaces that are wireless.
|
|
|
|
Returns:
|
|
Interface names having a /sys/class/net/<name>/wireless entry.
|
|
"""
|
|
return {p.split("/")[-2] for p in glob.glob("/sys/class/net/*/wireless")}
|
|
|
|
|
|
def _ssid(iface: str) -> str | None:
|
|
r"""Get the SSID currently associated on a wifi interface.
|
|
|
|
Shells out to `iw dev <iface> link` and matches the unquoted
|
|
`SSID: name` line; the working regex is `SSID:\s+(\S.*)` (a `$` anchor
|
|
would only match the final line of the output without MULTILINE). The
|
|
result is
|
|
cached per interface for 15 s so the 2 s poll doesn't spawn a
|
|
subprocess every cycle.
|
|
|
|
Args:
|
|
iface: network interface name.
|
|
|
|
Returns:
|
|
The SSID, or None if not associated or `iw` is unavailable.
|
|
"""
|
|
hit = _wifi_cache.get(iface)
|
|
now = time.monotonic()
|
|
if hit is not None and now - hit[0] < _WIFI_TTL:
|
|
return hit[1]
|
|
ssid: str | None = None
|
|
if shutil.which("iw"):
|
|
try:
|
|
out = subprocess.run(
|
|
["iw", "dev", iface, "link"], capture_output=True, text=True, timeout=3, check=False
|
|
).stdout
|
|
m = _SSID_RE.search(out)
|
|
if m:
|
|
ssid = m.group(1).strip().strip('"') or None
|
|
except (OSError, subprocess.SubprocessError):
|
|
pass
|
|
_wifi_cache[iface] = (now, ssid)
|
|
return ssid
|
|
|
|
|
|
def sample() -> dict[str, Any | None]:
|
|
"""Collect interface list and wifi association for the overview page.
|
|
|
|
Only interfaces that are up and are not the loopback are included;
|
|
each entry carries its IPv4 addresses. The first up wifi interface
|
|
(alphabetical order) provides the displayed SSID.
|
|
|
|
Returns:
|
|
A dict with "net_ifaces" (list of {name, ipv4}) and "net_wifi"
|
|
({iface, ssid} or None).
|
|
"""
|
|
addrs = psutil.net_if_addrs()
|
|
stats = psutil.net_if_stats()
|
|
wifi_set = _wifi_ifaces()
|
|
ifaces: list[dict[str, Any]] = []
|
|
wifi: dict[str, Any] | None = None
|
|
for name in sorted(addrs):
|
|
if name == "lo":
|
|
continue
|
|
st = stats.get(name)
|
|
if st is None or not bool(st.isup):
|
|
continue
|
|
ipv4 = [a.address for a in addrs[name] if a.family == socket.AF_INET]
|
|
ifaces.append({"name": name, "ipv4": ipv4})
|
|
if name in wifi_set and wifi is None:
|
|
wifi = {"iface": name, "ssid": _ssid(name)}
|
|
return {"net_ifaces": ifaces, "net_wifi": wifi}
|