diff --git a/src/bin/show/__init__.py b/src/bin/show/__init__.py index 14785d9c..0b3471bd 100755 --- a/src/bin/show/__init__.py +++ b/src/bin/show/__init__.py @@ -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-*" zones. +# Note the hyphen after "cp" 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-*" (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: diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index a686e8db..b4c5b34e 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -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() diff --git a/src/statd/python/yanger/ietf_hardware.py b/src/statd/python/yanger/ietf_hardware.py index 62e32cd7..934b2f6d 100644 --- a/src/statd/python/yanger/ietf_hardware.py +++ b/src/statd/python/yanger/ietf_hardware.py @@ -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 = {