From 748ecf33b367df4508d431f24497e716717de323 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Sat, 29 Nov 2025 22:42:15 +0000 Subject: [PATCH] hardware statistics integrate with storage collector --- repeater/data_acquisition/hardware_stats.py | 156 +++++++++++--------- repeater/web/api_endpoints.py | 24 ++- 2 files changed, 101 insertions(+), 79 deletions(-) diff --git a/repeater/data_acquisition/hardware_stats.py b/repeater/data_acquisition/hardware_stats.py index af6d5b2..a19ffc0 100644 --- a/repeater/data_acquisition/hardware_stats.py +++ b/repeater/data_acquisition/hardware_stats.py @@ -3,7 +3,13 @@ Hardware statistics collection using psutil. KISS - Keep It Simple Stupid approach. """ -import psutil +try: + import psutil + PSUTIL_AVAILABLE = True +except ImportError: + PSUTIL_AVAILABLE = False + psutil = None + import time import logging @@ -11,17 +17,19 @@ 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. - """ + + 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() @@ -34,7 +42,6 @@ class HardwareStatsCollector: # Memory stats memory = psutil.virtual_memory() - swap = psutil.swap_memory() # Disk stats disk = psutil.disk_usage('/') @@ -42,123 +49,126 @@ class HardwareStatsCollector: # 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 + # 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 = { - "timestamp": now, - "uptime_seconds": uptime, - "system_uptime_seconds": system_uptime, - - # CPU "cpu": { - "percent": cpu_percent, + "usage_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 + "frequency": cpu_freq.current if cpu_freq else 0, + "load_avg": { + "1min": load_avg[0], + "5min": load_avg[1], + "15min": load_avg[2] + } }, - - # 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 + "total": memory.total, + "available": memory.available, + "used": memory.used, + "usage_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) + "total": disk.total, + "used": disk.used, + "free": disk.free, + "usage_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 + "packets_recv": net_io.packets_recv }, - - # Temperature - "temperature_celsius": temperature + "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 { - "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. + 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']): + for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent', 'memory_info']): try: - processes.append(proc.info) + 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 - 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] + # Sort by CPU usage and get top processes + top_processes = sorted(processes, key=lambda x: x['cpu_percent'], reverse=True)[:limit] return { - "top_cpu": top_cpu, - "top_memory": top_memory, + "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) } \ No newline at end of file diff --git a/repeater/web/api_endpoints.py b/repeater/web/api_endpoints.py index 7b7fc2f..1147b6e 100644 --- a/repeater/web/api_endpoints.py +++ b/repeater/web/api_endpoints.py @@ -8,7 +8,6 @@ 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") @@ -65,7 +64,6 @@ 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) @@ -250,8 +248,15 @@ class APIEndpoints: def hardware_stats(self): """Get comprehensive hardware statistics""" try: - stats = self.hardware_stats.get_stats() - return self._success(stats) + # Get hardware stats from storage collector + if hasattr(self.daemon_instance, 'storage_collector') and self.daemon_instance.storage_collector: + stats = self.daemon_instance.storage_collector.get_hardware_stats() + if stats: + return self._success(stats) + else: + return self._error("Hardware stats not available (psutil may not be installed)") + else: + return self._error("Storage collector not available") except Exception as e: logger.error(f"Error getting hardware stats: {e}") return self._error(e) @@ -261,8 +266,15 @@ class APIEndpoints: def hardware_processes(self): """Get summary of top processes""" try: - processes = self.hardware_stats.get_processes_summary() - return self._success(processes) + # Get process stats from storage collector + if hasattr(self.daemon_instance, 'storage_collector') and self.daemon_instance.storage_collector: + processes = self.daemon_instance.storage_collector.get_hardware_processes() + if processes: + return self._success(processes) + else: + return self._error("Process information not available (psutil may not be installed)") + else: + return self._error("Storage collector not available") except Exception as e: logger.error(f"Error getting process stats: {e}") return self._error(e)