WiFi: Refactor statd (python) implementation

* Add support for AP (list connected stations)
* Add scan-mode (Scan without create a fully configured station)

Signed-off-by: Mattias Walström <lazzer@gmail.com>
This commit is contained in:
Mattias Walström
2026-01-02 22:32:57 +01:00
parent c2cfb2e1b4
commit da37904108
7 changed files with 1047 additions and 126 deletions
+174 -44
View File
@@ -190,12 +190,6 @@ class PadNtpSource:
poll = 14
class PadWifiScan:
ssid = 40
encryption = 30
signal = 9
class PadLldp:
interface = 16
rem_idx = 10
@@ -544,7 +538,7 @@ class Decore():
@staticmethod
def bright_green(txt):
return Decore.decorate("1;32", txt, "39")
return Decore.decorate("1;32", txt, "0")
@staticmethod
def yellow(txt):
@@ -1248,20 +1242,73 @@ class Iface:
print(row)
def pr_wifi_ssids(self):
hdr = (f"{'SSID':<{PadWifiScan.ssid}}"
f"{'ENCRYPTION':<{PadWifiScan.encryption}}"
f"{'SIGNAL':<{PadWifiScan.signal}}")
print("\nAVAILABLE NETWORKS:")
ssid_table = SimpleTable([
Column('SSID'),
Column('SECURITY'),
Column('SIGNAL'),
Column('CHANNEL')
])
print(Decore.invert(hdr))
results = self.wifi.get("scan-results", {})
station = self.wifi.get("station", {})
results = station.get("scan-results", {})
for result in results:
encstr = ", ".join(result["encryption"])
status = rssi_to_status(result["rssi"])
row = f"{result['ssid']:<{PadWifiScan.ssid}}"
row += f"{encstr:<{PadWifiScan.encryption}}"
row += f"{status:<{PadWifiScan.signal}}"
encstr = ", ".join(result.get("encryption", ["Unknown"]))
status = rssi_to_status(result.get("rssi", -100))
channel = result.get("channel", "?")
print(row)
ssid_table.row(result.get('ssid', 'Hidden'), encstr, status, channel)
ssid_table.print()
def pr_wifi_stations(self):
"""Display connected stations for AP mode"""
if not self.wifi:
return
# Get stations from access-point container
ap = self.wifi.get("access-point", {})
stations_data = ap.get("stations", {})
stations = stations_data.get("station", [])
if not stations:
return
print("\nCONNECTED STATIONS:")
stations_table = SimpleTable([
Column('MAC'),
Column('SIGNAL'),
Column('TIME'),
Column('RX PKT'),
Column('TX PKT'),
Column('RX BYTES'),
Column('TX BYTES'),
Column('RX SPEED'),
Column('TX SPEED')
])
for station in stations:
mac = station.get("mac-address", "unknown")
rssi = station.get("rssi")
signal_str = rssi_to_status(rssi) if rssi is not None else "------"
conn_time = station.get("connected-time", 0)
time_str = f"{conn_time}s"
rx_pkt = station.get("rx-packets", 0)
tx_pkt = station.get("tx-packets", 0)
rx_bytes = station.get("rx-bytes", 0)
tx_bytes = station.get("tx-bytes", 0)
# Speed in 100 kbit/s units, convert to Mbps for display
rx_speed = station.get("rx-speed", 0)
tx_speed = station.get("tx-speed", 0)
rx_speed_str = f"{rx_speed / 10:.1f}" if rx_speed else "-"
tx_speed_str = f"{tx_speed / 10:.1f}" if tx_speed else "-"
stations_table.row(mac, signal_str, time_str, rx_pkt, tx_pkt,
rx_bytes, tx_bytes, rx_speed_str, tx_speed_str)
stations_table.print()
def pr_proto_wifi(self, pipe=''):
@@ -1269,20 +1316,35 @@ class Iface:
print(row)
ssid = None
rssi = None
mode = None
if self.wifi:
rssi=self.wifi.get("rssi")
ssid=self.wifi.get("ssid")
if ssid is None:
ssid="------"
if rssi is None:
signal="------"
# Detect mode: AP has "stations", Station has "rssi" or "scan-results"
ap=self.wifi.get("access-point", {})
if ap:
ssid = ap.get("ssid", "------")
mode = "AP"
stations_data = ap.get("stations", {})
stations = stations_data.get("station", [])
station_count = len(stations)
data_str = f"{mode}, ssid: {ssid}, stations: {station_count}"
else:
station=self.wifi.get("station", {})
ssid = station.get("ssid", "------")
rssi = station.get("rssi")
mode = "Station"
if rssi is not None:
signal = rssi_to_status(rssi)
data_str = f"{mode}, ssid: {ssid}, signal: {signal}"
else:
data_str = f"{mode}, ssid: {ssid}"
else:
signal=rssi_to_status(rssi)
data_str = f"ssid: {ssid}, signal: {signal}"
data_str = "ssid: ------"
row = f"{'':<{Pad.iface}}"
row = f"{'':<{Pad.flags}}"
row += f"{pipe:<{Pad.iface}}"
row = f"{'':<{Pad.flags}}"
row += f"{pipe:<{Pad.iface}}"
row += f"{'wifi':<{Pad.proto}}"
row += f"{'':<{Pad.state}}{data_str}"
print(row)
@@ -1550,13 +1612,37 @@ class Iface:
else:
print(f"{'ipv6 addresses':<{20}}:")
if self.in_octets and self.out_octets:
print(f"{'in-octets':<{20}}: {self.in_octets}")
print(f"{'out-octets':<{20}}: {self.out_octets}")
frame = get_json_data([], self.data,'ieee802-ethernet-interface:ethernet',
'statistics', 'frame')
if self.wifi:
ssid=self.wifi.get('ssid', "----")
rssi=self.wifi.get('rssi', "----")
print(f"{'SSID':<{20}}: {ssid}")
print(f"{'Signal':<{20}}: {rssi}")
print("")
self.pr_wifi_ssids()
# Detect mode: AP has "stations", Station has "rssi" or "scan-results"
ap = self.wifi.get('access-point')
if ap:
mode = "access-point"
ssid = ap.get('ssid', "----")
stations_data = ap.get("stations", {})
stations = stations_data.get("station", [])
print(f"{'mode':<{20}}: {mode}")
print(f"{'ssid':<{20}}: {ssid}")
print(f"{'connected stations':<{20}}: {len(stations)}")
self.pr_wifi_stations()
else:
mode = "station"
station = self.wifi.get('station', {})
rssi = station.get('rssi')
ssid = station.get('ssid', "----")
print(f"{'mode':<{20}}: {mode}")
print(f"{'ssid':<{20}}: {ssid}")
if rssi is not None:
signal_status = rssi_to_status(rssi)
print(f"{'signal':<{20}}: {rssi} dBm ({signal_status})")
if "scan-results" in station:
self.pr_wifi_ssids()
if self.gre:
print(f"{'local address':<{20}}: {self.gre['local']}")
@@ -1567,12 +1653,6 @@ class Iface:
print(f"{'remote address':<{20}}: {self.vxlan['remote']}")
print(f"{'VxLAN id':<{20}}: {self.vxlan['vni']}")
if self.in_octets and self.out_octets:
print(f"{'in-octets':<{20}}: {self.in_octets}")
print(f"{'out-octets':<{20}}: {self.out_octets}")
frame = get_json_data([], self.data,'ieee802-ethernet-interface:ethernet',
'statistics', 'frame')
if frame:
print("")
for key, val in frame.items():
@@ -1953,9 +2033,6 @@ def show_services(json):
services_data = get_json_data({}, json, 'ietf-system:system-state', 'infix-system:services')
services = services_data.get("service", [])
# This is the first usage of simple table. I assume this will be
# copied so I left a lot of comments. If you copy it feel free
# to be less verbose..
service_table = SimpleTable([
Column('NAME'),
Column('STATUS'),
@@ -2005,9 +2082,10 @@ def show_hardware(json):
motherboard = [c for c in components if c.get("class") == "iana-hardware:chassis"]
usb_ports = [c for c in components if c.get("class") == "infix-hardware:usb"]
sensors = [c for c in components if c.get("class") == "iana-hardware:sensor"]
wifi_radios = [c for c in components if c.get("class") == "infix-hardware:wifi"]
# Determine overall width (use the wider of the two sections)
width = max(PadUsbPort.table_width(), PadSensor.table_width())
width = max(PadUsbPort.table_width(), PadSensor.table_width(), 100)
# Display full-width inverted heading
print(Decore.invert(f"{'HARDWARE COMPONENTS':<{width}}"))
@@ -2027,6 +2105,56 @@ def show_hardware(json):
if board.get("hardware-rev"):
print(f"Hardware Revision : {board['hardware-rev']}")
if wifi_radios:
Decore.title("WiFi radios", width)
radios_table = SimpleTable([
Column('NAME'),
Column('MANUFACTURER'),
Column('BANDS', 'right'),
Column('STANDARDS', 'right'),
Column('MAX AP', 'right')
])
for component in wifi_radios:
phy = component.get("name", "")
manufacturer = component.get("mfg-name", "Unknown")
radio_data = component.get("infix-hardware:wifi-radio", {})
bands = radio_data.get("bands", [])
band_names = []
has_ht = False
has_vht = False
has_he = False
for band in bands:
if band.get("name"):
band_names.append(band["name"])
if band.get("ht-capable"):
has_ht = True
if band.get("vht-capable"):
has_vht = True
if band.get("he-capable"):
has_he = True
bands_str = "/".join(band_names) if band_names else "Unknown"
standards = []
if has_ht:
standards.append("11n")
if has_vht:
standards.append("11ac")
if has_he:
standards.append("11ax")
standard_str = "/".join(standards) if standards else "Unknown"
max_if = radio_data.get("max-interfaces", {})
max_ap = max_if.get('ap', 'N/A') if max_if else 'N/A'
radios_table.row(phy, manufacturer, bands_str, standard_str, max_ap)
radios_table.print()
if usb_ports:
Decore.title("USB Ports", width)
hdr = (f"{'NAME':<{PadUsbPort.name}}"
@@ -4342,6 +4470,8 @@ def main():
show_firewall_logs(args.limit)
elif args.command == "show-ntp":
show_ntp(json_data)
elif args.command == "show-wifi-radio":
show_wifi_radio(json_data)
elif args.command == "show-bfd":
show_bfd(json_data)
elif args.command == "show-bfd-status":
+3
View File
@@ -87,6 +87,9 @@ def main():
elif args.model == 'ietf-bfd-ip-sh':
from . import ietf_bfd_ip_sh
yang_data = ietf_bfd_ip_sh.operational()
elif args.model == 'infix-wifi-radio':
from . import infix_wifi_radio
yang_data = infix_wifi_radio.operational()
else:
common.LOG.warning("Unsupported model %s", args.model)
sys.exit(1)
+210 -51
View File
@@ -1,6 +1,8 @@
import datetime
import os
import glob
import re
import sys
from .common import insert, YangDate
from .host import HOST
@@ -150,65 +152,44 @@ def normalize_sensor_name(name):
def get_wifi_phy_info():
"""
Discover WiFi PHYs and map them to bands and interface names.
Discover WiFi PHYs using iw list command.
Returns dict: {phy_name: {band: str, iface: str, description: str}}
Example: {"phy0": {"band": "2.4 GHz", "iface": "wlan0", "description": "WiFi Radio (2.4 GHz)"}}
Example: {"radio0": {"band": "2.4 GHz", "iface": "wlan0", "description": "WiFi Radio (2.4 GHz)"}}
"""
phy_info = {}
try:
# Enumerate PHYs from /sys/class/ieee80211/
ieee80211_path = "/sys/class/ieee80211"
if not os.path.exists(ieee80211_path):
# Use iw.py to list all PHYs
phys = HOST.run_json(("/usr/libexec/infix/iw.py", "list"), default=[])
if not phys:
return phy_info
for phy in os.listdir(ieee80211_path):
if not phy.startswith("phy"):
continue
# Initialize PHY info for each PHY
for phy in phys:
phy_info[phy] = {"band": "Unknown", "iface": None, "description": None}
phy_path = os.path.join(ieee80211_path, phy)
info = {"band": "Unknown", "iface": None, "description": None}
# Create a mapping from PHY number to PHY name
phy_num_to_name = {}
for phy_name in phy_info.keys():
# Extract number from radio/phy name (e.g., "0" from "radio0" or "phy0")
num_match = re.search(r'(\d+)$', phy_name)
if num_match:
phy_num = num_match.group(1)
phy_num_to_name[phy_num] = phy_name
# Try to determine band from device path or hwmon name
# The hwmon device usually tells us: mt7915_phy0, mt7915_phy1, etc.
# We'll check supported frequencies to determine band
try:
# Read supported bands - check if device supports 5 GHz
# Most dual-band chips expose phy0 as 2.4 GHz and phy1 as 5 GHz
device_path = os.path.join(phy_path, "device")
if os.path.exists(device_path):
# Simple heuristic: phy0 is usually 2.4 GHz, phy1 is 5 GHz
# This works for most MediaTek chips (mt7915, mt7921, etc.)
if phy == "phy0":
info["band"] = "2.4 GHz"
elif phy == "phy1":
info["band"] = "5 GHz"
elif phy == "phy2":
info["band"] = "6 GHz" # WiFi 6E
except:
pass
# Find associated virtual interfaces using iw.py dev
dev_map = HOST.run_json(("/usr/libexec/infix/iw.py", "dev"), default={})
# Find associated interface by checking which interface has a phy80211 link to this PHY
try:
net_path = "/sys/class/net"
if os.path.exists(net_path):
for iface in os.listdir(net_path):
phy_link = os.path.join(net_path, iface, "phy80211")
if os.path.islink(phy_link):
# Read the link target and extract PHY name
try:
link_target = os.readlink(phy_link)
linked_phy = os.path.basename(link_target)
if linked_phy == phy:
info["iface"] = iface
break
except:
continue
except:
pass
# dev_map is a dict mapping PHY numbers to list of interfaces
for phy_num, interfaces in dev_map.items():
phy_name = phy_num_to_name.get(phy_num)
if phy_name and phy_name in phy_info and interfaces:
# Use the first interface
phy_info[phy_name]["iface"] = interfaces[0]
# Build description
# Build descriptions
for phy, info in phy_info.items():
if info["iface"] and info["band"] != "Unknown":
info["description"] = f"WiFi Radio {info['iface']} ({info['band']})"
elif info["band"] != "Unknown":
@@ -218,8 +199,6 @@ def get_wifi_phy_info():
else:
info["description"] = "WiFi Radio"
phy_info[phy] = info
except Exception:
pass
@@ -257,6 +236,12 @@ def hwmon_sensor_components():
continue
device_name = HOST.read(name_path).strip()
# Check if device/name exists (e.g., for WiFi radios) and use that instead
device_name_path = os.path.join(hwmon_path, "device", "name")
if HOST.exists(device_name_path):
device_name = HOST.read(device_name_path).strip()
base_name = normalize_sensor_name(device_name)
# Helper to create sensor component with human-readable description
@@ -426,8 +411,8 @@ def hwmon_sensor_components():
wifi_info = get_wifi_phy_info()
for component in components:
name = component.get("name", "")
# Match phy0, phy1, etc. sensors
if name.startswith("phy") and name in wifi_info:
# Match radio0, radio1, etc. sensors
if name.startswith("radio") and name in wifi_info:
phy = wifi_info[name]
# Add WiFi-specific description
component["description"] = phy["description"]
@@ -496,6 +481,179 @@ def thermal_sensor_components():
return components
def get_survey_data(ifname):
"""Get channel survey data using iw.py script"""
channels = []
try:
survey_data = HOST.run_json(("/usr/libexec/infix/iw.py", "survey", ifname), default=[])
for entry in survey_data:
channel = {
"frequency": entry.get("frequency"),
"in-use": entry.get("in_use", False)
}
# Add optional fields if present
if "noise" in entry:
channel["noise"] = entry["noise"]
if "active_time" in entry:
channel["active-time"] = entry["active_time"]
if "busy_time" in entry:
channel["busy-time"] = entry["busy_time"]
if "receive_time" in entry:
channel["receive-time"] = entry["receive_time"]
if "transmit_time" in entry:
channel["transmit-time"] = entry["transmit_time"]
channels.append(channel)
except Exception:
pass
return channels
def get_phy_info(phy_name):
"""Get complete PHY information using iw.py script"""
try:
return HOST.run_json(("/usr/libexec/infix/iw.py", "info", phy_name), default={})
except Exception:
return {}
def convert_iw_phy_info_for_yanger(phy_info):
"""
Convert iw.py phy_info format to yanger format.
Input: iw.py format with 'bands', 'driver', 'manufacturer', 'interface_combinations'
Output: yanger format with renamed/restructured fields
"""
result = {"bands": [], "driver": None, "manufacturer": "Unknown", "max-interfaces": {}}
# Convert bands - iw.py already uses snake_case for capabilities
for band in phy_info.get("bands", []):
band_data = {
"band": str(band.get("band", 0)),
"name": band.get("name", "Unknown")
}
# Add capability flags (iw.py uses snake_case: ht_capable, vht_capable, he_capable)
if band.get("ht_capable"):
band_data["ht-capable"] = True
if band.get("vht_capable"):
band_data["vht-capable"] = True
if band.get("he_capable"):
band_data["he-capable"] = True
result["bands"].append(band_data)
# Copy driver and manufacturer
if phy_info.get("driver"):
result["driver"] = phy_info["driver"]
if phy_info.get("manufacturer"):
result["manufacturer"] = phy_info["manufacturer"]
# Convert interface combinations to max-interfaces
# Find max AP interfaces from combinations
for comb in phy_info.get("interface_combinations", []):
for limit in comb.get("limits", []):
if "AP" in limit.get("types", []):
ap_max = limit.get("max", 0)
if "ap" not in result["max-interfaces"] or ap_max > result["max-interfaces"]["ap"]:
result["max-interfaces"]["ap"] = ap_max
return result
def wifi_radio_components():
"""
Create WiFi radio components with complete operational data.
Returns a list of hardware components for WiFi radios.
"""
components = []
wifi_info = get_wifi_phy_info()
for phy_name, phy_data in wifi_info.items():
component = {
"name": phy_name,
"class": "infix-hardware:wifi",
"description": phy_data.get("description", "WiFi Radio")
}
# Initialize wifi-radio data structure
wifi_radio_data = {}
# Get complete PHY information from iw.py script
iw_info = get_phy_info(phy_name)
# Convert iw.py format to yanger format
phy_details = convert_iw_phy_info_for_yanger(iw_info)
# Add manufacturer to component
if phy_details.get("manufacturer") and phy_details["manufacturer"] != "Unknown":
component["mfg-name"] = phy_details["manufacturer"]
# Add bands
if phy_details.get("bands"):
wifi_radio_data["bands"] = phy_details["bands"]
# Add driver
if phy_details.get("driver"):
wifi_radio_data["driver"] = phy_details["driver"]
# Add max-interfaces
if phy_details.get("max-interfaces"):
wifi_radio_data["max-interfaces"] = phy_details["max-interfaces"]
# Add max TX power from iw info
if iw_info.get("max_txpower"):
wifi_radio_data["max-txpower"] = iw_info["max_txpower"]
# Add supported channels from band frequencies
supported_channels = []
for band in iw_info.get("bands", []):
for freq in band.get("frequencies", []):
# Convert frequency to channel number
if 2412 <= freq <= 2484:
channel = (freq - 2407) // 5
elif 5170 <= freq <= 5825:
channel = (freq - 5000) // 5
elif 5955 <= freq <= 7115:
channel = (freq - 5950) // 5
else:
continue
supported_channels.append(channel)
if supported_channels:
wifi_radio_data['supported-channels'] = sorted(set(supported_channels))
# Count virtual interfaces from iw info
num_ifaces = iw_info.get('num_virtual_interfaces', 0)
wifi_radio_data['num-virtual-interfaces'] = num_ifaces
# Get survey data if we have an interface
iface = phy_data.get("iface")
if iface:
try:
channels = get_survey_data(iface)
if channels:
wifi_radio_data["survey"] = {
"channel": channels
}
except Exception:
# If survey fails, continue without survey data
pass
# Add wifi-radio data to component
if wifi_radio_data:
component["infix-hardware:wifi-radio"] = wifi_radio_data
components.append(component)
return components
def operational():
systemjson = HOST.read_json("/run/system.json")
@@ -507,6 +665,7 @@ def operational():
usb_port_components(systemjson) +
hwmon_sensor_components() +
thermal_sensor_components() +
wifi_radio_components() +
[],
},
}
+204 -15
View File
@@ -2,38 +2,213 @@ from ..host import HOST
import json
import re
def wifi(ifname):
wifi_data={}
def detect_wifi_mode(ifname):
"""Detect if interface is in AP or Station mode"""
try:
output = HOST.run(tuple(f"iw dev {ifname} info".split()), default="")
for line in output.splitlines():
if 'type' in line.lower():
if 'ap' in line.lower():
return 'ap'
else:
return 'station'
except Exception:
pass
# Default to station mode
return 'station'
def find_primary_interface_from_config(ifname):
"""Find primary interface by reading hostapd config files"""
try:
file_list = HOST.run(tuple("ls /etc/hostapd-*.conf".split()), default="")
if not file_list:
return None
for config_file in file_list.splitlines():
config_file = config_file.strip()
if not config_file:
continue
try:
content = HOST.run(tuple(f"cat {config_file}".split()), default="")
if not content:
continue
if f"interface={ifname}" in content or f"bss={ifname}" in content:
for line in content.splitlines():
if line.startswith("interface="):
return line.split("=", 1)[1].strip()
except Exception:
continue
except Exception:
pass
return None
def wifi_ap(ifname):
"""Get operational data for AP mode using hostapd_cli"""
ap_data = {}
try:
data=HOST.run(tuple(f"wpa_cli -i {ifname} status".split()), default="")
primary_if = find_primary_interface_from_config(ifname)
if not primary_if:
return {}
data = HOST.run(tuple(f"hostapd_cli -i {primary_if} status".split()), default="")
if not data:
return {}
# Find our interface's SSID, different for bss and primary, because it is
if ifname == primary_if:
# Primary interface - get ssid[0] or ssid
for line in data.splitlines():
if "=" in line:
try:
k, v = line.split("=", 1)
if k in ("ssid[0]", "ssid"):
ap_data["ssid"] = v
break
except ValueError:
continue
else:
# Secondary BSS - find in BSS array
bss_idx = None
for line in data.splitlines():
if "=" in line:
try:
k, v = line.split("=", 1)
if v == ifname and k.startswith("bss["):
bss_idx = k[4:-1] # Extract index from bss[N]
break
except ValueError:
continue
if bss_idx:
for line in data.splitlines():
if "=" in line:
try:
k, v = line.split("=", 1)
if k == f"ssid[{bss_idx}]":
ap_data["ssid"] = v
break
except ValueError:
continue
stations_data = HOST.run(tuple(f"iw dev {ifname} station dump".split()), default="")
stations = parse_iw_stations(stations_data)
if stations:
ap_data["stations"] = {
"station": stations
}
except Exception:
pass
# Nest data inside access-point container to match YANG schema
return {
"access-point": ap_data
} if ap_data else {}
def parse_iw_stations(output):
"""Parse iw station dump output to get connected stations"""
stations = []
current_station = None
for line in output.splitlines():
line = line.strip()
# Station line: "Station aa:bb:cc:dd:ee:ff (on wifiX)"
if line.startswith("Station "):
if current_station:
stations.append(current_station)
# Extract MAC address
parts = line.split()
if len(parts) >= 2:
current_station = {
"mac-address": parts[1].lower()
}
elif current_station:
# Parse station attributes
try:
# Lines are in format "key: value" with tabs
if ":" not in line:
continue
parts = line.split(":", 1)
key = parts[0].strip()
value = parts[1].strip()
if key == "signal":
# Format: "-42 dBm" or "-42 [-44] dBm"
rssi = int(value.split()[0])
current_station["rssi"] = rssi
elif key == "connected time":
# Format: "123 seconds"
seconds = int(value.split()[0])
current_station["connected-time"] = seconds
elif key == "rx packets":
current_station["rx-packets"] = int(value)
elif key == "tx packets":
current_station["tx-packets"] = int(value)
elif key == "rx bytes":
current_station["rx-bytes"] = int(value)
elif key == "tx bytes":
current_station["tx-bytes"] = int(value)
elif key == "tx bitrate":
# Format: "866.7 MBit/s ..." - extract speed and convert to 100kbit/s units
speed_mbps = float(value.split()[0])
current_station["tx-speed"] = int(speed_mbps * 10)
elif key == "rx bitrate":
# Format: "780.0 MBit/s ..." - extract speed and convert to 100kbit/s units
speed_mbps = float(value.split()[0])
current_station["rx-speed"] = int(speed_mbps * 10)
except (ValueError, KeyError, IndexError):
# Skip invalid values
continue
# Add last station
if current_station:
stations.append(current_station)
return stations
def wifi_station(ifname):
"""Get operational data for Station mode using wpa_cli"""
station_data = {}
try:
data = HOST.run(tuple(f"wpa_cli -i {ifname} status".split()), default="")
if data != "":
for line in data.splitlines():
try:
if "=" not in line:
continue
k,v = line.split("=", 1)
k, v = line.split("=", 1)
if k == "ssid":
wifi_data["ssid"] = v
if k == "wpa_state" and v == "DISCONNECTED": # wpa_suppicant has most likely restarted, restart scanning
HOST.run(tuple(f"wpa_cli -i {ifname} scan".split()), default="")
station_data["ssid"] = v
except ValueError:
# Skip malformed lines
continue
try:
data=HOST.run(tuple(f"wpa_cli -i {ifname} signal_poll".split()), default="FAIL")
data = HOST.run(tuple(f"wpa_cli -i {ifname} signal_poll".split()), default="FAIL")
# signal_poll return FAIL not connected
# signal_poll return FAIL if not connected
if data.strip() != "FAIL":
for line in data.splitlines():
try:
if "=" not in line:
continue
k,v = line.strip().split("=", 1)
k, v = line.strip().split("=", 1)
if k == "RSSI":
wifi_data["rssi"]=int(v)
station_data["rssi"] = int(v)
except (ValueError, KeyError):
# Skip malformed lines or invalid integers
continue
@@ -45,14 +220,28 @@ def wifi(ifname):
pass
try:
data=HOST.run(tuple(f"wpa_cli -i {ifname} scan_result".split()), default="FAIL")
data = HOST.run(tuple(f"wpa_cli -i {ifname} scan_result".split()), default="FAIL")
if data != "FAIL":
wifi_data["scan-results"] = parse_wpa_scan_result(data)
scan_results = parse_wpa_scan_result(data)
if scan_results:
station_data["scan-results"] = scan_results
except Exception:
# If scan results fail, just omit them
pass
return wifi_data
# Always nest data inside station container to match YANG schema
# In scan-only mode, this will be just scan-results with no ssid/rssi
return {"station": station_data} if station_data else {}
def wifi(ifname):
"""Main entry point - detect mode and return appropriate data"""
mode = detect_wifi_mode(ifname)
if mode == 'ap':
return wifi_ap(ifname)
else:
return wifi_station(ifname)
def parse_wpa_scan_result(scan_output):
@@ -106,7 +295,7 @@ def parse_wpa_scan_result(scan_output):
# Convert to list and sort by RSSI (best first)
result = list(networks.values())
result.sort(key=lambda x: x['rssi'], reverse=False)
result.sort(key=lambda x: x['rssi'], reverse=True)
return result