emo-online-api / emo /desktop /system_monitor.py
Emo Online
Deploy Emo API
dd87944
Raw
History Blame Contribute Delete
6.63 kB
"""Surveillance CPU/RAM — métriques UI + alertes vocales."""
from __future__ import annotations
import ctypes
import platform
import threading
import time
from dataclasses import dataclass
from typing import Callable
try:
import psutil
except ImportError:
psutil = None # type: ignore
_OS = platform.system()
DEFAULT_THRESHOLDS = {
"cpu": 90.0,
"ram": 90.0,
"temp": 85.0,
"gpu": 95.0,
}
_COOLDOWN = 300
_CPU_STREAK = 3
_nvml_lib: object = None
_nvml_ok: object = None
@dataclass
class SystemStats:
cpu_percent: float = 0.0
ram_percent: float = 0.0
ram_used_mb: float = 0.0
ram_total_mb: float = 0.0
def summary(self) -> str:
return (
f"CPU {self.cpu_percent:.0f}% | "
f"RAM {self.ram_percent:.0f}% "
f"({self.ram_used_mb:.0f}/{self.ram_total_mb:.0f} Mo)"
)
def _nvml_gpu() -> float:
global _nvml_lib, _nvml_ok
if _nvml_ok is False:
return -1.0
try:
class _Util(ctypes.Structure):
_fields_ = [("gpu", ctypes.c_uint), ("memory", ctypes.c_uint)]
if _nvml_lib is None:
if _OS == "Windows":
candidates = ("nvml", r"C:\Windows\System32\nvml.dll")
_load = ctypes.WinDLL
else:
candidates = ("libnvidia-ml.so.1", "libnvidia-ml.so", "libnvidia-ml.dylib")
_load = ctypes.CDLL
for name in candidates:
try:
lib = _load(name)
lib.nvmlInit_v2()
_nvml_lib = lib
break
except Exception:
continue
if _nvml_lib is None:
_nvml_ok = False
return -1.0
dev = ctypes.c_void_p()
_nvml_lib.nvmlDeviceGetHandleByIndex_v2(0, ctypes.byref(dev))
u = _Util()
_nvml_lib.nvmlDeviceGetUtilizationRates(dev, ctypes.byref(u))
_nvml_ok = True
return float(u.gpu)
except Exception:
_nvml_ok = False
return -1.0
def _get_gpu_usage() -> float:
try:
import pynvml # type: ignore
pynvml.nvmlInit()
h = pynvml.nvmlDeviceGetHandleByIndex(0)
return float(pynvml.nvmlDeviceGetUtilizationRates(h).gpu)
except Exception:
pass
return _nvml_gpu()
def _get_cpu_temp() -> float:
if psutil is None:
return -1.0
try:
temps = psutil.sensors_temperatures()
for name in ["coretemp", "k10temp", "cpu_thermal", "acpitz", "cpu-thermal"]:
if name in temps and temps[name]:
return temps[name][0].current
for entries in temps.values():
if entries:
return entries[0].current
except Exception:
pass
if _OS == "Windows":
try:
import wmi # type: ignore
w = wmi.WMI(namespace="root/wmi")
tz = w.MSAcpi_ThermalZoneTemperature()
if tz:
return (tz[0].CurrentTemperature / 10.0) - 273.15
except Exception:
pass
return -1.0
class VoiceAlertMonitor:
"""Alertes vocales quand les métriques dépassent les seuils."""
def __init__(self, thresholds: dict | None = None):
self.thresholds = {**DEFAULT_THRESHOLDS, **(thresholds or {})}
self._last_alert: dict[str, float] = {}
self._cpu_streak = 0
def _can_alert(self, key: str) -> bool:
return (time.monotonic() - self._last_alert.get(key, 0)) > _COOLDOWN
def _record(self, key: str) -> None:
self._last_alert[key] = time.monotonic()
def check(self) -> str | None:
if psutil is None:
return None
try:
cpu = psutil.cpu_percent(interval=None)
ram = psutil.virtual_memory().percent
temp = _get_cpu_temp()
gpu = _get_gpu_usage()
except Exception:
return None
alerts: list[str] = []
if cpu >= self.thresholds["cpu"]:
self._cpu_streak += 1
if self._cpu_streak >= _CPU_STREAK and self._can_alert("cpu"):
alerts.append(
f"[SYSTEM_ALERT] CPU usage has been critically high ({cpu:.0f}%). "
"Warn the user in their language and suggest closing heavy applications."
)
self._record("cpu")
self._cpu_streak = 0
else:
self._cpu_streak = 0
if ram >= self.thresholds["ram"] and self._can_alert("ram"):
alerts.append(
f"[SYSTEM_ALERT] RAM is at {ram:.0f}% — nearly exhausted. "
"Warn the user in their language and suggest freeing memory."
)
self._record("ram")
if temp > 0 and temp >= self.thresholds["temp"] and self._can_alert("temp"):
alerts.append(
f"[SYSTEM_ALERT] CPU temperature is {temp:.0f}°C — above the safe limit. "
"Warn the user in their language."
)
self._record("temp")
if gpu >= 0 and gpu >= self.thresholds["gpu"] and self._can_alert("gpu"):
alerts.append(
f"[SYSTEM_ALERT] GPU load is at {gpu:.0f}%. "
"Briefly inform the user in their language."
)
self._record("gpu")
return " ".join(alerts) if alerts else None
class SystemMonitor(threading.Thread):
def __init__(self, interval: float = 10.0, on_update: Callable[[SystemStats], None] | None = None):
super().__init__(daemon=True, name="emo-system-monitor")
self.interval = interval
self.on_update = on_update
self._stop = threading.Event()
self.stats = SystemStats()
def stop(self) -> None:
self._stop.set()
def snapshot(self) -> SystemStats:
if psutil is None:
return SystemStats()
try:
cpu = psutil.cpu_percent(interval=0.1)
vm = psutil.virtual_memory()
self.stats = SystemStats(
cpu_percent=cpu,
ram_percent=vm.percent,
ram_used_mb=vm.used / (1024 * 1024),
ram_total_mb=vm.total / (1024 * 1024),
)
except Exception:
pass
return self.stats
def run(self) -> None:
while not self._stop.is_set():
stats = self.snapshot()
if self.on_update:
try:
self.on_update(stats)
except Exception:
pass
self._stop.wait(self.interval)