mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
The SSE status stream sent 'disk_used_percent': 0 as a literal, so every
consumer of the live view showed 0% disk no matter how full the card was.
/api/v3/system/status computed it correctly; the stream that the dashboard
actually watches did not. On a Pi with a modest SD card that is the warning a
user most needs, and it was guaranteed to never appear.
The stream also omitted memory_available_mb. /api/v3/system/status carries it
with a comment spelling out why it matters: MemAvailable accounts for
reclaimable page cache, so it is what separates a board reading 70% "used" that
is fine from one reading 70% that is about to fail fork(). A 1GB Pi 3B+ can sit
at either. The number that predicts the failure was missing from the live view.
An unreadable disk now reports None rather than 0. The UI already renders null
as '--'; a confident 0 reads as "plenty of room", which is worse than a blank.
Metric collection moves to web_interface/system_metrics.py, with no Flask or app
imports. That is not cosmetic: importing web_interface.app constructs the Flask
application and a CacheManager, and the latter claims the cache directory with a
cleanup thread. The first version of these tests imported the generator directly
and broke test_cache_cleanup_thread_ownership ("one thread per directory") plus
four starlark route tests through that side effect. Reading a CPU percentage
should not boot a web application, and testing it should not either.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
63 lines
2.2 KiB
Python
63 lines
2.2 KiB
Python
"""System metrics for the status stream.
|
|
|
|
Deliberately free of Flask and app imports: importing web_interface.app
|
|
constructs the Flask application and a CacheManager, and the latter claims the
|
|
cache directory with a cleanup thread. Reading a CPU percentage should not do
|
|
that, and neither should testing it.
|
|
|
|
Every value is best-effort. A number that cannot be taken comes back as None so
|
|
the UI can render '--', because a confident wrong number is worse than a blank:
|
|
``disk_used_percent`` used to be hardcoded to 0, which read as "plenty of room"
|
|
on a card that was filling up.
|
|
"""
|
|
|
|
from typing import Any, Dict, Optional
|
|
|
|
_THERMAL_ZONE = '/sys/class/thermal/thermal_zone0/temp'
|
|
|
|
|
|
def _cpu_temp_c() -> Optional[float]:
|
|
"""CPU temperature in degrees C, or None off a Raspberry Pi."""
|
|
try:
|
|
with open(_THERMAL_ZONE, 'r') as f:
|
|
return round(float(f.read()) / 1000.0, 1)
|
|
except (OSError, ValueError):
|
|
return None
|
|
|
|
|
|
def collect_system_metrics() -> Dict[str, Any]:
|
|
"""Collect the numbers that predict trouble on a small board.
|
|
|
|
``memory_available_mb`` is MemAvailable rather than a used percentage: it
|
|
accounts for reclaimable page cache, so it is what separates a board at 70%
|
|
"used" that is fine from one at 70% that is about to fail fork(). A 1GB Pi
|
|
can sit at either and only this number tells them apart.
|
|
"""
|
|
try:
|
|
import psutil
|
|
except ImportError:
|
|
return {
|
|
'cpu_percent': 0,
|
|
'memory_used_percent': 0,
|
|
'memory_available_mb': None,
|
|
'disk_used_percent': None,
|
|
'cpu_temp': _cpu_temp_c() or 0,
|
|
}
|
|
|
|
# interval=None is non-blocking; app startup primes psutil's internal state.
|
|
cpu_percent = round(psutil.cpu_percent(interval=None), 1)
|
|
memory = psutil.virtual_memory()
|
|
|
|
try:
|
|
disk_used_percent: Optional[float] = round(psutil.disk_usage('/').percent, 1)
|
|
except OSError:
|
|
disk_used_percent = None
|
|
|
|
return {
|
|
'cpu_percent': cpu_percent,
|
|
'memory_used_percent': round(memory.percent, 1),
|
|
'memory_available_mb': round(memory.available / (1024 * 1024), 1),
|
|
'disk_used_percent': disk_used_percent,
|
|
'cpu_temp': _cpu_temp_c() or 0,
|
|
}
|