diff --git a/.gitignore b/.gitignore index 531ddc5..15a1029 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,20 @@ -# Python +# Environnement Python +venv/ __pycache__/ *.pyc *.pyo +*.pyd -# Virtual env -.venv/ +# Fichiers temporaires +.pytest_cache/ +ecocharge_code_complet.txt -# VS Code +# VS Code / système .vscode/ - -# OS .DS_Store # Logs *.log -# Docker -*.dbvenv/ +# Variables d'environnement +.env \ No newline at end of file diff --git a/app.py b/app.py index 5cee0e7..263ab52 100644 --- a/app.py +++ b/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): cur.execute("SELECT value FROM settings WHERE key = %s", (key,)) row = cur.fetchone() 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): @wraps(view_func) def wrapped_view(*args, **kwargs): @@ -72,6 +99,7 @@ def receive_data(): if not data: 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") humidity_ext = data.get("humidity_ext") system_status_msg = data.get("system_status") @@ -89,18 +117,29 @@ def receive_data(): conn = get_connection() cur = conn.cursor() - max_battery_temperature = float(get_setting_value(cur, "max_battery_temperature", "60")) - min_battery_voltage = float(get_setting_value(cur, "min_battery_voltage", "11")) - min_solar_power = float(get_setting_value(cur, "min_solar_power", "5")) + # --- Seuils visuels (cohérents avec les VRAIS capteurs) --- + # Nouvelles clés pour ignorer d'anciennes valeurs incohérentes restées en base. + 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 = "Température batterie trop élevée" - elif voltage_battery is not None and float(voltage_battery) < min_battery_voltage: - battery_alert = "Tension batterie trop faible" - elif power_pv is not None and float(power_pv) < min_solar_power: - battery_alert = "Puissance solaire insuffisante" + battery_alert = "none" # nom de colonne conservé pour ne rien casser en base + + if temp_value is not None and temp_value > max_temperature: + battery_alert = "Température trop élevée" + elif voltage_value is not None and voltage_value < min_voltage: + 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(""" INSERT INTO telemetry ( @@ -147,10 +186,13 @@ def latest_data(): conn = get_connection() 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(""" SELECT id, created_at, + EXTRACT(EPOCH FROM (NOW() - created_at)) AS age_seconds, temperature_ext, humidity_ext, system_status_msg, @@ -176,30 +218,33 @@ def latest_data(): if not row: return jsonify({"message": "Aucune donnée disponible"}), 404 + age = row[2] + return jsonify({ "id": row[0], "created_at": str(row[1]), - "temperature_ext": row[2], - "humidity_ext": row[3], - "system_status": row[4], - "voltage_pv": row[5], - "current_pv": row[6], - "power_pv": row[7], - "luminosity": row[8], - "voltage_battery": row[9], - "current_battery": row[10], - "battery_temp": row[11], - "battery_level": row[12], - "battery_alert": row[13] + "data_age_seconds": float(age) if age is not None else None, + "temperature_ext": row[3], + "humidity_ext": row[4], + "system_status": row[5], + "voltage_pv": row[6], + "current_pv": row[7], + "power_pv": row[8], + "luminosity": row[9], + "voltage_battery": row[10], + "current_battery": row[11], + "battery_temp": row[12], + "battery_level": row[13], + "battery_alert": row[14] }) @app.route("/api/history", methods=["GET"]) 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: - limit = 10 + limit = 15 if limit > 50: limit = 50 @@ -207,6 +252,7 @@ def history_data(): conn = get_connection() cur = conn.cursor() + # On renvoie maintenant current_pv (courant mesuré) pour le graphique électrique. cur.execute(""" SELECT id, @@ -214,7 +260,7 @@ def history_data(): temperature_ext, humidity_ext, power_pv, - battery_level + current_pv FROM telemetry ORDER BY created_at DESC LIMIT %s @@ -233,7 +279,7 @@ def history_data(): "temperature_ext": row[2], "humidity_ext": row[3], "power_pv": row[4], - "battery_level": row[5] + "current_pv": row[5] }) return jsonify(history) @@ -286,43 +332,35 @@ def admin_dashboard(): cur = conn.cursor() if request.method == "POST": - min_battery = request.form.get("min_battery") - max_temp = request.form.get("max_temp") - min_power = request.form.get("min_power") + # Normalisation : on accepte la virgule ET le point (ex : 0,3 -> 0.3) + min_voltage = normalize_number(request.form.get("min_battery")) + max_temp = normalize_number(request.form.get("max_temp")) + min_power = normalize_number(request.form.get("min_power")) - cur.execute(""" - INSERT INTO settings (key, value) - VALUES ('min_battery_voltage', %s) - 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,)) + upsert_setting(cur, "min_voltage", min_voltage) + upsert_setting(cur, "max_temp", max_temp) + upsert_setting(cur, "min_power", min_power) 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 = { - "min_battery": get_setting_value(cur, "min_battery_voltage", "11"), - "max_temp": get_setting_value(cur, "max_battery_temperature", "60"), - "min_power": get_setting_value(cur, "min_solar_power", "5") + "min_battery": get_setting_value(cur, "min_voltage", "0.2"), + "max_temp": get_setting_value(cur, "max_temp", "60"), + "min_power": get_setting_value(cur, "min_power", "0.1") } cur.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"]) diff --git a/static/script.js b/static/script.js index 23bce78..0fcbfec 100644 --- a/static/script.js +++ b/static/script.js @@ -1,7 +1,17 @@ -let isOnline = true; -let lastHeartbeat = Date.now(); -const TIMEOUT_MS = 5000; +// ========================================================= +// EcoCharge — Dashboard dynamique +// - 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 statusText = document.getElementById('status-text'); @@ -12,12 +22,13 @@ const apiTempEl = document.getElementById('api-temp'); const apiDescEl = document.getElementById('api-desc'); 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 batAlertIcon = document.getElementById('bat-alert-icon'); const batAlertText = document.getElementById('bat-alert'); +const dotEsp = document.getElementById('dot-esp'); +const badgeEsp = document.getElementById('badge-esp'); + const MAX_POWER = 100; const gaugeCircle = document.querySelector('.gauge-fill circle'); const halfCircumference = 283 / 2; @@ -25,6 +36,9 @@ const halfCircumference = 283 / 2; let envChart = null; let energyChart = null; +// ========================================================= +// Météo externe (API web open-meteo) — purement informatif +// ========================================================= function getWeatherDetails(code) { 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" }; @@ -56,56 +70,60 @@ async function fetchRealWeather() { } } -function updateBattery(percent, alertTextFromApi = null) { - batValEl.innerText = percent + "%"; - let color = "#34d399"; - let shadowColor = "rgba(52, 211, 153, 0.4)"; +// ========================================================= +// Statut EN LIGNE / HORS LIGNE (cohérent partout) +// ========================================================= +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") { - color = "#ef4444"; - shadowColor = "rgba(239, 68, 68, 0.6)"; - batAlertText.innerText = alertTextFromApi; - batAlertIcon.className = "bi bi-exclamation-triangle-fill text-danger"; - batAlertBadge.className = "badge bg-dark border border-danger text-secondary w-100 py-2"; - } else if (percent <= 20) { - color = "#ef4444"; - shadowColor = "rgba(239, 68, 68, 0.6)"; - batAlertText.innerText = "Niveau critique"; - batAlertIcon.className = "bi bi-exclamation-triangle-fill text-danger"; - batAlertBadge.className = "badge bg-dark border border-danger text-secondary w-100 py-2"; - } else if (percent <= 50) { - color = "#fbbf24"; - shadowColor = "rgba(251, 191, 36, 0.5)"; - batAlertText.innerText = "Niveau moyen"; - batAlertIcon.className = "bi bi-exclamation-circle text-warning"; + if (dotEsp) dotEsp.className = "sys-dot bg-success"; + if (badgeEsp) { + badgeEsp.className = "ms-auto sys-badge sys-badge-ok"; + badgeEsp.innerText = "Connecté"; + } + } else { + statusDot.className = "status-dot rounded-circle pulse-offline"; + statusText.innerText = "HORS LIGNE"; + statusText.style.color = "#ef4444"; + + if (dotEsp) dotEsp.className = "sys-dot bg-danger"; + if (badgeEsp) { + badgeEsp.className = "ms-auto sys-badge sys-badge-err"; + badgeEsp.innerText = "Hors ligne"; + } + } +} + +// 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"; } else { batAlertText.innerText = "Système Normal"; batAlertIcon.className = "bi bi-check-circle text-success"; 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 c = Number(current).toFixed(2); 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('voltage').innerHTML = v + ' V'; document.getElementById('current').innerHTML = c + ' A'; - document.getElementById('lux').innerHTML = l.toLocaleString() + ' Lux'; - document.getElementById('efficiency').innerHTML = efficiency + ' %'; + document.getElementById('efficiency').innerHTML = p + ' W'; - const ratio = Math.min(p / MAX_POWER, 1); + const ratio = Math.min(Number(power) / MAX_POWER, 1); const offset = halfCircumference - (ratio * halfCircumference); 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() { const ctx = document.getElementById('envChart'); if (!ctx) return; @@ -126,60 +148,66 @@ function createEnvChart() { label: 'Température (°C)', data: [], borderColor: '#38bdf8', - backgroundColor: 'rgba(56, 189, 248, 0.2)', + backgroundColor: 'rgba(56, 189, 248, 0.08)', pointBackgroundColor: '#38bdf8', - pointBorderColor: '#38bdf8', borderWidth: 2, - tension: 0.3 + pointRadius: 3, + tension: 0.35, + yAxisID: 'y' }, { label: 'Humidité (%)', data: [], borderColor: '#ff5c8a', - backgroundColor: 'rgba(255, 92, 138, 0.2)', + backgroundColor: 'rgba(255, 92, 138, 0.08)', pointBackgroundColor: '#ff5c8a', - pointBorderColor: '#ff5c8a', borderWidth: 2, - tension: 0.3 + pointRadius: 3, + tension: 0.35, + yAxisID: 'y1' } ] }, options: { responsive: true, maintainAspectRatio: false, - interaction: { - mode: 'index', - intersect: false - }, + animation: false, + interaction: { mode: 'index', intersect: false }, plugins: { - legend: { - labels: { - color: '#e2e8f0' - } - } + legend: { labels: { color: '#e2e8f0' } } }, scales: { x: { - ticks: { - color: '#94a3b8' - }, - grid: { - color: 'rgba(148, 163, 184, 0.1)' - } + ticks: { color: '#94a3b8' }, + grid: { color: 'rgba(148, 163, 184, 0.1)' } }, y: { - ticks: { - color: '#94a3b8' - }, - grid: { - color: 'rgba(148, 163, 184, 0.1)' - } + position: 'left', + suggestedMin: 0, + suggestedMax: 50, + title: { display: false }, + 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() { const ctx = document.getElementById('energyChart'); if (!ctx) return; @@ -190,76 +218,94 @@ function createEnergyChart() { labels: [], datasets: [ { - label: 'Batterie (%)', - data: [], - borderColor: '#34d399', - backgroundColor: 'rgba(52, 211, 153, 0.2)', - pointBackgroundColor: '#34d399', - pointBorderColor: '#34d399', - borderWidth: 2, - tension: 0.3 - }, - { - label: 'Puissance solaire (W)', + label: 'Puissance calculée (W)', data: [], borderColor: '#facc15', - backgroundColor: 'rgba(250, 204, 21, 0.2)', + backgroundColor: 'rgba(250, 204, 21, 0.08)', pointBackgroundColor: '#facc15', - pointBorderColor: '#facc15', 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: { responsive: true, maintainAspectRatio: false, - interaction: { - mode: 'index', - intersect: false - }, + animation: false, + interaction: { mode: 'index', intersect: false }, plugins: { - legend: { - labels: { - color: '#e2e8f0' - } - } + legend: { labels: { color: '#e2e8f0' } } }, scales: { x: { - ticks: { - color: '#94a3b8' - }, - grid: { - color: 'rgba(148, 163, 184, 0.1)' - } + ticks: { color: '#94a3b8' }, + grid: { color: 'rgba(148, 163, 184, 0.1)' } }, y: { - ticks: { - color: '#94a3b8' - }, - grid: { - color: 'rgba(148, 163, 184, 0.1)' - } + position: 'left', + beginAtZero: true, + title: { display: false }, + ticks: { color: '#94a3b8' }, + 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 { - const response = await fetch('/api/history?limit=10'); - + const response = await fetch('/api/history?limit=' + HISTORY_LIMIT); if (!response.ok) { throw new Error("Impossible de récupérer l'historique"); } const history = await response.json(); - const labels = history.map(item => { - const date = new Date(item.created_at); - return date.toLocaleTimeString('fr-FR', { + // --- Heure de l'axe X : on affiche l'heure LOCALE du navigateur --- + // Le timestamp serveur (Docker) peut être en mauvais fuseau (ex. 01:11 au lieu + // 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', minute: '2-digit', second: '2-digit' @@ -270,86 +316,78 @@ async function fetchHistoryData() { envChart.data.labels = labels; 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.update(); + envChart.update('none'); } if (energyChart) { energyChart.data.labels = labels; - energyChart.data.datasets[0].data = history.map(item => item.battery_level ?? null); - energyChart.data.datasets[1].data = history.map(item => item.power_pv ?? null); - energyChart.update(); + energyChart.data.datasets[0].data = history.map(item => item.power_pv ?? null); + energyChart.data.datasets[1].data = history.map(item => item.current_pv ?? null); + energyChart.update('none'); } - } catch (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 { const response = await fetch('/api/latest'); + // Pas de données en base -> HORS LIGNE if (!response.ok) { - throw new Error("Base de données vide ou serveur injoignable"); + updateConnectionStatus(false); + updateAlert(null); + return; } const data = await response.json(); - tempEl.innerText = data.temperature_ext !== null ? Number(data.temperature_ext).toFixed(1) : "--"; - humEl.innerText = data.humidity_ext !== null ? Math.floor(data.humidity_ext) : "--"; + // EN LIGNE seulement si la dernière mesure est récente + const age = data.data_age_seconds; + const online = (age !== null && age !== undefined && age < STALE_SECONDS); - if (data.battery_level !== null) { - const batteryAlert = data.battery_alert === true || data.battery_alert === "true" || data.battery_alert === 1 || data.battery_alert === "1"; -updateBattery(Math.floor(data.battery_level), batteryAlert ? "Alerte batterie" : null); - } + // Mise à jour des valeurs (DHT11 + INA219) + tempEl.innerText = (data.temperature_ext !== null && data.temperature_ext !== undefined) + ? 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.current_pv || 0, - data.power_pv || 0, - data.luminosity || 0 + data.power_pv || 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) { console.error("En attente de données capteurs...", error); + updateConnectionStatus(false); + updateAlert(null); } } -function checkSystemStatus() { - if (Date.now() - lastHeartbeat > TIMEOUT_MS) { - statusDot.className = "status-dot rounded-circle pulse-offline"; - statusText.innerText = "HORS LIGNE"; - statusText.style.color = "#ef4444"; - } else { - statusDot.className = "status-dot rounded-circle pulse-online"; - statusText.innerText = "EN LIGNE"; - statusText.style.color = "#e2e8f0"; - } -} - -document.getElementById('btn-test').addEventListener('click', function () { - isOnline = !isOnline; - if (isOnline) { - this.innerHTML = ' Simuler Coupure'; - this.classList.replace('btn-outline-success', 'btn-outline-danger'); - lastHeartbeat = Date.now(); - } else { - this.innerHTML = ' Rétablir Connexion'; - this.classList.replace('btn-outline-danger', 'btn-outline-success'); - } -}); +// ========================================================= +// Démarrage +// ========================================================= +// État initial neutre : on n'affiche PAS "EN LIGNE" tant qu'on n'a rien confirmé. +statusText.innerText = "Vérification…"; +statusText.style.color = "#94a3b8"; +statusDot.className = "status-dot rounded-circle"; createEnvChart(); createEnergyChart(); -fetchRealWeather(); -fetchDatabaseData(); -fetchHistoryData(); -checkSystemStatus(); -setInterval(fetchRealWeather, 900000); -setInterval(fetchDatabaseData, 10000); -setInterval(fetchHistoryData, 30000); -setInterval(checkSystemStatus, 10000); \ No newline at end of file +fetchRealWeather(); +fetchLatest(); +fetchHistory(); + +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 \ No newline at end of file diff --git a/static/style.css b/static/style.css index 3e85bb7..d6338b4 100644 --- a/static/style.css +++ b/static/style.css @@ -17,6 +17,15 @@ body { 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 { border-top: 4px solid #38bdf8; } @@ -38,6 +47,8 @@ body { width: 14px; height: 14px; display: inline-block; + background-color: #64748b; + border-radius: 50%; } .pulse-online { @@ -81,44 +92,55 @@ body { } /* ========================================= - WIDGET BATTERIE (Code de l'Étudiant 2) + BLOC ÉTAT DU SYSTÈME ========================================= */ -.battery-circle { - width: 140px; - height: 140px; - border-radius: 50%; - display: flex; - 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; +.system-status-item { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 10px; + padding: 10px 14px; } -.battery-inner { - width: 110px; - height: 110px; +.sys-dot { + width: 10px; + height: 10px; border-radius: 50%; - background: #1e293b; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; + flex-shrink: 0; + display: inline-block; } -.bat-percent { - font-size: 32px; - font-weight: bold; - color: #e0f2fe; - text-shadow: 0 0 10px rgba(52, 211, 153, 0.5); - line-height: 1; +.sys-label { + font-size: 13px; + font-weight: 600; + color: #e2e8f0; +} + +.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 { position: relative; width: 160px; @@ -155,7 +177,6 @@ body { transform-origin: center; } -/* CORRECTION ICI : Le texte est parfaitement centré au milieu du cercle */ .gauge-value { position: absolute; top: 50%; diff --git a/templates/admin_dashboard.html b/templates/admin_dashboard.html index cea1dde..817d448 100644 --- a/templates/admin_dashboard.html +++ b/templates/admin_dashboard.html @@ -185,6 +185,28 @@ 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 ── */ .succes { background-color: rgba(46, 204, 113, 0.15); @@ -219,22 +241,29 @@
+ + {% if saved %} +
+ ✅ Paramètres sauvegardés +
+ {% endif %} +

