system_stats.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. """Collect OS-level system stats from the Raspberry Pi using stdlib only."""
  2. import os
  3. import platform
  4. def _read_file(path: str) -> str | None:
  5. try:
  6. with open(path) as f:
  7. return f.read().strip()
  8. except OSError:
  9. return None
  10. def _cpu_temp() -> float | None:
  11. raw = _read_file("/sys/class/thermal/thermal_zone0/temp")
  12. if raw is None:
  13. return None
  14. try:
  15. return round(int(raw) / 1000, 1)
  16. except (ValueError, TypeError):
  17. return None
  18. def _memory_info() -> dict | None:
  19. raw = _read_file("/proc/meminfo")
  20. if raw is None:
  21. return None
  22. info: dict[str, int] = {}
  23. for line in raw.splitlines():
  24. parts = line.split()
  25. if len(parts) >= 2 and parts[0].endswith(":"):
  26. key = parts[0][:-1]
  27. try:
  28. info[key] = int(parts[1]) # kB
  29. except ValueError:
  30. continue
  31. total = info.get("MemTotal", 0)
  32. available = info.get("MemAvailable", 0)
  33. if total == 0:
  34. return None
  35. return {
  36. "total_mb": round(total / 1024),
  37. "available_mb": round(available / 1024),
  38. "used_mb": round((total - available) / 1024),
  39. "percent": round((total - available) / total * 100, 1),
  40. }
  41. def _disk_info() -> dict | None:
  42. try:
  43. st = os.statvfs("/")
  44. except OSError:
  45. return None
  46. total = st.f_frsize * st.f_blocks
  47. free = st.f_frsize * st.f_bavail
  48. used = total - free
  49. if total == 0:
  50. return None
  51. return {
  52. "total_gb": round(total / (1024**3), 1),
  53. "used_gb": round(used / (1024**3), 1),
  54. "free_gb": round(free / (1024**3), 1),
  55. "percent": round(used / total * 100, 1),
  56. }
  57. def _load_avg() -> list[float] | None:
  58. try:
  59. load = os.getloadavg()
  60. return [round(x, 2) for x in load]
  61. except OSError:
  62. return None
  63. def _cpu_count() -> int | None:
  64. return os.cpu_count()
  65. def _os_info() -> dict:
  66. uname = platform.uname()
  67. os_release = _read_file("/etc/os-release")
  68. pretty_name = None
  69. if os_release:
  70. for line in os_release.splitlines():
  71. if line.startswith("PRETTY_NAME="):
  72. pretty_name = line.split("=", 1)[1].strip().strip('"')
  73. break
  74. return {
  75. "os": pretty_name or f"{uname.system} {uname.release}",
  76. "kernel": uname.release,
  77. "arch": uname.machine,
  78. "python": platform.python_version(),
  79. }
  80. def _system_uptime() -> int | None:
  81. raw = _read_file("/proc/uptime")
  82. if raw is None:
  83. return None
  84. try:
  85. return int(float(raw.split()[0]))
  86. except (ValueError, IndexError):
  87. return None
  88. def collect() -> dict:
  89. """Collect all system stats. Returns a flat dict safe for JSON serialization."""
  90. stats: dict = {}
  91. stats["os"] = _os_info()
  92. temp = _cpu_temp()
  93. if temp is not None:
  94. stats["cpu_temp_c"] = temp
  95. cpu_count = _cpu_count()
  96. if cpu_count is not None:
  97. stats["cpu_count"] = cpu_count
  98. load = _load_avg()
  99. if load is not None:
  100. stats["load_avg"] = load
  101. mem = _memory_info()
  102. if mem is not None:
  103. stats["memory"] = mem
  104. disk = _disk_info()
  105. if disk is not None:
  106. stats["disk"] = disk
  107. uptime = _system_uptime()
  108. if uptime is not None:
  109. stats["system_uptime_s"] = uptime
  110. return stats