mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 02:48:38 -04:00
controller: report cooling fan RPM in CPU status
The RPi 5 fan connector exposes a tachometer via the pwmfan hwmon device. Report it as fan_rpm in cpu_stats and show it in both the Controller fallback UI and the React Status page. Hidden when no fan is present. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f635671266
commit
42a017294e
6 changed files with 54 additions and 2 deletions
|
|
@ -93,6 +93,14 @@ function CPUTemperatureStatistic({temperature, high_temperature, critical_temper
|
|||
return <Statistic label='Temp C°' value={temperature} {...props}/>
|
||||
}
|
||||
|
||||
function FanRPMStatistic({fan_rpm, ...props}) {
|
||||
// Only rendered when the device reports a fan (e.g. RPi 5 fan connector).
|
||||
if (fan_rpm === null || fan_rpm === undefined) {
|
||||
return null;
|
||||
}
|
||||
return <Statistic label='Fan RPM' value={fan_rpm} {...props}/>
|
||||
}
|
||||
|
||||
function IOWaitStatistic({percent_iowait, ...props}) {
|
||||
if (percent_iowait === null || percent_iowait === undefined) {
|
||||
return <Statistic label='IO Wait %' value='?'/>
|
||||
|
|
@ -236,6 +244,7 @@ export function StatusPage() {
|
|||
let temperature;
|
||||
let high_temperature;
|
||||
let critical_temperature;
|
||||
let fan_rpm;
|
||||
let processesStats = null;
|
||||
let minute_1;
|
||||
let minute_5;
|
||||
|
|
@ -255,6 +264,7 @@ export function StatusPage() {
|
|||
temperature = cpu_stats['temperature'];
|
||||
high_temperature = cpu_stats['high_temperature'];
|
||||
critical_temperature = cpu_stats['critical_temperature'];
|
||||
fan_rpm = cpu_stats['fan_rpm'];
|
||||
processesStats = processes_stats;
|
||||
|
||||
minute_1 = load_stats['minute_1'];
|
||||
|
|
@ -312,6 +322,7 @@ export function StatusPage() {
|
|||
high_temperature={high_temperature}
|
||||
critical_temperature={critical_temperature}
|
||||
/>
|
||||
<FanRPMStatistic fan_rpm={fan_rpm}/>
|
||||
<IOWaitStatistic percent_iowait={percent_iowait}/>
|
||||
<UptimeStatistic uptime_seconds={uptime_seconds}/>
|
||||
</Statistic.Group>
|
||||
|
|
@ -332,6 +343,7 @@ export function StatusPage() {
|
|||
critical_temperature={critical_temperature}
|
||||
style={{marginRight: 0}}
|
||||
/>
|
||||
<FanRPMStatistic fan_rpm={fan_rpm}/>
|
||||
<IOWaitStatistic percent_iowait={percent_iowait}/>
|
||||
<UptimeStatistic uptime_seconds={uptime_seconds}/>
|
||||
</Statistic.Group>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ class CpuStatusResponse(BaseModel):
|
|||
frequency_max_mhz: Optional[int] = Field(default=None, description="Maximum CPU frequency in MHz")
|
||||
temperature_c: Optional[float] = Field(default=None, description="CPU temperature in Celsius")
|
||||
cores: int = Field(description="Number of CPU cores")
|
||||
fan_rpm: Optional[int] = Field(default=None, description="Cooling fan speed in RPM, if a fan is present")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
|
|
|||
|
|
@ -92,6 +92,17 @@ def get_cpu_status() -> dict:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
# Get fan RPM (e.g. the RPi 5 fan connector's `pwmfan` hwmon device).
|
||||
# sensors_fans only exists on Linux psutil.
|
||||
fan_rpm = None
|
||||
try:
|
||||
fans = psutil.sensors_fans() if hasattr(psutil, "sensors_fans") else {}
|
||||
readings = [i.current for readings_ in fans.values() for i in readings_ if i.current]
|
||||
if readings:
|
||||
fan_rpm = int(max(readings))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"cores": cpu_count,
|
||||
"cur_frequency": cur_frequency - min_frequency if cur_frequency and min_frequency else None,
|
||||
|
|
@ -101,6 +112,7 @@ def get_cpu_status() -> dict:
|
|||
"temperature": int(temperature) if temperature else None,
|
||||
"high_temperature": int(high_temperature) if high_temperature else None,
|
||||
"critical_temperature": int(critical_temperature) if critical_temperature else None,
|
||||
"fan_rpm": fan_rpm,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -258,6 +258,7 @@ async def dashboard(request: Request):
|
|||
"cpu": {
|
||||
"percent": cpu["percent"],
|
||||
"temperature_c": cpu["temperature"], # Field renamed
|
||||
"fan_rpm": cpu.get("fan_rpm"),
|
||||
},
|
||||
"memory": {
|
||||
"percent": round((memory["used"] / memory["total"]) * 100, 1) if memory["total"] > 0 else 0,
|
||||
|
|
|
|||
|
|
@ -938,7 +938,7 @@
|
|||
<div class="status-card">
|
||||
<h3>CPU</h3>
|
||||
<p class="value">{{ cpu.percent if cpu.percent is not none else '--' }}%</p>
|
||||
<p class="sub">{% if cpu.temperature_c %}{{ cpu.temperature_c }}°C{% else %}--{% endif %}</p>
|
||||
<p class="sub">{% if cpu.temperature_c %}{{ cpu.temperature_c }}°C{% else %}--{% endif %}{% if cpu.fan_rpm %} · {{ cpu.fan_rpm }} RPM{% endif %}</p>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<h3>Memory</h3>
|
||||
|
|
@ -2692,7 +2692,13 @@
|
|||
const cpuValue = document.querySelector('.status-card:nth-child(1) .value');
|
||||
const cpuSub = document.querySelector('.status-card:nth-child(1) .sub');
|
||||
if (cpuValue) cpuValue.textContent = (cpu.percent !== null ? cpu.percent : '--') + '%';
|
||||
if (cpuSub) cpuSub.innerHTML = cpu.temperature ? cpu.temperature + '°C' : '--';
|
||||
if (cpuSub) {
|
||||
let cpuSubText = cpu.temperature ? cpu.temperature + '°C' : '--';
|
||||
if (cpu.fan_rpm) {
|
||||
cpuSubText += ' · ' + cpu.fan_rpm + ' RPM';
|
||||
}
|
||||
cpuSub.innerHTML = cpuSubText;
|
||||
}
|
||||
|
||||
// Update Memory card
|
||||
const memValue = document.querySelector('.status-card:nth-child(2) .value');
|
||||
|
|
|
|||
|
|
@ -52,6 +52,26 @@ class TestGetCpuStatus:
|
|||
assert isinstance(result["cores"], int)
|
||||
assert result["cores"] > 0
|
||||
|
||||
def test_fan_rpm(self):
|
||||
"""Fan RPM should be reported when a fan sensor exists (e.g. RPi 5 pwmfan)."""
|
||||
fan = mock.Mock()
|
||||
fan.current = 2599
|
||||
with mock.patch("controller.lib.status.psutil.sensors_fans", create=True, return_value={"pwmfan": [fan]}):
|
||||
result = get_cpu_status()
|
||||
assert result["fan_rpm"] == 2599
|
||||
|
||||
def test_fan_rpm_no_fan(self):
|
||||
"""Fan RPM should be None when no fan sensor exists."""
|
||||
with mock.patch("controller.lib.status.psutil.sensors_fans", create=True, return_value={}):
|
||||
result = get_cpu_status()
|
||||
assert result["fan_rpm"] is None
|
||||
|
||||
def test_fan_rpm_sensor_error(self):
|
||||
"""Fan RPM should be None when the sensor read fails."""
|
||||
with mock.patch("controller.lib.status.psutil.sensors_fans", create=True, side_effect=OSError("no sysfs")):
|
||||
result = get_cpu_status()
|
||||
assert result["fan_rpm"] is None
|
||||
|
||||
|
||||
class TestGetMemoryStatus:
|
||||
"""Tests for get_memory_status function."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue