Files
pyMC_Repeater/repeater/data_acquisition/hardware_stats.py
agessaman c2f8a2e3cd refactor: companion FrameServer and related (substantive only, no Black)
Reapply refactor from ce8381a (replace monolithic FrameServer with thin
pymc_core subclass, re-export constants, SQLite persistence hooks) while
preserving pre-refactor whitespace where patch applied cleanly. Remaining
files match refactor commit exactly. Diff vs ce8381a is whitespace-only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 15:35:47 -08:00

160 lines
5.5 KiB
Python

"""
Hardware statistics collection using psutil.
KISS - Keep It Simple Stupid approach.
"""
try:
import psutil
PSUTIL_AVAILABLE = True
except ImportError:
PSUTIL_AVAILABLE = False
psutil = None
import logging
import time
logger = logging.getLogger("HardwareStats")
class HardwareStatsCollector:
def __init__(self):
self.start_time = time.time()
def get_stats(self):
if not PSUTIL_AVAILABLE:
logger.error("psutil not available - cannot collect hardware stats")
return {"error": "psutil library not available - cannot collect hardware statistics"}
try:
# Get current timestamp
now = time.time()
uptime = now - self.start_time
# CPU stats
cpu_percent = psutil.cpu_percent(interval=0.1)
cpu_count = psutil.cpu_count()
cpu_freq = psutil.cpu_freq()
# Memory stats
memory = psutil.virtual_memory()
# Disk stats
disk = psutil.disk_usage("/")
# Network stats (total across all interfaces)
net_io = psutil.net_io_counters()
# Load average (Unix only)
load_avg = None
try:
load_avg = psutil.getloadavg()
except (AttributeError, OSError):
# Not available on all systems - use zeros
load_avg = (0.0, 0.0, 0.0)
# System boot time
boot_time = psutil.boot_time()
system_uptime = now - boot_time
# Temperature (if available)
temperatures = {}
try:
temps = psutil.sensors_temperatures()
for name, entries in temps.items():
for i, entry in enumerate(entries):
temp_name = f"{name}_{i}" if len(entries) > 1 else name
temperatures[temp_name] = entry.current
except (AttributeError, OSError):
# Temperature sensors not available
pass
# Format data structure to match Vue component expectations
stats = {
"cpu": {
"usage_percent": cpu_percent,
"count": cpu_count,
"frequency": cpu_freq.current if cpu_freq else 0,
"load_avg": {"1min": load_avg[0], "5min": load_avg[1], "15min": load_avg[2]},
},
"memory": {
"total": memory.total,
"available": memory.available,
"used": memory.used,
"usage_percent": memory.percent,
},
"disk": {
"total": disk.total,
"used": disk.used,
"free": disk.free,
"usage_percent": round((disk.used / disk.total) * 100, 1),
},
"network": {
"bytes_sent": net_io.bytes_sent,
"bytes_recv": net_io.bytes_recv,
"packets_sent": net_io.packets_sent,
"packets_recv": net_io.packets_recv,
},
"system": {"uptime": system_uptime, "boot_time": boot_time},
}
# Add temperatures if available
if temperatures:
stats["temperatures"] = temperatures
return stats
except Exception as e:
logger.error(f"Error collecting hardware stats: {e}")
return {"error": str(e)}
def get_processes_summary(self, limit=10):
"""
Get top processes by CPU and memory usage.
Returns a dictionary with process information in the format expected by the UI.
"""
if not PSUTIL_AVAILABLE:
logger.error("psutil not available - cannot collect process stats")
return {
"processes": [],
"total_processes": 0,
"error": "psutil library not available - cannot collect process statistics",
}
try:
processes = []
# Get all processes
for proc in psutil.process_iter(
["pid", "name", "cpu_percent", "memory_percent", "memory_info"]
):
try:
pinfo = proc.info
# Calculate memory in MB
memory_mb = 0
if pinfo["memory_info"]:
memory_mb = pinfo["memory_info"].rss / 1024 / 1024 # RSS in MB
process_data = {
"pid": pinfo["pid"],
"name": pinfo["name"] or "Unknown",
"cpu_percent": pinfo["cpu_percent"] or 0.0,
"memory_percent": pinfo["memory_percent"] or 0.0,
"memory_mb": round(memory_mb, 1),
}
processes.append(process_data)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
# Sort by CPU usage and get top processes
top_processes = sorted(processes, key=lambda x: x["cpu_percent"], reverse=True)[:limit]
return {"processes": top_processes, "total_processes": len(processes)}
except Exception as e:
logger.error(f"Error collecting process stats: {e}")
return {"processes": [], "total_processes": 0, "error": str(e)}