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:
Joachim Wiberg
2026-06-20 19:26:21 +02:00
parent b3450d004d
commit 0229057612
3 changed files with 76 additions and 9 deletions
+18 -5
View File
@@ -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:
+17 -4
View File
@@ -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()
+41
View File
@@ -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 = {