commit 9e94e93f12a9d9718c9ba685bbb41662691fc5ad Author: Taras Syvash Date: Sun Oct 26 17:24:37 2025 +0200 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80ef064 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +temperature_history.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd25bce --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Монітор температури Raspberry Pi + +## Підготовка до запуску + +1. Встановіть необхідні залежності: +```bash +pip install flask paramiko psutil +``` + +2. Створіть файл конфігурації `config.json`: +```json +{ + "devices": [ + { + "host": "192.168.1.100", + "username": "pi", + "password": "raspberry" + } + ] +} +``` + +## Запуск додатку + +1. Запустіть колектор даних в окремому терміналі: +```bash +python collector.py +``` + +2. Запустіть Flask-додаток в іншому терміналі: +```bash +python app.py +``` + +3. Відкрийте веб-браузер та перейдіть за адресою: +``` +http://localhost:8080 +``` + +## Примітки + +- Переконайтеся, що всі Raspberry Pi доступні в мережі +- Перевірте правильність логіну та паролю в config.json +- За замовчуванням додаток буде доступний на порту 8080 +- Для доступу з інших пристроїв використовуйте IP-адресу комп'ютера замість localhost + +## Структура проекту + +``` +PI-SYSTEM-MONITOR/ +├── app.py # Основний Flask-додаток +├── config.json # Конфігурація підключень +└── templates/ + └── index.html # HTML шаблон +``` diff --git a/__pycache__/database.cpython-314.pyc b/__pycache__/database.cpython-314.pyc new file mode 100644 index 0000000..0e5cfcf Binary files /dev/null and b/__pycache__/database.cpython-314.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..2005658 --- /dev/null +++ b/app.py @@ -0,0 +1,147 @@ +from flask import Flask, render_template +import psutil +import paramiko +import json +from collections import defaultdict +from datetime import datetime +from database import TemperatureDB + +app = Flask(__name__) + +CONFIG_FILE = "config.json" +db = TemperatureDB() +# Зберігаємо історію температур (останні 20 значень для кожного пристрою) +temperature_history = defaultdict(lambda: {'gpu': [], 'cpu': [], 'timestamps': []}) +MAX_HISTORY = 20 + +def load_config(file_path): + """Завантаження налаштувань з JSON файлу.""" + try: + with open(file_path, "r") as file: + return json.load(file) + except Exception as e: + print(f"Помилка завантаження конфігурації: {e}") + return {"devices": []} + +def get_remote_temperature(host, username, password): + """Підключення до Raspberry Pi через SSH та отримання температури.""" + try: + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + client.connect(hostname=host, username=username, password=password) + + # Отримуємо hostname + stdin, stdout, stderr = client.exec_command("hostname") + hostname = stdout.read().decode().strip() + + # Перевіряємо модель Raspberry Pi + stdin, stdout, stderr = client.exec_command("cat /proc/cpuinfo | grep Model") + model_info = stdout.read().decode().strip() + is_pi5 = "Raspberry Pi 5" in model_info + + # Вибираємо правильну команду залежно від моделі + if is_pi5: + temp_command = "vcgencmd measure_temp" + stdin, stdout, stderr = client.exec_command(temp_command) + output = stdout.read().decode().strip() + temp_value = float(output.replace("temp=", "").replace("'C", "")) + + # Отримуємо додаткову інформацію про процесор + stdin, stdout, stderr = client.exec_command("cat /sys/class/thermal/thermal_zone0/temp") + cpu_temp = float(stdout.read().decode().strip()) / 1000 + + client.close() + return { + 'temp': temp_value, + 'cpu_temp': cpu_temp, + 'model': 'Raspberry Pi 5', + 'status': 'OK', + 'hostname': hostname + } + else: + client.close() + return { + 'temp': None, + 'cpu_temp': None, + 'model': model_info if model_info else 'Unknown', + 'status': 'Unsuported device', + 'hostname': hostname + } + + except Exception as e: + print(f"Помилка підключення до {host}: {e}") + return { + 'temp': None, + 'cpu_temp': None, + 'model': 'Unknown', + 'status': f'Error: {str(e)}', + 'hostname': 'Unknown' + } + +@app.route('/') +def index(): + temperatures = [] + current_time = datetime.now().strftime('%H:%M:%S') + + # Очищення старих записів при кожному запиті + db.cleanup_old_records() + + # Отримуємо віддалені температури + config = load_config(CONFIG_FILE) + for device in config.get("devices", []): + host = device.get("host") + username = device.get("username") + password = device.get("password") + + if host and username and password: + temp_data = get_remote_temperature(host, username, password) + if temp_data['temp'] is not None: + # Отримуємо історію з бази даних + history = db.get_history(host) + + temperatures.append({ + 'name': f"Raspberry Pi 5: {host}", + 'value': round(temp_data['temp'], 1), + 'cpu': round(temp_data['cpu_temp'], 1), + 'status': temp_data['status'], + 'hostname': temp_data['hostname'], + 'history': history + }) + else: + temperatures.append({ + 'name': f"Пристрій: {host}", + 'value': f"Помилка: {temp_data['status']}", + 'cpu': 'Н/Д', + 'status': 'Error', + 'hostname': temp_data['hostname'] + }) + + return render_template('index.html', + temperatures=temperatures, + current_time=current_time) + +@app.route('/graphs') +def graphs(): + devices = [] + config = load_config(CONFIG_FILE) + + for device in config.get("devices", []): + host = device.get("host") + username = device.get("username") + password = device.get("password") + + if host and username and password: + # Отримуємо hostname + temp_data = get_remote_temperature(host, username, password) + history = db.get_history(host) + + devices.append({ + 'host': host, + 'hostname': temp_data['hostname'], + 'history': history + }) + + return render_template('graphs.html', devices=devices) + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=8080) diff --git a/collector.py b/collector.py new file mode 100644 index 0000000..f1eb28e --- /dev/null +++ b/collector.py @@ -0,0 +1,48 @@ +import time +import logging +from app import load_config, get_remote_temperature +from database import TemperatureDB + +# Налаштування логування +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(levelname)s - %(message)s' +) + +def collect_temperatures(): + db = TemperatureDB() + logging.info("Starting temperature collector") + + while True: + try: + config = load_config("config.json") + for device in config.get("devices", []): + host = device.get("host") + username = device.get("username") + password = device.get("password") + + if host and username and password: + logging.debug(f"Collecting temperature for {host}") + temp_data = get_remote_temperature(host, username, password) + + if temp_data['temp'] is not None: + logging.info(f"Got temperature for {host}: GPU={temp_data['temp']}°C, CPU={temp_data['cpu_temp']}°C") + try: + db.add_temperature( + host=host, + gpu_temp=temp_data['temp'], + cpu_temp=temp_data['cpu_temp'] + ) + logging.debug(f"Successfully saved temperature for {host}") + except Exception as e: + logging.error(f"Failed to save temperature for {host}: {e}") + else: + logging.warning(f"No temperature data received for {host}") + + except Exception as e: + logging.error(f"Error in collector main loop: {e}") + + time.sleep(10) + +if __name__ == "__main__": + collect_temperatures() diff --git a/config.json b/config.json new file mode 100644 index 0000000..f137f67 --- /dev/null +++ b/config.json @@ -0,0 +1,29 @@ +{ + "devices": [ + { + "host": "192.168.1.20", + "username": "watcher", + "password": "watcher" + }, + { + "host": "192.168.1.21", + "username": "watcher", + "password": "watcher" + }, + { + "host": "192.168.1.22", + "username": "watcher", + "password": "watcher" + }, + { + "host": "192.168.1.23", + "username": "watcher", + "password": "watcher" + }, + { + "host": "192.168.1.24", + "username": "watcher", + "password": "watcher" + } + ] +} \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..ceb9af1 --- /dev/null +++ b/database.py @@ -0,0 +1,108 @@ +import sqlite3 +import logging +from datetime import datetime, timedelta + +class TemperatureDB: + def __init__(self, db_file='temperature_history.db'): + self.db_file = db_file + self.init_db() + + def get_connection(self): + """Створення підключення до бази даних""" + try: + conn = sqlite3.connect(self.db_file) + conn.row_factory = sqlite3.Row + return conn + except Exception as e: + logging.error(f"Failed to connect to database: {e}") + raise + + def init_db(self): + """Ініціалізація бази даних""" + try: + conn = self.get_connection() + c = conn.cursor() + + c.execute(''' + CREATE TABLE IF NOT EXISTS temperatures ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host TEXT NOT NULL, + gpu_temp REAL, + cpu_temp REAL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + conn.commit() + logging.info("Database initialized successfully") + except Exception as e: + logging.error(f"Failed to initialize database: {e}") + raise + finally: + if 'conn' in locals(): + conn.close() + + def add_temperature(self, host, gpu_temp, cpu_temp): + """Додавання нового запису температури""" + try: + conn = self.get_connection() + c = conn.cursor() + + c.execute(''' + INSERT INTO temperatures (host, gpu_temp, cpu_temp, timestamp) + VALUES (?, ?, ?, datetime('now', 'localtime')) + ''', (host, gpu_temp, cpu_temp)) + + conn.commit() + logging.debug(f"Added temperature record for {host}: GPU={gpu_temp}°C, CPU={cpu_temp}°C") + except Exception as e: + logging.error(f"Failed to add temperature record: {e}") + raise + finally: + if 'conn' in locals(): + conn.close() + + def get_history(self, host, minutes=30): + """Отримання історії температур за останні N хвилин""" + try: + conn = self.get_connection() + c = conn.cursor() + + time_threshold = (datetime.now() - timedelta(minutes=minutes)).strftime('%Y-%m-%d %H:%M:%S') + + c.execute(''' + SELECT gpu_temp, cpu_temp, timestamp + FROM temperatures + WHERE host = ? AND timestamp > ? + ORDER BY timestamp ASC + ''', (host, time_threshold)) + + results = c.fetchall() + return { + 'gpu': [r['gpu_temp'] for r in results], + 'cpu': [r['cpu_temp'] for r in results], + 'timestamps': [r['timestamp'].split(' ')[1] for r in results] # Беремо тільки час + } + except Exception as e: + logging.error(f"Failed to retrieve history: {e}") + raise + finally: + if 'conn' in locals(): + conn.close() + + def cleanup_old_records(self, minutes=30): + """Видалення старих записів""" + try: + conn = self.get_connection() + c = conn.cursor() + + time_threshold = (datetime.now() - timedelta(minutes=minutes)).strftime('%Y-%m-%d %H:%M:%S') + + c.execute('DELETE FROM temperatures WHERE timestamp < ?', (time_threshold,)) + conn.commit() + logging.info(f"Old records older than {minutes} minutes have been cleaned up") + except Exception as e: + logging.error(f"Failed to cleanup old records: {e}") + raise + finally: + if 'conn' in locals(): + conn.close() diff --git a/templates/graphs.html b/templates/graphs.html new file mode 100644 index 0000000..dd35083 --- /dev/null +++ b/templates/graphs.html @@ -0,0 +1,90 @@ + + + + + + Графіки температур - Raspberry Pi + + + + + +
+
+

Графіки температур за останні 30 хвилин

+ ← Повернутися до моніторингу +
+ +
+ {% for device in devices %} +
+
+
+
{{ device.hostname }} ({{ device.host }})
+
+ +
+
+
+
+ {% endfor %} +
+
+ + + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..6f3411c --- /dev/null +++ b/templates/index.html @@ -0,0 +1,127 @@ + + + + + + Монітор температури Raspberry Pi + + + + + + +
+
+

Температура системи

+ Переглянути детальні графіки → +
+ +
+
+
+ Оновлено: {{ current_time }} + (автоматичне оновлення кожні 10 секунд) +
+ +
+ + + + + + + + + + + + + + {% for temp in temperatures %} + + + + + + + + + + {% endfor %} + +
IP-адресаHostnameТемпература GPUТемпература CPUГрафік GPUГрафік CPUСтатус
{{ temp.name }}{{ temp.hostname }}{% if temp.value is string %}{{ temp.value }}{% else %}{{ temp.value }}°C{% endif %}{% if temp.cpu is string %}{{ temp.cpu }}{% else %}{{ temp.cpu }}°C{% endif %} + {{ temp.status }} +
+
+
+
+
+ + + + +