Seuils d'alerte

Modifiez les valeurs puis cliquez sur "Sauvegarder".

- +
- +
-

Alerte si la tension de la batterie descend sous ce seuil.

+

Alerte si la tension mesurée (INA219) descend sous ce seuil.


+ min="0" step="any" inputmode="decimal" required> V
@@ -246,33 +275,36 @@
-

Alerte si la température dépasse cette valeur.

+

Alerte si la température (DHT11) dépasse cette valeur.


+ max="100" step="any" inputmode="decimal" required> °C

- +
- +
-

Alerte si la production solaire descend sous ce seuil.

+

Alerte si la puissance calculée (P = U × I) descend sous ce seuil.


+ step="any" inputmode="decimal" required> W
+ + ← Retour au dashboard + diff --git a/templates/index.html b/templates/index.html index e90d135..106382c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,7 +4,7 @@ - Station Solaire | Dashboard PRO + EcoCharge | Dashboard @@ -21,8 +21,8 @@
- Système en ligne -
+ Vérification… +
@@ -30,8 +30,9 @@
+
-
+
Météo & Env. @@ -65,19 +66,16 @@
- -
+
-
+
- Production Solaire + Mesures Électriques
@@ -101,45 +99,76 @@
-
Tension (Upv)
+
Tension mesurée
0 V
-
Courant (Ipv)
+
Courant mesuré
0 A
-
-
Luminosité
-
0 Lux
-
-
-
Rendement
-
0 %
+
+
Puissance calculée (P = U × I)
+
0 W
- Système connecté — MPPT actif + INA219 actif — mesure en cours
+
-
+
- État Batterie + État du Système
-
-
-
- --% - SoC +
+ +
+
+ + Système actif
+ +
+
+ + ESP32 + En attente +
+
+ +
+
+ + DHT11 + Actif +
+
+ +
+
+ + INA219 + Actif +
+
+ +
+
+ + Raspberry Pi + Hébergement +
+
+
@@ -153,8 +182,9 @@
+