From 330d97d862d3441dc3ab5bf9109e961695254d5b Mon Sep 17 00:00:00 2001 From: Lloyd Date: Sat, 29 Nov 2025 22:13:53 +0000 Subject: [PATCH] add hardware statistics collection and new API endpoints for hardware stats --- pyproject.toml | 1 + repeater/data_acquisition/hardware_stats.py | 164 ++++++++++++++++++++ repeater/web/api_endpoints.py | 24 +++ 3 files changed, 189 insertions(+) create mode 100644 repeater/data_acquisition/hardware_stats.py diff --git a/pyproject.toml b/pyproject.toml index 9160b98..c4522c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ "cherrypy>=18.0.0", "paho-mqtt>=1.6.0", "cherrypy-cors==1.7.0", + "psutil>=5.9.0", ] diff --git a/repeater/data_acquisition/hardware_stats.py b/repeater/data_acquisition/hardware_stats.py new file mode 100644 index 0000000..af6d5b2 --- /dev/null +++ b/repeater/data_acquisition/hardware_stats.py @@ -0,0 +1,164 @@ +""" +Hardware statistics collection using psutil. +KISS - Keep It Simple Stupid approach. +""" + +import psutil +import time +import logging + +logger = logging.getLogger("HardwareStats") + + +class HardwareStatsCollector: + """Simple hardware statistics collector using psutil.""" + + def __init__(self): + """Initialize the hardware stats collector.""" + self.start_time = time.time() + + def get_stats(self): + """ + Get current hardware statistics. + Returns a dictionary with system stats. + """ + 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() + swap = psutil.swap_memory() + + # Disk stats + disk = psutil.disk_usage('/') + + # Network stats (total across all interfaces) + net_io = psutil.net_io_counters() + + # Temperature (if available) + temperature = None + try: + temps = psutil.sensors_temperatures() + if 'cpu_thermal' in temps and len(temps['cpu_thermal']) > 0: + temperature = temps['cpu_thermal'][0].current + elif temps: + # Fallback to first available temperature sensor + first_sensor = next(iter(temps.values())) + if first_sensor: + temperature = first_sensor[0].current + except (AttributeError, OSError): + # Temperature sensors not available + pass + + # Load average (Unix only) + load_avg = None + try: + load_avg = psutil.getloadavg() + except (AttributeError, OSError): + # Not available on all systems + pass + + # System boot time + boot_time = psutil.boot_time() + system_uptime = now - boot_time + + stats = { + "timestamp": now, + "uptime_seconds": uptime, + "system_uptime_seconds": system_uptime, + + # CPU + "cpu": { + "percent": cpu_percent, + "count": cpu_count, + "frequency_mhz": cpu_freq.current if cpu_freq else None, + "load_average": list(load_avg) if load_avg else None + }, + + # Memory + "memory": { + "total_mb": round(memory.total / 1024 / 1024, 1), + "available_mb": round(memory.available / 1024 / 1024, 1), + "used_mb": round(memory.used / 1024 / 1024, 1), + "percent": memory.percent + }, + + # Swap + "swap": { + "total_mb": round(swap.total / 1024 / 1024, 1), + "used_mb": round(swap.used / 1024 / 1024, 1), + "percent": swap.percent + }, + + # Disk + "disk": { + "total_gb": round(disk.total / 1024 / 1024 / 1024, 1), + "used_gb": round(disk.used / 1024 / 1024 / 1024, 1), + "free_gb": round(disk.free / 1024 / 1024 / 1024, 1), + "percent": round((disk.used / disk.total) * 100, 1) + }, + + # Network + "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, + "errors_in": net_io.errin, + "errors_out": net_io.errout, + "drops_in": net_io.dropin, + "drops_out": net_io.dropout + }, + + # Temperature + "temperature_celsius": temperature + } + + return stats + + except Exception as e: + logger.error(f"Error collecting hardware stats: {e}") + return { + "timestamp": time.time(), + "error": str(e) + } + + def get_processes_summary(self, limit=10): + """ + Get top processes by CPU and memory usage. + Returns a dictionary with process information. + """ + try: + processes = [] + + # Get all processes + for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']): + try: + processes.append(proc.info) + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + + # Sort by CPU usage + top_cpu = sorted(processes, key=lambda x: x['cpu_percent'] or 0, reverse=True)[:limit] + + # Sort by memory usage + top_memory = sorted(processes, key=lambda x: x['memory_percent'] or 0, reverse=True)[:limit] + + return { + "top_cpu": top_cpu, + "top_memory": top_memory, + "total_processes": len(processes) + } + + except Exception as e: + logger.error(f"Error collecting process stats: {e}") + return { + "error": str(e) + } \ No newline at end of file diff --git a/repeater/web/api_endpoints.py b/repeater/web/api_endpoints.py index b4205c4..7b7fc2f 100644 --- a/repeater/web/api_endpoints.py +++ b/repeater/web/api_endpoints.py @@ -8,6 +8,7 @@ import cherrypy from repeater import __version__ from repeater.config import update_global_flood_policy from .cad_calibration_engine import CADCalibrationEngine +from repeater.data_acquisition.hardware_stats import HardwareStatsCollector logger = logging.getLogger("HTTPServer") @@ -64,6 +65,7 @@ class APIEndpoints: self.daemon_instance = daemon_instance self._config_path = config_path or '/etc/pymc_repeater/config.yaml' self.cad_calibration = CADCalibrationEngine(daemon_instance, event_loop) + self.hardware_stats = HardwareStatsCollector() def _is_cors_enabled(self): return self.config.get("web", {}).get("cors_enabled", False) @@ -243,6 +245,28 @@ class APIEndpoints: logger.error(f"Error fetching logs: {e}") return {"error": str(e), "logs": []} + @cherrypy.expose + @cherrypy.tools.json_out() + def hardware_stats(self): + """Get comprehensive hardware statistics""" + try: + stats = self.hardware_stats.get_stats() + return self._success(stats) + except Exception as e: + logger.error(f"Error getting hardware stats: {e}") + return self._error(e) + + @cherrypy.expose + @cherrypy.tools.json_out() + def hardware_processes(self): + """Get summary of top processes""" + try: + processes = self.hardware_stats.get_processes_summary() + return self._success(processes) + except Exception as e: + logger.error(f"Error getting process stats: {e}") + return self._error(e) + @cherrypy.expose @cherrypy.tools.json_out() def packet_stats(self, hours=24):