diff --git a/README.md b/README.md index e149f58..4395760 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ While created for my own use, they are shared here in case others find them usef - **indeel**: File organization tool that watches a directory and sorts incoming files based on content hashes - **mover**: Monitors a source directory and moves files with specified extensions to a destination directory - **pushover**: Sends push notifications via Pushover API +- **stats**: HTTP server that provides system statistics (CPU, memory, disk usage, etc.) in JSON format - **vc**: Video converter using ffmpeg and youtube-dl for local files or YouTube URLs - **vcmp**: Advanced video compression utility with progress tracking and logging diff --git a/bin/stats b/bin/stats new file mode 100755 index 0000000..27add29 --- /dev/null +++ b/bin/stats @@ -0,0 +1,138 @@ +#!/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()