From fdb02895fb7687b8153ee62a0ff81414eee1d138 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 30 Nov 2025 17:16:21 +0100 Subject: [PATCH] statd: add pwm fan support Due to their nature we cannot read out the RPM of the pwm fans on the BPi-R3. But we can at least show it's alive, or steering out a signal to the fan. Signed-off-by: Joachim Wiberg --- src/statd/python/cli_pretty/cli_pretty.py | 13 +++++++++ src/statd/python/yanger/ietf_hardware.py | 32 ++++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index 851e8eb4..0c6d3e69 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -652,7 +652,20 @@ class Sensor: else: return f"{self.value} W" + # Handle PWM fan sensors (reported as "other" type with milli scale) + # PWM duty cycle is reported as percentage (0-100) + elif self.value_type == 'other' and self.value_scale == 'milli': + # Check if this is likely a PWM sensor based on description or name + name_lower = self.name.lower() + desc_lower = (self.description or "").lower() + if 'pwm' in desc_lower or 'fan' in name_lower or 'fan' in desc_lower: + percent = self.value / 1000.0 + return f"{percent:.1f}%" + # Fall through for other "other" type sensors + # For unknown sensor types, show raw value + if self.value_type in ['other', 'unknown']: + return f"{self.value}" else: return f"{self.value} {self.value_type}" diff --git a/src/statd/python/yanger/ietf_hardware.py b/src/statd/python/yanger/ietf_hardware.py index 3535cbc6..876ab863 100644 --- a/src/statd/python/yanger/ietf_hardware.py +++ b/src/statd/python/yanger/ietf_hardware.py @@ -297,7 +297,7 @@ def hwmon_sensor_components(): except (FileNotFoundError, ValueError, IOError): continue - # Fan sensors + # Fan sensors (RPM from tachometer) for fan_file in glob.glob(os.path.join(hwmon_path, "fan*_input")): try: sensor_num = os.path.basename(fan_file).split('_')[0].replace('fan', '') @@ -314,6 +314,36 @@ def hwmon_sensor_components(): 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(glob.glob(os.path.join(hwmon_path, "fan*_input"))) + if not has_rpm_sensor: + for pwm_file in glob.glob(os.path.join(hwmon_path, "pwm[0-9]*")): + # 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 for voltage_file in glob.glob(os.path.join(hwmon_path, "in*_input")): try: