Finalisation dashboard EcoCharge dynamique
This commit is contained in:
17
.gitignore
vendored
17
.gitignore
vendored
@@ -1,19 +1,20 @@
|
|||||||
# Python
|
# Environnement Python
|
||||||
|
venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
|
||||||
# Virtual env
|
# Fichiers temporaires
|
||||||
.venv/
|
.pytest_cache/
|
||||||
|
ecocharge_code_complet.txt
|
||||||
|
|
||||||
# VS Code
|
# VS Code / système
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# Docker
|
# Variables d'environnement
|
||||||
*.dbvenv/
|
.env
|
||||||
144
app.py
144
app.py
@@ -30,12 +30,39 @@ def get_connection():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_float(value):
|
||||||
|
"""Convertit une valeur en float de façon sûre.
|
||||||
|
Accepte la virgule ou le point. Retourne None si impossible."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(str(value).replace(",", ".").strip())
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_number(value):
|
||||||
|
"""Normalise une saisie utilisateur (virgule -> point)."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return str(value).replace(",", ".").strip()
|
||||||
|
|
||||||
|
|
||||||
def get_setting_value(cur, key, default_value):
|
def get_setting_value(cur, key, default_value):
|
||||||
cur.execute("SELECT value FROM settings WHERE key = %s", (key,))
|
cur.execute("SELECT value FROM settings WHERE key = %s", (key,))
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
return row[0] if row else default_value
|
return row[0] if row else default_value
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_setting(cur, key, value):
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO settings (key, value)
|
||||||
|
VALUES (%s, %s)
|
||||||
|
ON CONFLICT (key)
|
||||||
|
DO UPDATE SET value = EXCLUDED.value
|
||||||
|
""", (key, value))
|
||||||
|
|
||||||
|
|
||||||
def admin_required(view_func):
|
def admin_required(view_func):
|
||||||
@wraps(view_func)
|
@wraps(view_func)
|
||||||
def wrapped_view(*args, **kwargs):
|
def wrapped_view(*args, **kwargs):
|
||||||
@@ -72,6 +99,7 @@ def receive_data():
|
|||||||
if not data:
|
if not data:
|
||||||
return jsonify({"error": "Aucune donnée JSON reçue"}), 400
|
return jsonify({"error": "Aucune donnée JSON reçue"}), 400
|
||||||
|
|
||||||
|
# --- Noms JSON envoyés par l'ESP32 : INCHANGÉS ---
|
||||||
temperature_ext = data.get("temperature_ext")
|
temperature_ext = data.get("temperature_ext")
|
||||||
humidity_ext = data.get("humidity_ext")
|
humidity_ext = data.get("humidity_ext")
|
||||||
system_status_msg = data.get("system_status")
|
system_status_msg = data.get("system_status")
|
||||||
@@ -89,18 +117,29 @@ def receive_data():
|
|||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
max_battery_temperature = float(get_setting_value(cur, "max_battery_temperature", "60"))
|
# --- Seuils visuels (cohérents avec les VRAIS capteurs) ---
|
||||||
min_battery_voltage = float(get_setting_value(cur, "min_battery_voltage", "11"))
|
# Nouvelles clés pour ignorer d'anciennes valeurs incohérentes restées en base.
|
||||||
min_solar_power = float(get_setting_value(cur, "min_solar_power", "5"))
|
max_temperature = safe_float(get_setting_value(cur, "max_temp", "60")) or 60.0
|
||||||
|
min_voltage = safe_float(get_setting_value(cur, "min_voltage", "0.2")) or 0.2
|
||||||
|
min_power = safe_float(get_setting_value(cur, "min_power", "0.1")) or 0.1
|
||||||
|
|
||||||
battery_alert = "none"
|
# --- Alerte basée UNIQUEMENT sur les mesures réelles ---
|
||||||
|
# - température DHT11 (temperature_ext)
|
||||||
|
# - tension mesurée INA219 (voltage_pv)
|
||||||
|
# - puissance calculée (power_pv)
|
||||||
|
# Plus aucune référence à une "batterie" (pas de BMS sur ce montage).
|
||||||
|
temp_value = safe_float(temperature_ext)
|
||||||
|
voltage_value = safe_float(voltage_pv)
|
||||||
|
power_value = safe_float(power_pv)
|
||||||
|
|
||||||
if battery_temp is not None and float(battery_temp) > max_battery_temperature:
|
battery_alert = "none" # nom de colonne conservé pour ne rien casser en base
|
||||||
battery_alert = "Température batterie trop élevée"
|
|
||||||
elif voltage_battery is not None and float(voltage_battery) < min_battery_voltage:
|
if temp_value is not None and temp_value > max_temperature:
|
||||||
battery_alert = "Tension batterie trop faible"
|
battery_alert = "Température trop élevée"
|
||||||
elif power_pv is not None and float(power_pv) < min_solar_power:
|
elif voltage_value is not None and voltage_value < min_voltage:
|
||||||
battery_alert = "Puissance solaire insuffisante"
|
battery_alert = "Tension mesurée trop faible"
|
||||||
|
elif power_value is not None and power_value < min_power:
|
||||||
|
battery_alert = "Puissance calculée insuffisante"
|
||||||
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO telemetry (
|
INSERT INTO telemetry (
|
||||||
@@ -147,10 +186,13 @@ def latest_data():
|
|||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# data_age_seconds = ancienneté de la dernière mesure, calculée côté serveur.
|
||||||
|
# Permet au dashboard de savoir si l'ESP32 envoie encore des données récentes.
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
created_at,
|
created_at,
|
||||||
|
EXTRACT(EPOCH FROM (NOW() - created_at)) AS age_seconds,
|
||||||
temperature_ext,
|
temperature_ext,
|
||||||
humidity_ext,
|
humidity_ext,
|
||||||
system_status_msg,
|
system_status_msg,
|
||||||
@@ -176,30 +218,33 @@ def latest_data():
|
|||||||
if not row:
|
if not row:
|
||||||
return jsonify({"message": "Aucune donnée disponible"}), 404
|
return jsonify({"message": "Aucune donnée disponible"}), 404
|
||||||
|
|
||||||
|
age = row[2]
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"id": row[0],
|
"id": row[0],
|
||||||
"created_at": str(row[1]),
|
"created_at": str(row[1]),
|
||||||
"temperature_ext": row[2],
|
"data_age_seconds": float(age) if age is not None else None,
|
||||||
"humidity_ext": row[3],
|
"temperature_ext": row[3],
|
||||||
"system_status": row[4],
|
"humidity_ext": row[4],
|
||||||
"voltage_pv": row[5],
|
"system_status": row[5],
|
||||||
"current_pv": row[6],
|
"voltage_pv": row[6],
|
||||||
"power_pv": row[7],
|
"current_pv": row[7],
|
||||||
"luminosity": row[8],
|
"power_pv": row[8],
|
||||||
"voltage_battery": row[9],
|
"luminosity": row[9],
|
||||||
"current_battery": row[10],
|
"voltage_battery": row[10],
|
||||||
"battery_temp": row[11],
|
"current_battery": row[11],
|
||||||
"battery_level": row[12],
|
"battery_temp": row[12],
|
||||||
"battery_alert": row[13]
|
"battery_level": row[13],
|
||||||
|
"battery_alert": row[14]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/history", methods=["GET"])
|
@app.route("/api/history", methods=["GET"])
|
||||||
def history_data():
|
def history_data():
|
||||||
limit = request.args.get("limit", default=10, type=int)
|
limit = request.args.get("limit", default=15, type=int)
|
||||||
|
|
||||||
if limit is None or limit <= 0:
|
if limit is None or limit <= 0:
|
||||||
limit = 10
|
limit = 15
|
||||||
|
|
||||||
if limit > 50:
|
if limit > 50:
|
||||||
limit = 50
|
limit = 50
|
||||||
@@ -207,6 +252,7 @@ def history_data():
|
|||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# On renvoie maintenant current_pv (courant mesuré) pour le graphique électrique.
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
@@ -214,7 +260,7 @@ def history_data():
|
|||||||
temperature_ext,
|
temperature_ext,
|
||||||
humidity_ext,
|
humidity_ext,
|
||||||
power_pv,
|
power_pv,
|
||||||
battery_level
|
current_pv
|
||||||
FROM telemetry
|
FROM telemetry
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT %s
|
LIMIT %s
|
||||||
@@ -233,7 +279,7 @@ def history_data():
|
|||||||
"temperature_ext": row[2],
|
"temperature_ext": row[2],
|
||||||
"humidity_ext": row[3],
|
"humidity_ext": row[3],
|
||||||
"power_pv": row[4],
|
"power_pv": row[4],
|
||||||
"battery_level": row[5]
|
"current_pv": row[5]
|
||||||
})
|
})
|
||||||
|
|
||||||
return jsonify(history)
|
return jsonify(history)
|
||||||
@@ -286,43 +332,35 @@ def admin_dashboard():
|
|||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
min_battery = request.form.get("min_battery")
|
# Normalisation : on accepte la virgule ET le point (ex : 0,3 -> 0.3)
|
||||||
max_temp = request.form.get("max_temp")
|
min_voltage = normalize_number(request.form.get("min_battery"))
|
||||||
min_power = request.form.get("min_power")
|
max_temp = normalize_number(request.form.get("max_temp"))
|
||||||
|
min_power = normalize_number(request.form.get("min_power"))
|
||||||
|
|
||||||
cur.execute("""
|
upsert_setting(cur, "min_voltage", min_voltage)
|
||||||
INSERT INTO settings (key, value)
|
upsert_setting(cur, "max_temp", max_temp)
|
||||||
VALUES ('min_battery_voltage', %s)
|
upsert_setting(cur, "min_power", min_power)
|
||||||
ON CONFLICT (key)
|
|
||||||
DO UPDATE SET value = EXCLUDED.value
|
|
||||||
""", (min_battery,))
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
INSERT INTO settings (key, value)
|
|
||||||
VALUES ('max_battery_temperature', %s)
|
|
||||||
ON CONFLICT (key)
|
|
||||||
DO UPDATE SET value = EXCLUDED.value
|
|
||||||
""", (max_temp,))
|
|
||||||
|
|
||||||
cur.execute("""
|
|
||||||
INSERT INTO settings (key, value)
|
|
||||||
VALUES ('min_solar_power', %s)
|
|
||||||
ON CONFLICT (key)
|
|
||||||
DO UPDATE SET value = EXCLUDED.value
|
|
||||||
""", (min_power,))
|
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Post/Redirect/Get : évite le ré-envoi du formulaire si on actualise,
|
||||||
|
# et affiche proprement le message de succès.
|
||||||
|
return redirect(url_for("admin_dashboard", saved=1))
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
"min_battery": get_setting_value(cur, "min_battery_voltage", "11"),
|
"min_battery": get_setting_value(cur, "min_voltage", "0.2"),
|
||||||
"max_temp": get_setting_value(cur, "max_battery_temperature", "60"),
|
"max_temp": get_setting_value(cur, "max_temp", "60"),
|
||||||
"min_power": get_setting_value(cur, "min_solar_power", "5")
|
"min_power": get_setting_value(cur, "min_power", "0.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return render_template("admin_dashboard.html", settings=settings)
|
saved = request.args.get("saved") == "1"
|
||||||
|
|
||||||
|
return render_template("admin_dashboard.html", settings=settings, saved=saved)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/admin/logout", methods=["GET", "POST"])
|
@app.route("/admin/logout", methods=["GET", "POST"])
|
||||||
|
|||||||
350
static/script.js
350
static/script.js
@@ -1,7 +1,17 @@
|
|||||||
let isOnline = true;
|
// =========================================================
|
||||||
let lastHeartbeat = Date.now();
|
// EcoCharge — Dashboard dynamique
|
||||||
const TIMEOUT_MS = 5000;
|
// - Interroge /api/latest toutes les 4 s (mise à jour temps réel)
|
||||||
|
// - Statut EN LIGNE / HORS LIGNE basé sur l'ancienneté des données
|
||||||
|
// - Graphiques lisibles à double axe Y
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
// L'ESP32 est considéré HORS LIGNE si la dernière mesure date de plus de 60 s.
|
||||||
|
const STALE_SECONDS = 60;
|
||||||
|
const POLL_LATEST_MS = 4000;
|
||||||
|
const POLL_HISTORY_MS = 8000;
|
||||||
|
const HISTORY_LIMIT = 15;
|
||||||
|
|
||||||
|
// --- Éléments du DOM ---
|
||||||
const statusDot = document.getElementById('system-status');
|
const statusDot = document.getElementById('system-status');
|
||||||
const statusText = document.getElementById('status-text');
|
const statusText = document.getElementById('status-text');
|
||||||
|
|
||||||
@@ -12,12 +22,13 @@ const apiTempEl = document.getElementById('api-temp');
|
|||||||
const apiDescEl = document.getElementById('api-desc');
|
const apiDescEl = document.getElementById('api-desc');
|
||||||
const apiIconEl = document.getElementById('api-icon');
|
const apiIconEl = document.getElementById('api-icon');
|
||||||
|
|
||||||
const batValEl = document.getElementById('bat-val');
|
|
||||||
const batCircleEl = document.getElementById('bat-circle');
|
|
||||||
const batAlertBadge = document.getElementById('bat-alert-badge');
|
const batAlertBadge = document.getElementById('bat-alert-badge');
|
||||||
const batAlertIcon = document.getElementById('bat-alert-icon');
|
const batAlertIcon = document.getElementById('bat-alert-icon');
|
||||||
const batAlertText = document.getElementById('bat-alert');
|
const batAlertText = document.getElementById('bat-alert');
|
||||||
|
|
||||||
|
const dotEsp = document.getElementById('dot-esp');
|
||||||
|
const badgeEsp = document.getElementById('badge-esp');
|
||||||
|
|
||||||
const MAX_POWER = 100;
|
const MAX_POWER = 100;
|
||||||
const gaugeCircle = document.querySelector('.gauge-fill circle');
|
const gaugeCircle = document.querySelector('.gauge-fill circle');
|
||||||
const halfCircumference = 283 / 2;
|
const halfCircumference = 283 / 2;
|
||||||
@@ -25,6 +36,9 @@ const halfCircumference = 283 / 2;
|
|||||||
let envChart = null;
|
let envChart = null;
|
||||||
let energyChart = null;
|
let energyChart = null;
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Météo externe (API web open-meteo) — purement informatif
|
||||||
|
// =========================================================
|
||||||
function getWeatherDetails(code) {
|
function getWeatherDetails(code) {
|
||||||
if (code === 0) return { desc: "Dégagé", icon: "bi-brightness-high", color: "text-warning" };
|
if (code === 0) return { desc: "Dégagé", icon: "bi-brightness-high", color: "text-warning" };
|
||||||
if (code >= 1 && code <= 3) return { desc: "Nuageux", icon: "bi-cloud", color: "text-light" };
|
if (code >= 1 && code <= 3) return { desc: "Nuageux", icon: "bi-cloud", color: "text-light" };
|
||||||
@@ -56,56 +70,60 @@ async function fetchRealWeather() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBattery(percent, alertTextFromApi = null) {
|
// =========================================================
|
||||||
batValEl.innerText = percent + "%";
|
// Statut EN LIGNE / HORS LIGNE (cohérent partout)
|
||||||
let color = "#34d399";
|
// =========================================================
|
||||||
let shadowColor = "rgba(52, 211, 153, 0.4)";
|
function updateConnectionStatus(online) {
|
||||||
|
if (online) {
|
||||||
|
statusDot.className = "status-dot rounded-circle pulse-online";
|
||||||
|
statusText.innerText = "EN LIGNE";
|
||||||
|
statusText.style.color = "#e2e8f0";
|
||||||
|
|
||||||
if (alertTextFromApi && alertTextFromApi !== "none") {
|
if (dotEsp) dotEsp.className = "sys-dot bg-success";
|
||||||
color = "#ef4444";
|
if (badgeEsp) {
|
||||||
shadowColor = "rgba(239, 68, 68, 0.6)";
|
badgeEsp.className = "ms-auto sys-badge sys-badge-ok";
|
||||||
batAlertText.innerText = alertTextFromApi;
|
badgeEsp.innerText = "Connecté";
|
||||||
batAlertIcon.className = "bi bi-exclamation-triangle-fill text-danger";
|
}
|
||||||
batAlertBadge.className = "badge bg-dark border border-danger text-secondary w-100 py-2";
|
} else {
|
||||||
} else if (percent <= 20) {
|
statusDot.className = "status-dot rounded-circle pulse-offline";
|
||||||
color = "#ef4444";
|
statusText.innerText = "HORS LIGNE";
|
||||||
shadowColor = "rgba(239, 68, 68, 0.6)";
|
statusText.style.color = "#ef4444";
|
||||||
batAlertText.innerText = "Niveau critique";
|
|
||||||
batAlertIcon.className = "bi bi-exclamation-triangle-fill text-danger";
|
if (dotEsp) dotEsp.className = "sys-dot bg-danger";
|
||||||
batAlertBadge.className = "badge bg-dark border border-danger text-secondary w-100 py-2";
|
if (badgeEsp) {
|
||||||
} else if (percent <= 50) {
|
badgeEsp.className = "ms-auto sys-badge sys-badge-err";
|
||||||
color = "#fbbf24";
|
badgeEsp.innerText = "Hors ligne";
|
||||||
shadowColor = "rgba(251, 191, 36, 0.5)";
|
}
|
||||||
batAlertText.innerText = "Niveau moyen";
|
}
|
||||||
batAlertIcon.className = "bi bi-exclamation-circle text-warning";
|
}
|
||||||
|
|
||||||
|
// Badge d'état (bas du bloc "État du Système")
|
||||||
|
function updateAlert(alertText) {
|
||||||
|
if (alertText && alertText !== "none") {
|
||||||
|
batAlertText.innerText = alertText;
|
||||||
|
batAlertIcon.className = "bi bi-exclamation-triangle-fill text-warning";
|
||||||
batAlertBadge.className = "badge bg-dark border border-warning text-secondary w-100 py-2";
|
batAlertBadge.className = "badge bg-dark border border-warning text-secondary w-100 py-2";
|
||||||
} else {
|
} else {
|
||||||
batAlertText.innerText = "Système Normal";
|
batAlertText.innerText = "Système Normal";
|
||||||
batAlertIcon.className = "bi bi-check-circle text-success";
|
batAlertIcon.className = "bi bi-check-circle text-success";
|
||||||
batAlertBadge.className = "badge bg-dark border border-secondary text-secondary w-100 py-2";
|
batAlertBadge.className = "badge bg-dark border border-secondary text-secondary w-100 py-2";
|
||||||
}
|
}
|
||||||
|
|
||||||
const angle = percent * 3.6;
|
|
||||||
batCircleEl.style.background = `conic-gradient(${color} ${angle}deg, #0f172a 0deg)`;
|
|
||||||
batCircleEl.style.boxShadow = `0 0 15px ${shadowColor}, inset 0 0 15px ${shadowColor}`;
|
|
||||||
batValEl.style.textShadow = `0 0 10px ${shadowColor}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSolar(voltage, current, power, lux) {
|
// =========================================================
|
||||||
|
// Mesures électriques (INA219) — jauge + cartes
|
||||||
|
// =========================================================
|
||||||
|
function updateElectrical(voltage, current, power) {
|
||||||
const v = Number(voltage).toFixed(1);
|
const v = Number(voltage).toFixed(1);
|
||||||
const c = Number(current).toFixed(2);
|
const c = Number(current).toFixed(2);
|
||||||
const p = Number(power).toFixed(1);
|
const p = Number(power).toFixed(1);
|
||||||
const l = Math.floor(Number(lux));
|
|
||||||
|
|
||||||
const efficiency = (p > 0) ? ((p / MAX_POWER) * 100).toFixed(1) : "0.0";
|
|
||||||
|
|
||||||
document.getElementById('powerValue').textContent = p;
|
document.getElementById('powerValue').textContent = p;
|
||||||
document.getElementById('voltage').innerHTML = v + ' <span class="stat-unit">V</span>';
|
document.getElementById('voltage').innerHTML = v + ' <span class="stat-unit">V</span>';
|
||||||
document.getElementById('current').innerHTML = c + ' <span class="stat-unit">A</span>';
|
document.getElementById('current').innerHTML = c + ' <span class="stat-unit">A</span>';
|
||||||
document.getElementById('lux').innerHTML = l.toLocaleString() + ' <span class="stat-unit">Lux</span>';
|
document.getElementById('efficiency').innerHTML = p + ' <span class="stat-unit">W</span>';
|
||||||
document.getElementById('efficiency').innerHTML = efficiency + ' <span class="stat-unit">%</span>';
|
|
||||||
|
|
||||||
const ratio = Math.min(p / MAX_POWER, 1);
|
const ratio = Math.min(Number(power) / MAX_POWER, 1);
|
||||||
const offset = halfCircumference - (ratio * halfCircumference);
|
const offset = halfCircumference - (ratio * halfCircumference);
|
||||||
|
|
||||||
if (gaugeCircle) {
|
if (gaugeCircle) {
|
||||||
@@ -113,6 +131,10 @@ function updateSolar(voltage, current, power, lux) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Graphique 1 : Historique environnement (DHT11)
|
||||||
|
// Température (axe gauche) + Humidité (axe droit)
|
||||||
|
// =========================================================
|
||||||
function createEnvChart() {
|
function createEnvChart() {
|
||||||
const ctx = document.getElementById('envChart');
|
const ctx = document.getElementById('envChart');
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
@@ -126,60 +148,66 @@ function createEnvChart() {
|
|||||||
label: 'Température (°C)',
|
label: 'Température (°C)',
|
||||||
data: [],
|
data: [],
|
||||||
borderColor: '#38bdf8',
|
borderColor: '#38bdf8',
|
||||||
backgroundColor: 'rgba(56, 189, 248, 0.2)',
|
backgroundColor: 'rgba(56, 189, 248, 0.08)',
|
||||||
pointBackgroundColor: '#38bdf8',
|
pointBackgroundColor: '#38bdf8',
|
||||||
pointBorderColor: '#38bdf8',
|
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
tension: 0.3
|
pointRadius: 3,
|
||||||
|
tension: 0.35,
|
||||||
|
yAxisID: 'y'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Humidité (%)',
|
label: 'Humidité (%)',
|
||||||
data: [],
|
data: [],
|
||||||
borderColor: '#ff5c8a',
|
borderColor: '#ff5c8a',
|
||||||
backgroundColor: 'rgba(255, 92, 138, 0.2)',
|
backgroundColor: 'rgba(255, 92, 138, 0.08)',
|
||||||
pointBackgroundColor: '#ff5c8a',
|
pointBackgroundColor: '#ff5c8a',
|
||||||
pointBorderColor: '#ff5c8a',
|
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
tension: 0.3
|
pointRadius: 3,
|
||||||
|
tension: 0.35,
|
||||||
|
yAxisID: 'y1'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
interaction: {
|
animation: false,
|
||||||
mode: 'index',
|
interaction: { mode: 'index', intersect: false },
|
||||||
intersect: false
|
|
||||||
},
|
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: { labels: { color: '#e2e8f0' } }
|
||||||
labels: {
|
|
||||||
color: '#e2e8f0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
ticks: {
|
ticks: { color: '#94a3b8' },
|
||||||
color: '#94a3b8'
|
grid: { color: 'rgba(148, 163, 184, 0.1)' }
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
color: 'rgba(148, 163, 184, 0.1)'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
ticks: {
|
position: 'left',
|
||||||
color: '#94a3b8'
|
suggestedMin: 0,
|
||||||
},
|
suggestedMax: 50,
|
||||||
grid: {
|
title: { display: false },
|
||||||
color: 'rgba(148, 163, 184, 0.1)'
|
ticks: { color: '#94a3b8' },
|
||||||
}
|
grid: { color: 'rgba(148, 163, 184, 0.1)' }
|
||||||
|
},
|
||||||
|
y1: {
|
||||||
|
position: 'right',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
title: { display: false },
|
||||||
|
ticks: { color: '#94a3b8' },
|
||||||
|
grid: { drawOnChartArea: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// Graphique 2 : Historique mesures électriques (INA219)
|
||||||
|
// Puissance calculée (axe gauche, W) + Courant mesuré (axe droit, A)
|
||||||
|
// -> deux axes Y = courant lisible même si la puissance varie
|
||||||
|
// -> beginAtZero = échelle stable (la courbe ne "saute" plus)
|
||||||
|
// =========================================================
|
||||||
function createEnergyChart() {
|
function createEnergyChart() {
|
||||||
const ctx = document.getElementById('energyChart');
|
const ctx = document.getElementById('energyChart');
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
@@ -190,76 +218,94 @@ function createEnergyChart() {
|
|||||||
labels: [],
|
labels: [],
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Batterie (%)',
|
label: 'Puissance calculée (W)',
|
||||||
data: [],
|
|
||||||
borderColor: '#34d399',
|
|
||||||
backgroundColor: 'rgba(52, 211, 153, 0.2)',
|
|
||||||
pointBackgroundColor: '#34d399',
|
|
||||||
pointBorderColor: '#34d399',
|
|
||||||
borderWidth: 2,
|
|
||||||
tension: 0.3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Puissance solaire (W)',
|
|
||||||
data: [],
|
data: [],
|
||||||
borderColor: '#facc15',
|
borderColor: '#facc15',
|
||||||
backgroundColor: 'rgba(250, 204, 21, 0.2)',
|
backgroundColor: 'rgba(250, 204, 21, 0.08)',
|
||||||
pointBackgroundColor: '#facc15',
|
pointBackgroundColor: '#facc15',
|
||||||
pointBorderColor: '#facc15',
|
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
tension: 0.3
|
pointRadius: 2,
|
||||||
|
tension: 0.35,
|
||||||
|
yAxisID: 'y'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Courant mesuré (A)',
|
||||||
|
data: [],
|
||||||
|
borderColor: '#34d399',
|
||||||
|
backgroundColor: 'rgba(52, 211, 153, 0.08)',
|
||||||
|
pointBackgroundColor: '#34d399',
|
||||||
|
borderWidth: 2.5,
|
||||||
|
pointRadius: 3,
|
||||||
|
tension: 0.35,
|
||||||
|
yAxisID: 'y1'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
interaction: {
|
animation: false,
|
||||||
mode: 'index',
|
interaction: { mode: 'index', intersect: false },
|
||||||
intersect: false
|
|
||||||
},
|
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: { labels: { color: '#e2e8f0' } }
|
||||||
labels: {
|
|
||||||
color: '#e2e8f0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
ticks: {
|
ticks: { color: '#94a3b8' },
|
||||||
color: '#94a3b8'
|
grid: { color: 'rgba(148, 163, 184, 0.1)' }
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
color: 'rgba(148, 163, 184, 0.1)'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
ticks: {
|
position: 'left',
|
||||||
color: '#94a3b8'
|
beginAtZero: true,
|
||||||
},
|
title: { display: false },
|
||||||
grid: {
|
ticks: { color: '#94a3b8' },
|
||||||
color: 'rgba(148, 163, 184, 0.1)'
|
grid: { color: 'rgba(148, 163, 184, 0.1)' }
|
||||||
}
|
},
|
||||||
|
y1: {
|
||||||
|
position: 'right',
|
||||||
|
beginAtZero: true,
|
||||||
|
title: { display: false },
|
||||||
|
ticks: { color: '#94a3b8' },
|
||||||
|
grid: { drawOnChartArea: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchHistoryData() {
|
// =========================================================
|
||||||
|
// Récupération de l'historique (graphiques)
|
||||||
|
// =========================================================
|
||||||
|
async function fetchHistory() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/history?limit=10');
|
const response = await fetch('/api/history?limit=' + HISTORY_LIMIT);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Impossible de récupérer l'historique");
|
throw new Error("Impossible de récupérer l'historique");
|
||||||
}
|
}
|
||||||
|
|
||||||
const history = await response.json();
|
const history = await response.json();
|
||||||
|
|
||||||
const labels = history.map(item => {
|
// --- Heure de l'axe X : on affiche l'heure LOCALE du navigateur ---
|
||||||
const date = new Date(item.created_at);
|
// Le timestamp serveur (Docker) peut être en mauvais fuseau (ex. 01:11 au lieu
|
||||||
return date.toLocaleTimeString('fr-FR', {
|
// de 16:08). On ancre donc la mesure la plus récente sur l'heure du navigateur,
|
||||||
|
// et on garde l'écart réel entre les mesures (calculé à partir des timestamps,
|
||||||
|
// ce qui est indépendant du fuseau horaire).
|
||||||
|
const browserNow = Date.now();
|
||||||
|
const times = history.map(item => {
|
||||||
|
const t = new Date(item.created_at).getTime();
|
||||||
|
return Number.isNaN(t) ? null : t;
|
||||||
|
});
|
||||||
|
const lastValid = [...times].reverse().find(t => t !== null);
|
||||||
|
|
||||||
|
const labels = times.map(t => {
|
||||||
|
let d;
|
||||||
|
if (t !== null && lastValid !== undefined) {
|
||||||
|
// décalage réel par rapport à la dernière mesure, reporté sur l'heure du navigateur
|
||||||
|
d = new Date(browserNow - (lastValid - t));
|
||||||
|
} else {
|
||||||
|
d = new Date(browserNow);
|
||||||
|
}
|
||||||
|
return d.toLocaleTimeString('fr-FR', {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit'
|
second: '2-digit'
|
||||||
@@ -270,86 +316,78 @@ async function fetchHistoryData() {
|
|||||||
envChart.data.labels = labels;
|
envChart.data.labels = labels;
|
||||||
envChart.data.datasets[0].data = history.map(item => item.temperature_ext ?? null);
|
envChart.data.datasets[0].data = history.map(item => item.temperature_ext ?? null);
|
||||||
envChart.data.datasets[1].data = history.map(item => item.humidity_ext ?? null);
|
envChart.data.datasets[1].data = history.map(item => item.humidity_ext ?? null);
|
||||||
envChart.update();
|
envChart.update('none');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (energyChart) {
|
if (energyChart) {
|
||||||
energyChart.data.labels = labels;
|
energyChart.data.labels = labels;
|
||||||
energyChart.data.datasets[0].data = history.map(item => item.battery_level ?? null);
|
energyChart.data.datasets[0].data = history.map(item => item.power_pv ?? null);
|
||||||
energyChart.data.datasets[1].data = history.map(item => item.power_pv ?? null);
|
energyChart.data.datasets[1].data = history.map(item => item.current_pv ?? null);
|
||||||
energyChart.update();
|
energyChart.update('none');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur historique :", error);
|
console.error("Erreur historique :", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchDatabaseData() {
|
// =========================================================
|
||||||
if (!isOnline) return;
|
// Récupération de la dernière mesure (cartes + statut)
|
||||||
|
// =========================================================
|
||||||
|
async function fetchLatest() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/latest');
|
const response = await fetch('/api/latest');
|
||||||
|
|
||||||
|
// Pas de données en base -> HORS LIGNE
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Base de données vide ou serveur injoignable");
|
updateConnectionStatus(false);
|
||||||
|
updateAlert(null);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
tempEl.innerText = data.temperature_ext !== null ? Number(data.temperature_ext).toFixed(1) : "--";
|
// EN LIGNE seulement si la dernière mesure est récente
|
||||||
humEl.innerText = data.humidity_ext !== null ? Math.floor(data.humidity_ext) : "--";
|
const age = data.data_age_seconds;
|
||||||
|
const online = (age !== null && age !== undefined && age < STALE_SECONDS);
|
||||||
|
|
||||||
if (data.battery_level !== null) {
|
// Mise à jour des valeurs (DHT11 + INA219)
|
||||||
const batteryAlert = data.battery_alert === true || data.battery_alert === "true" || data.battery_alert === 1 || data.battery_alert === "1";
|
tempEl.innerText = (data.temperature_ext !== null && data.temperature_ext !== undefined)
|
||||||
updateBattery(Math.floor(data.battery_level), batteryAlert ? "Alerte batterie" : null);
|
? Number(data.temperature_ext).toFixed(1) : "--";
|
||||||
}
|
humEl.innerText = (data.humidity_ext !== null && data.humidity_ext !== undefined)
|
||||||
|
? Math.round(data.humidity_ext) : "--";
|
||||||
|
|
||||||
updateSolar(
|
updateElectrical(
|
||||||
data.voltage_pv || 0,
|
data.voltage_pv || 0,
|
||||||
data.current_pv || 0,
|
data.current_pv || 0,
|
||||||
data.power_pv || 0,
|
data.power_pv || 0
|
||||||
data.luminosity || 0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
lastHeartbeat = Date.now();
|
updateConnectionStatus(online);
|
||||||
|
// On n'affiche une alerte que si les données sont récentes (sinon non fiable)
|
||||||
|
updateAlert(online ? (data.battery_alert || null) : null);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("En attente de données capteurs...", error);
|
console.error("En attente de données capteurs...", error);
|
||||||
|
updateConnectionStatus(false);
|
||||||
|
updateAlert(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkSystemStatus() {
|
// =========================================================
|
||||||
if (Date.now() - lastHeartbeat > TIMEOUT_MS) {
|
// Démarrage
|
||||||
statusDot.className = "status-dot rounded-circle pulse-offline";
|
// =========================================================
|
||||||
statusText.innerText = "HORS LIGNE";
|
// État initial neutre : on n'affiche PAS "EN LIGNE" tant qu'on n'a rien confirmé.
|
||||||
statusText.style.color = "#ef4444";
|
statusText.innerText = "Vérification…";
|
||||||
} else {
|
statusText.style.color = "#94a3b8";
|
||||||
statusDot.className = "status-dot rounded-circle pulse-online";
|
statusDot.className = "status-dot rounded-circle";
|
||||||
statusText.innerText = "EN LIGNE";
|
|
||||||
statusText.style.color = "#e2e8f0";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('btn-test').addEventListener('click', function () {
|
|
||||||
isOnline = !isOnline;
|
|
||||||
if (isOnline) {
|
|
||||||
this.innerHTML = '<i class="bi bi-wifi-off"></i> Simuler Coupure';
|
|
||||||
this.classList.replace('btn-outline-success', 'btn-outline-danger');
|
|
||||||
lastHeartbeat = Date.now();
|
|
||||||
} else {
|
|
||||||
this.innerHTML = '<i class="bi bi-wifi"></i> Rétablir Connexion';
|
|
||||||
this.classList.replace('btn-outline-danger', 'btn-outline-success');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
createEnvChart();
|
createEnvChart();
|
||||||
createEnergyChart();
|
createEnergyChart();
|
||||||
fetchRealWeather();
|
|
||||||
fetchDatabaseData();
|
|
||||||
fetchHistoryData();
|
|
||||||
checkSystemStatus();
|
|
||||||
|
|
||||||
setInterval(fetchRealWeather, 900000);
|
fetchRealWeather();
|
||||||
setInterval(fetchDatabaseData, 10000);
|
fetchLatest();
|
||||||
setInterval(fetchHistoryData, 30000);
|
fetchHistory();
|
||||||
setInterval(checkSystemStatus, 10000);
|
|
||||||
|
setInterval(fetchRealWeather, 900000); // météo externe : toutes les 15 min
|
||||||
|
setInterval(fetchLatest, POLL_LATEST_MS); // cartes + statut : toutes les 4 s
|
||||||
|
setInterval(fetchHistory, POLL_HISTORY_MS); // graphiques : toutes les 8 s
|
||||||
@@ -17,6 +17,15 @@ body {
|
|||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Les 4 cartes principales gardent une hauteur cohérente (alignement propre) */
|
||||||
|
.row.g-4 > [class*="col-"] {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.g-4 > [class*="col-"] > .card {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.card-meteo {
|
.card-meteo {
|
||||||
border-top: 4px solid #38bdf8;
|
border-top: 4px solid #38bdf8;
|
||||||
}
|
}
|
||||||
@@ -38,6 +47,8 @@ body {
|
|||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
background-color: #64748b;
|
||||||
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pulse-online {
|
.pulse-online {
|
||||||
@@ -81,44 +92,55 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* =========================================
|
/* =========================================
|
||||||
WIDGET BATTERIE (Code de l'Étudiant 2)
|
BLOC ÉTAT DU SYSTÈME
|
||||||
========================================= */
|
========================================= */
|
||||||
.battery-circle {
|
.system-status-item {
|
||||||
width: 140px;
|
background: rgba(255, 255, 255, 0.03);
|
||||||
height: 140px;
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
border-radius: 50%;
|
border-radius: 10px;
|
||||||
display: flex;
|
padding: 10px 14px;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background: conic-gradient(#34d399 0deg, #0f172a 0deg);
|
|
||||||
box-shadow: 0 0 15px rgba(52, 211, 153, 0.3), inset 0 0 15px rgba(52, 211, 153, 0.2);
|
|
||||||
transition: background 0.5s ease, box-shadow 0.5s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.battery-inner {
|
.sys-dot {
|
||||||
width: 110px;
|
width: 10px;
|
||||||
height: 110px;
|
height: 10px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #1e293b;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: inline-block;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bat-percent {
|
.sys-label {
|
||||||
font-size: 32px;
|
font-size: 13px;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
color: #e0f2fe;
|
color: #e2e8f0;
|
||||||
text-shadow: 0 0 10px rgba(52, 211, 153, 0.5);
|
}
|
||||||
line-height: 1;
|
|
||||||
|
.sys-badge {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
color: #94a3b8;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sys-badge-ok {
|
||||||
|
background: rgba(52, 211, 153, 0.12);
|
||||||
|
color: #34d399;
|
||||||
|
border-color: rgba(52, 211, 153, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sys-badge-err {
|
||||||
|
background: rgba(239, 68, 68, 0.12);
|
||||||
|
color: #ef4444;
|
||||||
|
border-color: rgba(239, 68, 68, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================================
|
/* =========================================
|
||||||
WIDGET SOLAIRE (Code de l'Étudiant 1 adapté et corrigé)
|
WIDGET SOLAIRE (renommé Mesures Électriques)
|
||||||
========================================= */
|
========================================= */
|
||||||
|
|
||||||
/* CORRECTION ICI : La boîte est maintenant carrée et ne déborde plus */
|
|
||||||
.gauge-container {
|
.gauge-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 160px;
|
width: 160px;
|
||||||
@@ -155,7 +177,6 @@ body {
|
|||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CORRECTION ICI : Le texte est parfaitement centré au milieu du cercle */
|
|
||||||
.gauge-value {
|
.gauge-value {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
|
|||||||
@@ -185,6 +185,28 @@
|
|||||||
background-color: #27ae60;
|
background-color: #27ae60;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Bouton Retour au dashboard ── */
|
||||||
|
.retour {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 13px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #2c3e50;
|
||||||
|
color: var(--blanc);
|
||||||
|
background-color: transparent;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: border-color 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retour:hover {
|
||||||
|
border-color: var(--vert);
|
||||||
|
color: var(--vert);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Message de confirmation Flask/Jinja ── */
|
/* ── Message de confirmation Flask/Jinja ── */
|
||||||
.succes {
|
.succes {
|
||||||
background-color: rgba(46, 204, 113, 0.15);
|
background-color: rgba(46, 204, 113, 0.15);
|
||||||
@@ -219,22 +241,29 @@
|
|||||||
<!-- ── Carte principale ── -->
|
<!-- ── Carte principale ── -->
|
||||||
<div class="carte">
|
<div class="carte">
|
||||||
|
|
||||||
|
<!-- Message affiché après une sauvegarde réussie -->
|
||||||
|
{% if saved %}
|
||||||
|
<div class="succes">
|
||||||
|
✅ Paramètres sauvegardés
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<h2>Seuils d'alerte</h2>
|
<h2>Seuils d'alerte</h2>
|
||||||
<p>Modifiez les valeurs puis cliquez sur "Sauvegarder".</p>
|
<p>Modifiez les valeurs puis cliquez sur "Sauvegarder".</p>
|
||||||
|
|
||||||
<!-- Formulaire — action vers la route Flask /admin/dashboard -->
|
<!-- Formulaire — action vers la route Flask /admin/dashboard -->
|
||||||
<form method="POST" action="/admin/dashboard">
|
<form method="POST" action="/admin/dashboard">
|
||||||
|
|
||||||
<!-- ── Champ 1 : batterie minimum ── -->
|
<!-- ── Champ 1 : tension mesurée minimum ── -->
|
||||||
<div class="champ">
|
<div class="champ">
|
||||||
<div class="champ-entete">
|
<div class="champ-entete">
|
||||||
<label for="min_battery">🔋 Tension batterie minimum</label>
|
<label for="min_battery">🔋 Tension mesurée minimum</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="champ-info">Alerte si la tension de la batterie descend sous ce seuil.</p>
|
<p class="champ-info">Alerte si la tension mesurée (INA219) descend sous ce seuil.</p>
|
||||||
<br>
|
<br>
|
||||||
<div class="input-ligne">
|
<div class="input-ligne">
|
||||||
<input type="number" id="min_battery" name="min_battery" value="{{ settings.min_battery }}"
|
<input type="number" id="min_battery" name="min_battery" value="{{ settings.min_battery }}"
|
||||||
min="0" required>
|
min="0" step="any" inputmode="decimal" required>
|
||||||
<span class="unite">V</span>
|
<span class="unite">V</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -246,33 +275,36 @@
|
|||||||
<div class="champ-entete">
|
<div class="champ-entete">
|
||||||
<label for="max_temp">🌡️ Température maximum</label>
|
<label for="max_temp">🌡️ Température maximum</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="champ-info">Alerte si la température dépasse cette valeur.</p>
|
<p class="champ-info">Alerte si la température (DHT11) dépasse cette valeur.</p>
|
||||||
<br>
|
<br>
|
||||||
<div class="input-ligne">
|
<div class="input-ligne">
|
||||||
<input type="number" id="max_temp" name="max_temp" value="{{ settings.max_temp }}" min="0"
|
<input type="number" id="max_temp" name="max_temp" value="{{ settings.max_temp }}" min="0"
|
||||||
max="100" required>
|
max="100" step="any" inputmode="decimal" required>
|
||||||
<span class="unite">°C</span>
|
<span class="unite">°C</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="separateur">
|
<hr class="separateur">
|
||||||
|
|
||||||
<!-- ── Champ 3 : puissance minimum ── -->
|
<!-- ── Champ 3 : puissance calculée minimum ── -->
|
||||||
<div class="champ">
|
<div class="champ">
|
||||||
<div class="champ-entete">
|
<div class="champ-entete">
|
||||||
<label for="min_power">☀️ Puissance minimum</label>
|
<label for="min_power">⚡ Puissance calculée minimum</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="champ-info">Alerte si la production solaire descend sous ce seuil.</p>
|
<p class="champ-info">Alerte si la puissance calculée (P = U × I) descend sous ce seuil.</p>
|
||||||
<br>
|
<br>
|
||||||
<div class="input-ligne">
|
<div class="input-ligne">
|
||||||
<input type="number" id="min_power" name="min_power" value="{{ settings.min_power }}" min="0"
|
<input type="number" id="min_power" name="min_power" value="{{ settings.min_power }}" min="0"
|
||||||
required>
|
step="any" inputmode="decimal" required>
|
||||||
<span class="unite">W</span>
|
<span class="unite">W</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="bouton" type="submit">💾 Sauvegarder</button>
|
<button class="bouton" type="submit">💾 Sauvegarder</button>
|
||||||
|
|
||||||
|
<!-- Retour propre vers le dashboard principal (route "/") -->
|
||||||
|
<a class="retour" href="/">← Retour au dashboard</a>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Station Solaire | Dashboard PRO</title>
|
<title>EcoCharge | Dashboard</title>
|
||||||
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
class="d-flex align-items-center bg-dark bg-opacity-50 px-3 py-2 rounded-pill border border-secondary border-opacity-25">
|
class="d-flex align-items-center bg-dark bg-opacity-50 px-3 py-2 rounded-pill border border-secondary border-opacity-25">
|
||||||
<span id="status-text" class="me-3 fw-semibold small text-uppercase tracking-wide">Système en ligne</span>
|
<span id="status-text" class="me-3 fw-semibold small text-uppercase tracking-wide">Vérification…</span>
|
||||||
<div id="system-status" class="status-dot rounded-circle pulse-online"></div>
|
<div id="system-status" class="status-dot rounded-circle"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -30,8 +30,9 @@
|
|||||||
<div class="container-fluid px-4">
|
<div class="container-fluid px-4">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
|
|
||||||
|
<!-- ── BLOC 1 : Météo & Env. (DHT11) ── -->
|
||||||
<div class="col-12 col-md-6 col-lg-3">
|
<div class="col-12 col-md-6 col-lg-3">
|
||||||
<div class="card card-meteo rounded-3">
|
<div class="card card-meteo rounded-3 h-100">
|
||||||
<div class="card-body d-flex flex-column p-4">
|
<div class="card-body d-flex flex-column p-4">
|
||||||
<h5 class="card-title fw-bold mb-4" style="color: #38bdf8;">
|
<h5 class="card-title fw-bold mb-4" style="color: #38bdf8;">
|
||||||
<i class="bi bi-cloud-sun me-2"></i>Météo & Env.
|
<i class="bi bi-cloud-sun me-2"></i>Météo & Env.
|
||||||
@@ -65,19 +66,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-outline-danger btn-sm mt-auto w-100" id="btn-test">
|
|
||||||
<i class="bi bi-wifi-off"></i> Simuler Coupure
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── BLOC 2 : Mesures Électriques (INA219) ── -->
|
||||||
<div class="col-12 col-md-6 col-lg-3">
|
<div class="col-12 col-md-6 col-lg-3">
|
||||||
<div class="card card-solaire rounded-3">
|
<div class="card card-solaire rounded-3 h-100">
|
||||||
<div class="card-body p-4 d-flex flex-column">
|
<div class="card-body p-4 d-flex flex-column">
|
||||||
<h5 class="card-title fw-bold mb-4" style="color: #fbbf24;">
|
<h5 class="card-title fw-bold mb-4" style="color: #fbbf24;">
|
||||||
<i class="bi bi-sun me-2"></i>Production Solaire
|
<i class="bi bi-activity me-2"></i>Mesures Électriques
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
<div class="gauge-container mx-auto">
|
<div class="gauge-container mx-auto">
|
||||||
@@ -101,45 +99,76 @@
|
|||||||
|
|
||||||
<div class="stats mt-auto">
|
<div class="stats mt-auto">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">Tension (Upv)</div>
|
<div class="stat-label">Tension mesurée</div>
|
||||||
<div class="stat-value" id="voltage">0 <span class="stat-unit">V</span></div>
|
<div class="stat-value" id="voltage">0 <span class="stat-unit">V</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">Courant (Ipv)</div>
|
<div class="stat-label">Courant mesuré</div>
|
||||||
<div class="stat-value" id="current">0 <span class="stat-unit">A</span></div>
|
<div class="stat-value" id="current">0 <span class="stat-unit">A</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card" style="grid-column: span 2;">
|
||||||
<div class="stat-label">Luminosité</div>
|
<div class="stat-label">Puissance calculée (P = U × I)</div>
|
||||||
<div class="stat-value" id="lux">0 <span class="stat-unit">Lux</span></div>
|
<div class="stat-value" id="efficiency">0 <span class="stat-unit">W</span></div>
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">Rendement</div>
|
|
||||||
<div class="stat-value" id="efficiency">0 <span class="stat-unit">%</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sol-status-bar mt-3">
|
<div class="sol-status-bar mt-3">
|
||||||
<div class="sol-status-dot"></div>
|
<div class="sol-status-dot"></div>
|
||||||
<span class="sol-status-text">Système connecté — MPPT actif</span>
|
<span class="sol-status-text">INA219 actif — mesure en cours</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── BLOC 3 : État du Système ── -->
|
||||||
<div class="col-12 col-md-6 col-lg-3">
|
<div class="col-12 col-md-6 col-lg-3">
|
||||||
<div class="card card-batterie rounded-3">
|
<div class="card card-batterie rounded-3 h-100">
|
||||||
<div class="card-body p-4 d-flex flex-column">
|
<div class="card-body p-4 d-flex flex-column">
|
||||||
<h5 class="card-title fw-bold mb-4" style="color: #34d399;">
|
<h5 class="card-title fw-bold mb-4" style="color: #34d399;">
|
||||||
<i class="bi bi-battery-charging me-2"></i>État Batterie
|
<i class="bi bi-cpu me-2"></i>État du Système
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
<div class="d-flex flex-column align-items-center justify-content-center flex-grow-1">
|
<div class="d-flex flex-column gap-3 flex-grow-1 justify-content-center">
|
||||||
<div class="battery-circle" id="bat-circle">
|
|
||||||
<div class="battery-inner">
|
<div class="system-status-item" id="sys-item-global">
|
||||||
<span class="bat-percent" id="bat-val">--%</span>
|
<div class="d-flex align-items-center gap-2">
|
||||||
<small class="text-secondary text-uppercase fw-bold">SoC</small>
|
<span class="sys-dot bg-success"></span>
|
||||||
|
<span class="sys-label">Système actif</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="system-status-item" id="sys-item-esp">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="sys-dot" id="dot-esp"></span>
|
||||||
|
<span class="sys-label">ESP32</span>
|
||||||
|
<span class="ms-auto sys-badge" id="badge-esp">En attente</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="system-status-item">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="sys-dot bg-success"></span>
|
||||||
|
<span class="sys-label">DHT11</span>
|
||||||
|
<span class="ms-auto sys-badge sys-badge-ok">Actif</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="system-status-item">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="sys-dot bg-success"></span>
|
||||||
|
<span class="sys-label">INA219</span>
|
||||||
|
<span class="ms-auto sys-badge sys-badge-ok">Actif</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="system-status-item">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="sys-dot bg-info"></span>
|
||||||
|
<span class="sys-label">Raspberry Pi</span>
|
||||||
|
<span class="ms-auto sys-badge sys-badge-ok">Hébergement</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 text-center">
|
<div class="mt-4 text-center">
|
||||||
@@ -153,8 +182,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── BLOC 4 : Administration ── -->
|
||||||
<div class="col-12 col-md-6 col-lg-3">
|
<div class="col-12 col-md-6 col-lg-3">
|
||||||
<div class="card card-admin rounded-3">
|
<div class="card card-admin rounded-3 h-100">
|
||||||
<div class="card-body p-4 d-flex flex-column">
|
<div class="card-body p-4 d-flex flex-column">
|
||||||
<h5 class="card-title fw-bold mb-4" style="color: #f87171;">
|
<h5 class="card-title fw-bold mb-4" style="color: #f87171;">
|
||||||
<i class="bi bi-sliders me-2"></i>Administration
|
<i class="bi bi-sliders me-2"></i>Administration
|
||||||
@@ -164,7 +194,7 @@
|
|||||||
<a href="/admin/dashboard"
|
<a href="/admin/dashboard"
|
||||||
class="btn btn-outline-danger w-100 py-3 fw-bold rounded-3 d-flex flex-column align-items-center shadow-sm">
|
class="btn btn-outline-danger w-100 py-3 fw-bold rounded-3 d-flex flex-column align-items-center shadow-sm">
|
||||||
<i class="bi bi-gear-fill display-4 mb-2"></i>
|
<i class="bi bi-gear-fill display-4 mb-2"></i>
|
||||||
<span>Paramètres Système</span>
|
Paramètres Système
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/admin/logout" class="btn btn-dark border-secondary w-100 text-secondary mt-2">
|
<a href="/admin/logout" class="btn btn-dark border-secondary w-100 text-secondary mt-2">
|
||||||
@@ -175,6 +205,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Historique environnement ── -->
|
||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
<div class="card rounded-3 shadow-sm border border-secondary border-opacity-25 h-100">
|
<div class="card rounded-3 shadow-sm border border-secondary border-opacity-25 h-100">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
@@ -188,11 +219,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Historique mesures électriques ── -->
|
||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
<div class="card rounded-3 shadow-sm border border-secondary border-opacity-25 h-100">
|
<div class="card rounded-3 shadow-sm border border-secondary border-opacity-25 h-100">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
<h5 class="fw-bold mb-4 text-white">
|
<h5 class="fw-bold mb-4 text-white">
|
||||||
<i class="bi bi-lightning-charge me-2 text-white"></i>Historique énergie
|
<i class="bi bi-lightning-charge me-2 text-white"></i>Historique mesures électriques
|
||||||
</h5>
|
</h5>
|
||||||
<div style="height: 340px;">
|
<div style="height: 340px;">
|
||||||
<canvas id="energyChart"></canvas>
|
<canvas id="energyChart"></canvas>
|
||||||
|
|||||||
Reference in New Issue
Block a user