mirror of
https://github.com/kernelkit/infix.git
synced 2026-08-01 05:13:01 +02:00
Also remove caching of GPS data in operational, gpsd now depend on its GPS device <dev/gps0> instead of hotplug magic.
845 lines
33 KiB
Python
845 lines
33 KiB
Python
import datetime
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
from .common import insert, YangDate
|
|
from .host import HOST
|
|
|
|
|
|
def vpd_vendor_extensions(data):
|
|
vendor_extensions = []
|
|
for ext in data:
|
|
vendor_extension = {}
|
|
vendor_extension["iana-enterprise-number"] = ext[0]
|
|
vendor_extension["extension-data"] = ext[1]
|
|
vendor_extensions.append(vendor_extension)
|
|
return vendor_extensions
|
|
|
|
|
|
def vpd_component(vpd):
|
|
component = {}
|
|
component["name"] = vpd.get("board")
|
|
component["infix-hardware:vpd-data"] = {}
|
|
|
|
if vpd.get("data"):
|
|
component["class"] = "infix-hardware:vpd"
|
|
if vpd["data"].get("manufacture-date"):
|
|
mfgdate = datetime.datetime.strptime(vpd["data"]["manufacture-date"],
|
|
"%m/%d/%Y %H:%M:%S")
|
|
component["mfg-date"] = mfgdate.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
if vpd["data"].get("manufacter"):
|
|
component["mfg-name"] = vpd["data"]["manufacturer"]
|
|
if vpd["data"].get("product-name"):
|
|
component["model-name"] = vpd["data"]["product-name"]
|
|
if vpd["data"].get("serial-number"):
|
|
component["serial-num"] = vpd["data"]["serial-number"]
|
|
|
|
# Set VPD-data entrys
|
|
for k, v in vpd["data"].items():
|
|
if vpd["data"].get(k):
|
|
if k != "vendor-extension":
|
|
component["infix-hardware:vpd-data"][k] = v
|
|
else:
|
|
vendor_extensions=vpd_vendor_extensions(v)
|
|
component["infix-hardware:vpd-data"]["infix-hardware:vendor-extension"] = vendor_extensions
|
|
return component
|
|
|
|
|
|
def vpd_components(systemjson):
|
|
return [vpd_component(vpd) for vpd in systemjson.get("vpd", {}).values()]
|
|
|
|
|
|
def usb_port_components(systemjson):
|
|
usb_ports = systemjson.get("usb-ports", [])
|
|
|
|
ports = []
|
|
for usb_port in usb_ports:
|
|
port = {}
|
|
if usb_port.get("path"):
|
|
# Path now points to the USB device directory, not the attribute
|
|
base_path = usb_port["path"]
|
|
authorized_default_path = os.path.join(base_path, "authorized_default")
|
|
|
|
if HOST.exists(authorized_default_path):
|
|
data = int(HOST.read(authorized_default_path))
|
|
enabled = "unlocked" if data == 1 else "locked"
|
|
port["state"] = {}
|
|
port["state"]["admin-state"] = enabled
|
|
port["name"] = usb_port["name"]
|
|
port["class"] = "infix-hardware:usb"
|
|
port["state"]["oper-state"] = "enabled"
|
|
ports.append(port)
|
|
|
|
return ports
|
|
|
|
|
|
def motherboard_component(systemjson):
|
|
"""
|
|
Create a mainboard/chassis component from system.json data.
|
|
This provides a standard ietf-hardware representation of the main board.
|
|
"""
|
|
component = {
|
|
"name": "mainboard",
|
|
"class": "iana-hardware:chassis",
|
|
}
|
|
|
|
# Add manufacturer if available (from VPD or defaults)
|
|
if systemjson.get("vendor"):
|
|
component["mfg-name"] = systemjson["vendor"]
|
|
|
|
# Add model name (from device tree or VPD)
|
|
if systemjson.get("product-name"):
|
|
component["model-name"] = systemjson["product-name"]
|
|
|
|
# Add serial number if available (from VPD)
|
|
if systemjson.get("serial-number"):
|
|
component["serial-num"] = systemjson["serial-number"]
|
|
|
|
# Add part number as hardware revision if available
|
|
if systemjson.get("part-number"):
|
|
component["hardware-rev"] = systemjson["part-number"]
|
|
|
|
# Add chassis physical address (MAC) if available (from VPD or interface fallback)
|
|
if systemjson.get("mac-address"):
|
|
component["infix-hardware:phys-address"] = systemjson["mac-address"]
|
|
|
|
# Set state - admin-state is "unknown" since chassis cannot be
|
|
# administratively controlled (locked/unlocked)
|
|
component["state"] = {
|
|
"admin-state": "unknown",
|
|
"oper-state": "enabled"
|
|
}
|
|
|
|
return [component]
|
|
|
|
|
|
def normalize_sensor_name(name):
|
|
"""
|
|
Normalize sensor names for cleaner display.
|
|
|
|
Examples:
|
|
sfp_2 -> sfp2
|
|
mt7915_phy0 -> phy0
|
|
marvell_alaska_tomte_phy7 -> phy7
|
|
cpu_thermal -> cpu
|
|
pwmfan -> pwmfan
|
|
|
|
Strategy:
|
|
1. Strip common suffixes like -thermal/_thermal
|
|
2. Extract well-known sensor type names (phy, sfp, fan, etc.) from
|
|
the end of the name, stripping any vendor/chipset prefix
|
|
3. Remove underscores before trailing numbers (sfp_2 -> sfp2)
|
|
"""
|
|
import re
|
|
|
|
# Strip common suffixes
|
|
name = name.replace("-thermal", "").replace("_thermal", "")
|
|
|
|
# Extract well-known sensor types from end of name, stripping any prefix
|
|
# This handles: mt7915_phy0 -> phy0, marvell_alaska_phy7 -> phy7, etc.
|
|
sensor_types = r'(phy|sfp|fan|temp|sensor|psu|cpu|gpu|memory|disk)'
|
|
match = re.search(rf'.*_({sensor_types}\d*)$', name)
|
|
if match:
|
|
name = match.group(1)
|
|
|
|
# Remove underscores before trailing numbers (sfp_2 -> sfp2)
|
|
name = re.sub(r'_(\d+)$', r'\1', name)
|
|
|
|
return name
|
|
|
|
|
|
def get_wifi_phy_info():
|
|
"""
|
|
Discover WiFi PHYs using iw list command.
|
|
Returns dict: {phy_name: {band: str, iface: str, description: str}}
|
|
|
|
Example: {"radio0": {"band": "2.4 GHz", "iface": "wlan0", "description": "WiFi Radio (2.4 GHz)"}}
|
|
"""
|
|
phy_info = {}
|
|
|
|
try:
|
|
# Use iw.py to list all PHYs
|
|
phys = HOST.run_json(("/usr/libexec/infix/iw.py", "list"), default=[])
|
|
if not phys:
|
|
return phy_info
|
|
|
|
# Initialize PHY info for each PHY
|
|
for phy in phys:
|
|
phy_info[phy] = {"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
|
|
|
|
# Find associated virtual interfaces using iw.py dev
|
|
dev_map = HOST.run_json(("/usr/libexec/infix/iw.py", "dev"), default={})
|
|
|
|
# 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 descriptions
|
|
for phy, info in phy_info.items():
|
|
if info["iface"] and info["band"] != "Unknown":
|
|
info["description"] = f"WiFi Radio {phy}"
|
|
elif info["band"] != "Unknown":
|
|
info["description"] = f"WiFi Radio ({info['band']})"
|
|
elif info["iface"]:
|
|
info["description"] = f"WiFi Radio {phy}"
|
|
else:
|
|
info["description"] = "WiFi Radio"
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
return phy_info
|
|
|
|
|
|
def hwmon_sensor_components():
|
|
"""
|
|
Discover hwmon sensors and create sensor components with parent/child relationships.
|
|
Returns a list of hardware components with sensor-data for temperature,
|
|
fan, voltage, current, and power sensors.
|
|
|
|
For devices with multiple sensors (like SFP modules), creates:
|
|
- A parent component representing the device (class: module/container)
|
|
- Child sensor components that reference the parent via "parent" field
|
|
|
|
For simple devices with only one sensor, creates standalone sensor components.
|
|
"""
|
|
components = []
|
|
device_sensors = {} # Track {device_base_name: [list of sensor components]}
|
|
|
|
def add_sensor(base_name, sensor_component):
|
|
"""Helper to track sensors per device"""
|
|
if base_name not in device_sensors:
|
|
device_sensors[base_name] = []
|
|
device_sensors[base_name].append(sensor_component)
|
|
|
|
try:
|
|
hwmon_entries = HOST.run(("ls", "/sys/class/hwmon"), default="").split()
|
|
hwmon_devices = [os.path.join("/sys/class/hwmon", entry) for entry in hwmon_entries if entry.startswith("hwmon")]
|
|
|
|
for hwmon_path in hwmon_devices:
|
|
try:
|
|
name_path = os.path.join(hwmon_path, "name")
|
|
if not HOST.exists(name_path):
|
|
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
|
|
def create_sensor(sensor_name, value, value_type, value_scale, label=None):
|
|
component = {
|
|
"name": sensor_name,
|
|
"class": "iana-hardware:sensor",
|
|
"sensor-data": {
|
|
"value": value,
|
|
"value-type": value_type,
|
|
"value-scale": value_scale,
|
|
"value-precision": 0,
|
|
"value-timestamp": str(YangDate()),
|
|
"oper-status": "ok"
|
|
}
|
|
}
|
|
# Add human-readable description if we have a label
|
|
if label:
|
|
# Format label nicely: "RX_power" -> "RX Power", "VCC" -> "VCC"
|
|
desc = label.replace('_', ' ').title()
|
|
component["description"] = desc
|
|
return component
|
|
|
|
# Temperature sensors
|
|
temp_entries = HOST.run(("ls", hwmon_path), default="").split()
|
|
temp_files = [os.path.join(hwmon_path, e) for e in temp_entries if e.startswith("temp") and e.endswith("_input")]
|
|
for temp_file in temp_files:
|
|
try:
|
|
sensor_num = os.path.basename(temp_file).split('_')[0].replace('temp', '')
|
|
value = int(HOST.read(temp_file).strip())
|
|
label_file = os.path.join(hwmon_path, f"temp{sensor_num}_label")
|
|
raw_label = None
|
|
if HOST.exists(label_file):
|
|
raw_label = HOST.read(label_file).strip()
|
|
label = normalize_sensor_name(raw_label)
|
|
sensor_name = f"{base_name}-{label}"
|
|
else:
|
|
sensor_name = base_name if sensor_num == '1' else f"{base_name}{sensor_num}"
|
|
add_sensor(base_name, create_sensor(sensor_name, value, "celsius", "milli", raw_label))
|
|
except (FileNotFoundError, ValueError, IOError):
|
|
continue
|
|
|
|
# Fan sensors (RPM from tachometer)
|
|
fan_entries = HOST.run(("ls", hwmon_path), default="").split()
|
|
fan_files = [os.path.join(hwmon_path, e) for e in fan_entries if e.startswith("fan") and e.endswith("_input")]
|
|
for fan_file in fan_files:
|
|
try:
|
|
sensor_num = os.path.basename(fan_file).split('_')[0].replace('fan', '')
|
|
value = int(HOST.read(fan_file).strip())
|
|
label_file = os.path.join(hwmon_path, f"fan{sensor_num}_label")
|
|
raw_label = None
|
|
if HOST.exists(label_file):
|
|
raw_label = HOST.read(label_file).strip()
|
|
label = normalize_sensor_name(raw_label)
|
|
sensor_name = f"{base_name}-{label}"
|
|
else:
|
|
sensor_name = base_name if sensor_num == '1' else f"{base_name}{sensor_num}"
|
|
add_sensor(base_name, create_sensor(sensor_name, value, "rpm", "units", raw_label))
|
|
except (FileNotFoundError, ValueError, IOError):
|
|
continue
|
|
|
|
# PWM fan sensors (duty cycle percentage)
|
|
# Only add if no fan*_input exists for this device (avoid duplicates)
|
|
has_rpm_sensor = bool(fan_files)
|
|
if not has_rpm_sensor:
|
|
pwm_entries = HOST.run(("ls", hwmon_path), default="").split()
|
|
pwm_files = [os.path.join(hwmon_path, e) for e in pwm_entries if e.startswith("pwm") and e[3:].replace('_', '').isdigit() if len(e) > 3]
|
|
for pwm_file in pwm_files:
|
|
# Skip pwm*_enable, pwm*_mode, etc. - only process pwm1, pwm2, etc.
|
|
pwm_basename = os.path.basename(pwm_file)
|
|
if not pwm_basename.replace('pwm', '').isdigit():
|
|
continue
|
|
try:
|
|
sensor_num = pwm_basename.replace('pwm', '')
|
|
pwm_raw = int(HOST.read(pwm_file).strip())
|
|
# Convert PWM duty cycle (0-255) to percentage (0-100)
|
|
# Note: Some devices are inverted (255=off, 0=max), but we report as-is
|
|
# The value represents duty cycle, not necessarily fan speed
|
|
# Use "other" value-type since PWM duty cycle isn't a standard IETF type
|
|
value = int((pwm_raw / 255.0) * 100 * 1000) # Convert to milli-percent (0-100000)
|
|
label_file = os.path.join(hwmon_path, f"pwm{sensor_num}_label")
|
|
raw_label = None
|
|
if HOST.exists(label_file):
|
|
raw_label = HOST.read(label_file).strip()
|
|
label = normalize_sensor_name(raw_label)
|
|
sensor_name = f"{base_name}-{label}"
|
|
else:
|
|
sensor_name = base_name if sensor_num == '1' else f"{base_name}{sensor_num}"
|
|
# Use "PWM Fan" as description so it displays nicely in show hardware
|
|
add_sensor(base_name, create_sensor(sensor_name, value, "other", "milli", raw_label or "PWM Fan"))
|
|
except (FileNotFoundError, ValueError, IOError):
|
|
continue
|
|
|
|
# Voltage sensors
|
|
voltage_entries = HOST.run(("ls", hwmon_path), default="").split()
|
|
voltage_files = [os.path.join(hwmon_path, e) for e in voltage_entries if e.startswith("in") and e.endswith("_input")]
|
|
for voltage_file in voltage_files:
|
|
try:
|
|
sensor_num = os.path.basename(voltage_file).split('_')[0].replace('in', '')
|
|
value = int(HOST.read(voltage_file).strip())
|
|
label_file = os.path.join(hwmon_path, f"in{sensor_num}_label")
|
|
raw_label = None
|
|
if HOST.exists(label_file):
|
|
raw_label = HOST.read(label_file).strip()
|
|
label = normalize_sensor_name(raw_label)
|
|
sensor_name = f"{base_name}-{label}"
|
|
else:
|
|
raw_label = "voltage"
|
|
sensor_name = f"{base_name}-voltage" if sensor_num == '0' else f"{base_name}-voltage{sensor_num}"
|
|
add_sensor(base_name, create_sensor(sensor_name, value, "volts-DC", "milli", raw_label))
|
|
except (FileNotFoundError, ValueError, IOError):
|
|
continue
|
|
|
|
# Current sensors
|
|
current_entries = HOST.run(("ls", hwmon_path), default="").split()
|
|
current_files = [os.path.join(hwmon_path, e) for e in current_entries if e.startswith("curr") and e.endswith("_input")]
|
|
for current_file in current_files:
|
|
try:
|
|
sensor_num = os.path.basename(current_file).split('_')[0].replace('curr', '')
|
|
value = int(HOST.read(current_file).strip())
|
|
label_file = os.path.join(hwmon_path, f"curr{sensor_num}_label")
|
|
raw_label = None
|
|
if HOST.exists(label_file):
|
|
raw_label = HOST.read(label_file).strip()
|
|
label = normalize_sensor_name(raw_label)
|
|
sensor_name = f"{base_name}-{label}"
|
|
else:
|
|
raw_label = "current"
|
|
sensor_name = f"{base_name}-current" if sensor_num == '1' else f"{base_name}-current{sensor_num}"
|
|
add_sensor(base_name, create_sensor(sensor_name, value, "amperes", "milli", raw_label))
|
|
except (FileNotFoundError, ValueError, IOError):
|
|
continue
|
|
|
|
# Power sensors
|
|
power_entries = HOST.run(("ls", hwmon_path), default="").split()
|
|
power_files = [os.path.join(hwmon_path, e) for e in power_entries if e.startswith("power") and e.endswith("_input")]
|
|
for power_file in power_files:
|
|
try:
|
|
sensor_num = os.path.basename(power_file).split('_')[0].replace('power', '')
|
|
value = int(HOST.read(power_file).strip())
|
|
label_file = os.path.join(hwmon_path, f"power{sensor_num}_label")
|
|
raw_label = None
|
|
if HOST.exists(label_file):
|
|
raw_label = HOST.read(label_file).strip()
|
|
label = normalize_sensor_name(raw_label)
|
|
sensor_name = f"{base_name}-{label}"
|
|
else:
|
|
raw_label = "power"
|
|
sensor_name = f"{base_name}-power" if sensor_num == '1' else f"{base_name}-power{sensor_num}"
|
|
add_sensor(base_name, create_sensor(sensor_name, value, "watts", "micro", raw_label))
|
|
except (FileNotFoundError, ValueError, IOError):
|
|
continue
|
|
|
|
except (FileNotFoundError, ValueError, IOError):
|
|
continue
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
# Now create parent/child relationships
|
|
for base_name, sensors in device_sensors.items():
|
|
if len(sensors) > 1:
|
|
# Multiple sensors: create parent component
|
|
parent = {
|
|
"name": base_name,
|
|
"class": "iana-hardware:module", # Use "module" for multi-sensor devices like SFP
|
|
}
|
|
components.append(parent)
|
|
|
|
# Add parent reference to all child sensors
|
|
for sensor in sensors:
|
|
sensor["parent"] = base_name
|
|
components.append(sensor)
|
|
else:
|
|
# Single sensor: add without parent
|
|
components.extend(sensors)
|
|
|
|
# Enrich WiFi PHY sensors with descriptive information
|
|
wifi_info = get_wifi_phy_info()
|
|
for component in components:
|
|
name = component.get("name", "")
|
|
# 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"]
|
|
# Optionally change class to wifi for WiFi PHY sensors
|
|
if component.get("class") == "iana-hardware:sensor":
|
|
# Keep as sensor but we could create a parent WiFi component later if needed
|
|
pass
|
|
|
|
return components
|
|
|
|
|
|
def thermal_sensor_components():
|
|
"""
|
|
Discover thermal zones and create sensor components.
|
|
Returns a list of hardware components with sensor-data.
|
|
"""
|
|
components = []
|
|
|
|
try:
|
|
# Find all thermal zones
|
|
thermal_entries = HOST.run(("ls", "/sys/class/thermal"), default="").split()
|
|
thermal_zones = [os.path.join("/sys/class/thermal", entry) for entry in thermal_entries if entry.startswith("thermal_zone")]
|
|
|
|
for zone_path in thermal_zones:
|
|
try:
|
|
# Read zone type (e.g., "cpu-thermal", "gpu-thermal")
|
|
type_path = os.path.join(zone_path, "type")
|
|
if not HOST.exists(type_path):
|
|
continue
|
|
|
|
zone_type = HOST.read(type_path).strip()
|
|
|
|
# Read temperature in millidegrees Celsius
|
|
temp_path = os.path.join(zone_path, "temp")
|
|
if not HOST.exists(temp_path):
|
|
continue
|
|
|
|
temp_millidegrees = int(HOST.read(temp_path).strip())
|
|
|
|
# Create component with sensor-data
|
|
# Component name: strip "-thermal" suffix for cleaner display
|
|
component_name = normalize_sensor_name(zone_type)
|
|
|
|
component = {
|
|
"name": component_name,
|
|
"class": "iana-hardware:sensor",
|
|
"sensor-data": {
|
|
"value": temp_millidegrees,
|
|
"value-type": "celsius",
|
|
"value-scale": "milli",
|
|
"value-precision": 0,
|
|
"value-timestamp": str(YangDate()),
|
|
"oper-status": "ok"
|
|
}
|
|
}
|
|
|
|
components.append(component)
|
|
|
|
except (FileNotFoundError, ValueError, IOError):
|
|
# Skip this thermal zone if we can't read it
|
|
continue
|
|
|
|
except Exception:
|
|
# If we can't access /sys/class/thermal at all, just return empty list
|
|
pass
|
|
|
|
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 _gpsd_poll():
|
|
"""Query gpsd for current state via ?POLL command.
|
|
|
|
Returns a dict keyed by device path with tpv/sky data merged,
|
|
or empty dict on failure.
|
|
"""
|
|
import json
|
|
import socket
|
|
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.settimeout(0.5)
|
|
sock.connect(("127.0.0.1", 2947))
|
|
|
|
# Read VERSION banner
|
|
sock.recv(4096)
|
|
|
|
# Send POLL request
|
|
sock.sendall(b'?WATCH={"enable":true,"json":true};\n?POLL;\n')
|
|
|
|
# Read until we get the POLL response
|
|
buf = b""
|
|
for _ in range(5):
|
|
chunk = sock.recv(4096)
|
|
if not chunk:
|
|
break
|
|
buf += chunk
|
|
for line in buf.decode(errors="replace").splitlines():
|
|
try:
|
|
msg = json.loads(line)
|
|
except (json.JSONDecodeError, ValueError):
|
|
continue
|
|
if msg.get("class") == "POLL":
|
|
sock.close()
|
|
return msg
|
|
sock.close()
|
|
except (OSError, socket.error):
|
|
pass
|
|
|
|
return {}
|
|
|
|
|
|
def gps_receiver_components():
|
|
"""Discover GPS/GNSS receivers and populate operational state.
|
|
|
|
GPS devices are discovered via /dev/gps* symlinks (created by udev
|
|
rules). Status is queried live from gpsd via the ?POLL command.
|
|
"""
|
|
components = []
|
|
|
|
# Discover GPS devices via /dev/gps* symlinks (created by udev rules)
|
|
gps_devices = {}
|
|
for i in range(4):
|
|
dev_path = f"/dev/gps{i}"
|
|
if not HOST.exists(dev_path):
|
|
continue
|
|
actual = HOST.run(("readlink", "-f", dev_path), default="").strip()
|
|
gps_devices[actual] = {
|
|
"name": f"gps{i}",
|
|
"symlink": dev_path,
|
|
}
|
|
|
|
if not gps_devices:
|
|
return components
|
|
|
|
# Query gpsd directly for current state
|
|
poll = _gpsd_poll()
|
|
active = poll.get("active", 0)
|
|
|
|
# Index TPV and SKY responses by device path
|
|
tpv_by_dev = {}
|
|
for tpv in poll.get("tpv", []):
|
|
dev = tpv.get("device", "")
|
|
tpv_by_dev[dev] = tpv
|
|
|
|
sky_by_dev = {}
|
|
for sky in poll.get("sky", []):
|
|
dev = sky.get("device", "")
|
|
sky_by_dev[dev] = sky
|
|
|
|
for actual_path, dev in gps_devices.items():
|
|
name = dev["name"]
|
|
symlink = dev["symlink"]
|
|
component = {
|
|
"name": name,
|
|
"class": "infix-hardware:gps",
|
|
"description": "GPS/GNSS Receiver"
|
|
}
|
|
|
|
gps_data = {}
|
|
gps_data["device"] = symlink
|
|
|
|
# Match by resolved path or symlink (fallback to single report)
|
|
tpv = tpv_by_dev.get(actual_path, tpv_by_dev.get(symlink, {}))
|
|
sky = sky_by_dev.get(actual_path, sky_by_dev.get(symlink, {}))
|
|
if not tpv and len(tpv_by_dev) == 1:
|
|
tpv = next(iter(tpv_by_dev.values()))
|
|
if not sky and len(sky_by_dev) == 1:
|
|
sky = next(iter(sky_by_dev.values()))
|
|
|
|
gps_data["activated"] = active > 0 and bool(tpv)
|
|
|
|
if tpv.get("driver"):
|
|
gps_data["driver"] = tpv["driver"]
|
|
|
|
mode = tpv.get("mode", 0)
|
|
if mode == 2:
|
|
gps_data["fix-mode"] = "2d"
|
|
elif mode == 3:
|
|
gps_data["fix-mode"] = "3d"
|
|
else:
|
|
gps_data["fix-mode"] = "none"
|
|
|
|
if "lat" in tpv:
|
|
gps_data["latitude"] = f"{float(tpv['lat']):.6f}"
|
|
if "lon" in tpv:
|
|
gps_data["longitude"] = f"{float(tpv['lon']):.6f}"
|
|
if "altHAE" in tpv:
|
|
gps_data["altitude"] = f"{float(tpv['altHAE']):.1f}"
|
|
|
|
sat_vis = 0
|
|
sat_used = 0
|
|
|
|
sats = sky.get("satellites", [])
|
|
if isinstance(sats, list):
|
|
sat_vis = len(sats)
|
|
sat_used = sum(1 for s in sats if s.get("used"))
|
|
|
|
# Fallback for gpsd variants that report aggregated counters only
|
|
# (common in POLL output for some setups).
|
|
if not sat_vis:
|
|
sat_vis = int(sky.get("nSat", sky.get("satellites_visible", 0)) or 0)
|
|
if not sat_used:
|
|
sat_used = int(sky.get("uSat", sky.get("satellites_used", 0)) or 0)
|
|
|
|
# Last fallback to TPV keys when SKY is absent.
|
|
if not sat_vis:
|
|
sat_vis = int(tpv.get("nSat", tpv.get("satellites_visible", 0)) or 0)
|
|
if not sat_used:
|
|
sat_used = int(tpv.get("uSat", tpv.get("satellites_used", 0)) or 0)
|
|
|
|
if sat_used > sat_vis:
|
|
sat_vis = sat_used
|
|
|
|
gps_data["satellites-visible"] = sat_vis
|
|
gps_data["satellites-used"] = sat_used
|
|
|
|
# Check for PPS device availability
|
|
pps_path = f"/dev/pps{name.replace('gps', '')}"
|
|
gps_data["pps-available"] = HOST.exists(pps_path)
|
|
|
|
if gps_data:
|
|
component["infix-hardware:gps-receiver"] = gps_data
|
|
|
|
components.append(component)
|
|
|
|
return components
|
|
|
|
|
|
def operational():
|
|
systemjson = HOST.read_json("/run/system.json", {})
|
|
|
|
return {
|
|
"ietf-hardware:hardware": {
|
|
"component":
|
|
motherboard_component(systemjson) +
|
|
vpd_components(systemjson) +
|
|
usb_port_components(systemjson) +
|
|
hwmon_sensor_components() +
|
|
thermal_sensor_components() +
|
|
wifi_radio_components() +
|
|
gps_receiver_components() +
|
|
[],
|
|
},
|
|
}
|