mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
Add SpoolBuddy System tab with live OS stats from Raspberry Pi
The daemon now collects CPU temp, core count, load average, memory/disk usage, OS info, and system uptime every heartbeat using stdlib-only reads from /proc and /sys. Stats are sent as a JSON blob in the heartbeat payload, stored in a new system_stats TEXT column, and displayed in a new "System" tab in SpoolBuddy Settings with color-coded usage bars.
This commit is contained in:
@@ -108,19 +108,23 @@ class APIClient:
|
||||
nfc_reader_type: str | None = None,
|
||||
nfc_connection: str | None = None,
|
||||
backend_url: str | None = None,
|
||||
system_stats: dict | None = None,
|
||||
) -> dict | None:
|
||||
payload: dict = {
|
||||
"nfc_ok": nfc_ok,
|
||||
"scale_ok": scale_ok,
|
||||
"uptime_s": uptime_s,
|
||||
"ip_address": ip_address,
|
||||
"firmware_version": firmware_version,
|
||||
"nfc_reader_type": nfc_reader_type,
|
||||
"nfc_connection": nfc_connection,
|
||||
"backend_url": backend_url,
|
||||
}
|
||||
if system_stats is not None:
|
||||
payload["system_stats"] = system_stats
|
||||
result = await self._post(
|
||||
f"/devices/{device_id}/heartbeat",
|
||||
{
|
||||
"nfc_ok": nfc_ok,
|
||||
"scale_ok": scale_ok,
|
||||
"uptime_s": uptime_s,
|
||||
"ip_address": ip_address,
|
||||
"firmware_version": firmware_version,
|
||||
"nfc_reader_type": nfc_reader_type,
|
||||
"nfc_connection": nfc_connection,
|
||||
"backend_url": backend_url,
|
||||
},
|
||||
payload,
|
||||
)
|
||||
if result and self._buffer:
|
||||
await self._flush_buffer()
|
||||
|
||||
@@ -10,7 +10,7 @@ import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from . import __version__
|
||||
from . import __version__, system_stats
|
||||
from .api_client import APIClient
|
||||
from .config import Config
|
||||
from .display_control import DisplayControl
|
||||
@@ -211,6 +211,7 @@ async def heartbeat_loop(config: Config, api: APIClient, start_time: float, shar
|
||||
nfc = shared.get("nfc")
|
||||
scale = shared.get("scale")
|
||||
uptime = int(time.monotonic() - start_time)
|
||||
stats = await asyncio.to_thread(system_stats.collect)
|
||||
result = await api.heartbeat(
|
||||
device_id=config.device_id,
|
||||
nfc_ok=nfc.ok if nfc else False,
|
||||
@@ -221,6 +222,7 @@ async def heartbeat_loop(config: Config, api: APIClient, start_time: float, shar
|
||||
nfc_reader_type=nfc.reader_type if nfc else None,
|
||||
nfc_connection=nfc.connection if nfc else None,
|
||||
backend_url=config.backend_url,
|
||||
system_stats=stats,
|
||||
)
|
||||
|
||||
if result:
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
"""Collect OS-level system stats from the Raspberry Pi using stdlib only."""
|
||||
|
||||
import os
|
||||
import platform
|
||||
|
||||
|
||||
def _read_file(path: str) -> str | None:
|
||||
try:
|
||||
with open(path) as f:
|
||||
return f.read().strip()
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def _cpu_temp() -> float | None:
|
||||
raw = _read_file("/sys/class/thermal/thermal_zone0/temp")
|
||||
if raw is None:
|
||||
return None
|
||||
try:
|
||||
return round(int(raw) / 1000, 1)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def _memory_info() -> dict | None:
|
||||
raw = _read_file("/proc/meminfo")
|
||||
if raw is None:
|
||||
return None
|
||||
info: dict[str, int] = {}
|
||||
for line in raw.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) >= 2 and parts[0].endswith(":"):
|
||||
key = parts[0][:-1]
|
||||
try:
|
||||
info[key] = int(parts[1]) # kB
|
||||
except ValueError:
|
||||
continue
|
||||
total = info.get("MemTotal", 0)
|
||||
available = info.get("MemAvailable", 0)
|
||||
if total == 0:
|
||||
return None
|
||||
return {
|
||||
"total_mb": round(total / 1024),
|
||||
"available_mb": round(available / 1024),
|
||||
"used_mb": round((total - available) / 1024),
|
||||
"percent": round((total - available) / total * 100, 1),
|
||||
}
|
||||
|
||||
|
||||
def _disk_info() -> dict | None:
|
||||
try:
|
||||
st = os.statvfs("/")
|
||||
except OSError:
|
||||
return None
|
||||
total = st.f_frsize * st.f_blocks
|
||||
free = st.f_frsize * st.f_bavail
|
||||
used = total - free
|
||||
if total == 0:
|
||||
return None
|
||||
return {
|
||||
"total_gb": round(total / (1024**3), 1),
|
||||
"used_gb": round(used / (1024**3), 1),
|
||||
"free_gb": round(free / (1024**3), 1),
|
||||
"percent": round(used / total * 100, 1),
|
||||
}
|
||||
|
||||
|
||||
def _load_avg() -> list[float] | None:
|
||||
try:
|
||||
load = os.getloadavg()
|
||||
return [round(x, 2) for x in load]
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def _cpu_count() -> int | None:
|
||||
return os.cpu_count()
|
||||
|
||||
|
||||
def _os_info() -> dict:
|
||||
uname = platform.uname()
|
||||
os_release = _read_file("/etc/os-release")
|
||||
pretty_name = None
|
||||
if os_release:
|
||||
for line in os_release.splitlines():
|
||||
if line.startswith("PRETTY_NAME="):
|
||||
pretty_name = line.split("=", 1)[1].strip().strip('"')
|
||||
break
|
||||
return {
|
||||
"os": pretty_name or f"{uname.system} {uname.release}",
|
||||
"kernel": uname.release,
|
||||
"arch": uname.machine,
|
||||
"python": platform.python_version(),
|
||||
}
|
||||
|
||||
|
||||
def _system_uptime() -> int | None:
|
||||
raw = _read_file("/proc/uptime")
|
||||
if raw is None:
|
||||
return None
|
||||
try:
|
||||
return int(float(raw.split()[0]))
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
def collect() -> dict:
|
||||
"""Collect all system stats. Returns a flat dict safe for JSON serialization."""
|
||||
stats: dict = {}
|
||||
|
||||
stats["os"] = _os_info()
|
||||
|
||||
temp = _cpu_temp()
|
||||
if temp is not None:
|
||||
stats["cpu_temp_c"] = temp
|
||||
|
||||
cpu_count = _cpu_count()
|
||||
if cpu_count is not None:
|
||||
stats["cpu_count"] = cpu_count
|
||||
|
||||
load = _load_avg()
|
||||
if load is not None:
|
||||
stats["load_avg"] = load
|
||||
|
||||
mem = _memory_info()
|
||||
if mem is not None:
|
||||
stats["memory"] = mem
|
||||
|
||||
disk = _disk_info()
|
||||
if disk is not None:
|
||||
stats["disk"] = disk
|
||||
|
||||
uptime = _system_uptime()
|
||||
if uptime is not None:
|
||||
stats["system_uptime_s"] = uptime
|
||||
|
||||
return stats
|
||||
Reference in New Issue
Block a user