init commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
temperature_history.db
|
||||
55
README.md
Normal file
55
README.md
Normal 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 шаблон
|
||||
```
|
||||
BIN
__pycache__/database.cpython-314.pyc
Normal file
BIN
__pycache__/database.cpython-314.pyc
Normal file
Binary file not shown.
147
app.py
Normal file
147
app.py
Normal 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
48
collector.py
Normal 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
29
config.json
Normal 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
108
database.py
Normal 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
90
templates/graphs.html
Normal 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
127
templates/index.html
Normal 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>
|
||||
Reference in New Issue
Block a user