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