mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
statd: fix temperature sensor display on Marvell switches
The per-port PHY temperature sensors on Marvell DSA switches show up in 'show hardware' with the name the kernel derives from the full device tree path, e.g. cp0busbusf2000000mdio12a200switch2mdio01, which honestly is completely unreadable and also overruns the value column. Name each sensor after the front-panel port it serves (e1, e2, ...) by matching the PHY's device-tree phandle against each interface's phy-handle. Also: - show system: report a representative SoC temperature on CN913x by matching the ap-* and cp<N>-* thermal zones, hottest wins. - cli-pretty: truncate over-long sensor names so they can never spill into the value column again. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -544,6 +544,13 @@ def mdns(args: List[str]) -> None:
|
||||
cli_pretty(data, "show-mdns")
|
||||
|
||||
|
||||
# Sensor names that represent the SoC/CPU temperature (not per-port PHYs).
|
||||
# Matches "cpu"/"soc"/"core", and Marvell CN913x "ap-*" / "cp<N>-*" zones.
|
||||
# Note the hyphen after "cp<N>" so mangled PHY names like "cp0busbus…" never
|
||||
# match.
|
||||
SOC_TEMP_RE = re.compile(r'^(cpu|soc|core|ap-|cp\d+-)')
|
||||
|
||||
|
||||
def system(args: List[str]) -> None:
|
||||
# Get system state from sysrepo
|
||||
data = get_json("/ietf-system:system-state")
|
||||
@@ -562,6 +569,7 @@ def system(args: List[str]) -> None:
|
||||
fan_rpm = None
|
||||
if hardware_data and "ietf-hardware:hardware" in hardware_data:
|
||||
components = hardware_data.get("ietf-hardware:hardware", {}).get("component", [])
|
||||
soc_temps = []
|
||||
for component in components:
|
||||
sensor_data = component.get("sensor-data", {})
|
||||
if not sensor_data:
|
||||
@@ -570,16 +578,21 @@ def system(args: List[str]) -> None:
|
||||
name = component.get("name", "")
|
||||
value_type = sensor_data.get("value-type")
|
||||
|
||||
# Only capture CPU/SoC temperature (ignore phy, sfp, etc.)
|
||||
# Different platforms use different names: cpu, soc, core, etc.
|
||||
if value_type == "celsius" and name in ("cpu", "soc", "core") and cpu_temp is None:
|
||||
temp_millidegrees = sensor_data.get("value", 0)
|
||||
cpu_temp = temp_millidegrees / 1000.0
|
||||
# Capture SoC/CPU temperature, ignoring per-port phy, sfp, etc.
|
||||
# Platforms name the zone differently: a plain "cpu"/"soc"/"core",
|
||||
# or, on Marvell CN913x, an "ap-*" (application processor) or
|
||||
# "cp<N>-*" (communication processor) cluster. Collect them all
|
||||
# and report the hottest as the representative SoC temperature.
|
||||
if value_type == "celsius" and SOC_TEMP_RE.match(name):
|
||||
soc_temps.append(sensor_data.get("value", 0) / 1000.0)
|
||||
|
||||
# Capture fan speed if available
|
||||
elif value_type == "rpm" and fan_rpm is None:
|
||||
fan_rpm = sensor_data.get("value", 0)
|
||||
|
||||
if soc_temps:
|
||||
cpu_temp = max(soc_temps)
|
||||
|
||||
if cpu_temp is not None:
|
||||
runtime["cpu_temp"] = cpu_temp
|
||||
if fan_rpm is not None:
|
||||
|
||||
@@ -916,7 +916,12 @@ class Sensor:
|
||||
# Standalone sensor without description: use name as-is
|
||||
display_name = self.name
|
||||
|
||||
row = f"{indent_str}{display_name:<{PadSensor.name - len(indent_str)}}"
|
||||
# Truncate over-long names so they never spill into the VALUE column
|
||||
# (e.g. unmapped switch-PHY hwmon names derived from the DT path).
|
||||
field = PadSensor.name - len(indent_str)
|
||||
if len(display_name) >= field:
|
||||
display_name = display_name[:field - 2] + "…"
|
||||
row = f"{indent_str}{display_name:<{field}}"
|
||||
# For colored value, pad manually to account for ANSI codes
|
||||
value_str = self.get_formatted_value()
|
||||
# Count visible characters (strip ANSI codes for length calculation)
|
||||
@@ -2245,6 +2250,13 @@ def show_services(json):
|
||||
service_table.print()
|
||||
|
||||
|
||||
def sensor_sort_key(component):
|
||||
"""Natural sort key for sensor names: digit runs compare numerically so
|
||||
e2 sorts before e10, while keeping ap-cpu/cp0-ic/sfp groups together."""
|
||||
name = component.get("name", "")
|
||||
return [int(t) if t.isdigit() else t for t in re.split(r'(\d+)', name)]
|
||||
|
||||
|
||||
def show_hardware(json):
|
||||
if not json.get("ietf-hardware:hardware"):
|
||||
print("Error, top level \"ietf-hardware:component\" missing")
|
||||
@@ -2418,15 +2430,16 @@ def show_hardware(json):
|
||||
print(f"\n{module_name}:")
|
||||
|
||||
if module_name in children:
|
||||
for child in sorted(children[module_name], key=lambda c: c.get("name", "")):
|
||||
for child in sorted(children[module_name], key=sensor_sort_key):
|
||||
sensor = Sensor(child)
|
||||
sensor.print(indent=1)
|
||||
|
||||
# Display standalone sensors (no parent)
|
||||
# Display standalone sensors (no parent), naturally sorted so port
|
||||
# temperatures read e1, e2, ... e28 rather than e1, e10, e11, ...
|
||||
if standalone:
|
||||
if modules:
|
||||
print() # Add blank line between modules and standalone
|
||||
for component in sorted(standalone, key=lambda c: c.get("name", "")):
|
||||
for component in sorted(standalone, key=sensor_sort_key):
|
||||
sensor = Sensor(component)
|
||||
sensor.print()
|
||||
|
||||
|
||||
@@ -149,6 +149,36 @@ def normalize_sensor_name(name):
|
||||
return name
|
||||
|
||||
|
||||
def _dt_phandle(path):
|
||||
"""Read a device-tree phandle cell as a normalized hex string.
|
||||
|
||||
phandle/phy-handle properties are 4-byte big-endian cells. Read them via
|
||||
od(1) so the binary content survives the text-based HOST transport (works
|
||||
both locally and over the ssh-style remote transport).
|
||||
"""
|
||||
out = HOST.run(("od", "-An", "-tx1", path), default="")
|
||||
return "".join(out.split()) if out else None
|
||||
|
||||
|
||||
def phy_handle_to_ifname():
|
||||
"""Map a PHY's device-tree phandle to the interface it drives.
|
||||
|
||||
DSA user ports carry a "phy-handle" pointing at the PHY that serves them.
|
||||
The reverse map lets us name a switch PHY's hwmon temperature sensor after
|
||||
the front-panel port (e.g. e1) instead of the unreadable name the kernel
|
||||
derives from the full device-tree path (cp0busbusf2000000mdio...).
|
||||
"""
|
||||
mapping = {}
|
||||
for ifname in HOST.run(("ls", "/sys/class/net"), default="").split():
|
||||
handle_path = os.path.join("/sys/class/net", ifname, "of_node", "phy-handle")
|
||||
if not HOST.exists(handle_path):
|
||||
continue
|
||||
handle = _dt_phandle(handle_path)
|
||||
if handle:
|
||||
mapping[handle] = ifname
|
||||
return mapping
|
||||
|
||||
|
||||
def get_wifi_phy_info():
|
||||
"""
|
||||
Discover WiFi PHYs using iw list command.
|
||||
@@ -218,6 +248,7 @@ def hwmon_sensor_components():
|
||||
"""
|
||||
components = []
|
||||
device_sensors = {} # Track {device_base_name: [list of sensor components]}
|
||||
phy_ifname = phy_handle_to_ifname()
|
||||
|
||||
def add_sensor(base_name, sensor_component):
|
||||
"""Helper to track sensors per device"""
|
||||
@@ -244,6 +275,16 @@ def hwmon_sensor_components():
|
||||
|
||||
base_name = normalize_sensor_name(device_name)
|
||||
|
||||
# Switch PHYs get an hwmon name derived from their full
|
||||
# device-tree path (e.g. cp0busbusf2000000mdio12a200switch2mdio01).
|
||||
# If this PHY drives a known port, name the sensor after that
|
||||
# port (e1, e2, ...) instead.
|
||||
phandle_path = os.path.join(hwmon_path, "device", "of_node", "phandle")
|
||||
if HOST.exists(phandle_path):
|
||||
ifname = phy_ifname.get(_dt_phandle(phandle_path))
|
||||
if ifname:
|
||||
base_name = ifname
|
||||
|
||||
# Helper to create sensor component with human-readable description
|
||||
def create_sensor(sensor_name, value, value_type, value_scale, label=None):
|
||||
component = {
|
||||
|
||||
Reference in New Issue
Block a user