#!/usr/bin/env python3 # Simple stat daemon # Listens on a port for requests, responds with some basic information about the current state of the system. import configparser import os import subprocess import time import argparse import json import http.server import socketserver def get_default_config(): config = configparser.ConfigParser(interpolation=None) config['stat.system.uptime'] = { 'command': 'uptime -p | cut -d" " -f2-', 'format': '{}' } config['stat.cpu.temp'] = { 'command': 'sensors -j | jq \'.["k10temp-pci-00c3"]["Tctl"]["temp1_input"]\'', 'transform': 'lambda x: float(x)', 'format': '{:.1f}', 'unit': '°C', 'historical': '1' } config['stat.cpu.act'] = { 'command': 'top -bn1 | grep "Cpu(s)" | sed "s/.*, *\\([0-9.]*\\)%%* id.*/\\1/" | awk \'{print 100-$1}\'', 'transform': 'lambda x: float(x)', 'format': '{:.1f}', 'unit': '%', 'historical': '1' } config['stat.memory.pct'] = { 'command': 'free | awk \'NR==2 {printf "%.1f", $3*100/$2}\'', 'transform': 'lambda x: float(x)', 'format': '{:.1f}', 'unit': '%', 'historical': '1' } config['stat.memory.total'] = { 'command': 'free -m | awk \'NR==2 {print $2}\'', 'transform': 'lambda x: int(x)', 'format': '{}', 'unit': 'MB' } config['stat.disk.root'] = { 'command': 'df -h / | awk \'NR==2 {printf "%s/%s [%d%%]", $3, $2, $5}\'', 'format': '{}' } return config def get_config(): config_home = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) config_dir = os.path.join(config_home, "stats") if not os.path.exists(config_dir): os.makedirs(config_dir) config_file = os.path.join(config_dir, "config.ini") config = configparser.ConfigParser(interpolation=None) if os.path.exists(config_file): config.read(config_file) else: config = get_default_config() with open(config_file, 'w') as f: config.write(f) return config def get_stats(config): stats = {} stat_sections = [s for s in config.sections() if s.startswith('stat.')] for section in stat_sections: # Split into category and name _, category, name = section.split('.') stat_config = config[section] if category not in stats: stats[category] = {} try: value = subprocess.getoutput(stat_config['command']).strip() transform = stat_config.get('transform', None) format = stat_config.get('format', None) unit = stat_config.get('unit', None) historical = stat_config.get('historical', False) if transform: transform_func = eval(transform) value = transform_func(value) if format: value = format.format(value) data = { 'value': value } if unit: data['unit'] = unit if historical: data['track'] = bool(historical) stats[category][name] = data except Exception as e: print(e) #stats[category][name] = f"Error: {str(e)}" return stats class StatsHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self): data = { 'timestamp': time.time(), 'stats': get_stats(get_config()) } response = json.dumps(data).encode('utf-8') self.send_response(200) self.send_header('Content-Type', 'application/json') self.send_header('Content-Length', len(response)) self.end_headers() self.wfile.write(response) class StatsServer(socketserver.TCPServer): allow_reuse_address = True def main(): parser = argparse.ArgumentParser(description='Stats HTTP Server') parser.add_argument('--port', type=int, default=5747, help='Port to listen on') parser.add_argument('--host', default='0.0.0.0', help='Host to bind to') args = parser.parse_args() with StatsServer((args.host, args.port), StatsHandler) as httpd: print(f"Serving stats on http://{args.host}:{args.port}") try: httpd.serve_forever() except KeyboardInterrupt: print("\nShutting down server...") httpd.server_close() if __name__ == '__main__': main()