init commit

This commit is contained in:
2025-10-26 17:24:37 +02:00
commit 9e94e93f12
9 changed files with 605 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
temperature_history.db

55
README.md Normal file
View File

@@ -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 шаблон
```

Binary file not shown.

147
app.py Normal file
View File

@@ -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)

48
collector.py Normal file
View File

@@ -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()

29
config.json Normal file
View File

@@ -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"
}
]
}

108
database.py Normal file
View File

@@ -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()

90
templates/graphs.html Normal file
View File

@@ -0,0 +1,90 @@
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Графіки температур - Raspberry Pi</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.chart-container {
height: 400px;
margin-bottom: 30px;
}
</style>
</head>
<body class="bg-light">
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">Графіки температур за останні 30 хвилин</h2>
<a href="/" class="btn btn-outline-primary">← Повернутися до моніторингу</a>
</div>
<div class="row">
{% for device in devices %}
<div class="col-12 mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ device.hostname }} ({{ device.host }})</h5>
<div class="chart-container">
<canvas id="chart-{{ loop.index }}"></canvas>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function createChart(elementId, data, hostname) {
const ctx = document.getElementById(elementId).getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: data.timestamps,
datasets: [{
label: 'GPU Temperature',
data: data.gpu,
borderColor: '#FF6B6B',
tension: 0.1
}, {
label: 'CPU Temperature',
data: data.cpu,
borderColor: '#4ECDC4',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Temperature (°C)'
}
}
},
plugins: {
title: {
display: true,
text: hostname
}
}
}
});
}
{% for device in devices %}
createChart(
'chart-{{ loop.index }}',
{{ device.history | tojson }},
'{{ device.hostname }}'
);
{% endfor %}
</script>
</body>
</html>

127
templates/index.html Normal file
View File

@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Монітор температури Raspberry Pi</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<meta http-equiv="refresh" content="10">
<style>
.sparkline {
width: 100px;
height: 30px;
display: inline-block;
}
.status-ok {
color: #198754;
}
.status-error {
color: #dc3545;
}
</style>
</head>
<body class="bg-light">
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">Температура системи</h2>
<a href="/graphs" class="btn btn-primary">Переглянути детальні графіки →</a>
</div>
<div class="card mb-4">
<div class="card-body">
<div class="text-muted small mb-3 text-end">
Оновлено: {{ current_time }}
(автоматичне оновлення кожні 10 секунд)
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>IP-адреса</th>
<th>Hostname</th>
<th>Температура GPU</th>
<th>Температура CPU</th>
<th>Графік GPU</th>
<th>Графік CPU</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
{% for temp in temperatures %}
<tr>
<td>{{ temp.name }}</td>
<td>{{ temp.hostname }}</td>
<td>{% if temp.value is string %}{{ temp.value }}{% else %}{{ temp.value }}°C{% endif %}</td>
<td>{% if temp.cpu is string %}{{ temp.cpu }}{% else %}{{ temp.cpu }}°C{% endif %}</td>
<td><canvas class="sparkline" id="gpu-{{ loop.index }}"></canvas></td>
<td><canvas class="sparkline" id="cpu-{{ loop.index }}"></canvas></td>
<td class="{% if temp.status == 'OK' %}status-ok{% else %}status-error{% endif %}">
{{ temp.status }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function createSparkline(elementId, data, labels, color) {
const ctx = document.getElementById(elementId).getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
data: data,
borderColor: color,
borderWidth: 1,
fill: false,
pointRadius: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
display: false
},
y: {
display: false
}
},
animation: false
}
});
}
{% for temp in temperatures %}
{% if temp.history is defined %}
createSparkline(
'gpu-{{ loop.index }}',
{{ temp.history.gpu | tojson }},
{{ temp.history.timestamps | tojson }},
'#FF6B6B'
);
createSparkline(
'cpu-{{ loop.index }}',
{{ temp.history.cpu | tojson }},
{{ temp.history.timestamps | tojson }},
'#4ECDC4'
);
{% endif %}
{% endfor %}
</script>
</body>
</html>