// ========================================================= // 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'); const tempEl = document.getElementById('temp-val'); const humEl = document.getElementById('hum-val'); const apiTempEl = document.getElementById('api-temp'); const apiDescEl = document.getElementById('api-desc'); const apiIconEl = document.getElementById('api-icon'); 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; 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" }; if (code >= 45 && code <= 48) return { desc: "Brouillard", icon: "bi-cloud-haze", color: "text-secondary" }; if (code >= 51 && code <= 67) return { desc: "Pluie", icon: "bi-cloud-rain", color: "text-info" }; if (code >= 71 && code <= 77) return { desc: "Neige", icon: "bi-cloud-snow", color: "text-white" }; if (code >= 95) return { desc: "Orage", icon: "bi-cloud-lightning", color: "text-danger" }; return { desc: "Inconnu", icon: "bi-thermometer", color: "text-muted" }; } async function fetchRealWeather() { try { const lat = 48.8566; const lon = 2.3522; const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,weather_code`; const response = await fetch(url); const data = await response.json(); const temp = data.current.temperature_2m; const code = data.current.weather_code; const weather = getWeatherDetails(code); apiTempEl.innerText = temp + "°C"; apiDescEl.innerText = weather.desc; apiIconEl.className = "bi " + weather.icon + " " + weather.color + " display-4 me-3"; } catch (error) { console.error("Erreur Météo:", error); } } // ========================================================= // 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 (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"; } } // ========================================================= // 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); document.getElementById('powerValue').textContent = p; document.getElementById('voltage').innerHTML = v + ' V'; document.getElementById('current').innerHTML = c + ' A'; document.getElementById('efficiency').innerHTML = p + ' W'; const ratio = Math.min(Number(power) / MAX_POWER, 1); const offset = halfCircumference - (ratio * halfCircumference); if (gaugeCircle) { gaugeCircle.style.strokeDashoffset = offset; } } // ========================================================= // Graphique 1 : Historique environnement (DHT11) // Température (axe gauche) + Humidité (axe droit) // ========================================================= function createEnvChart() { const ctx = document.getElementById('envChart'); if (!ctx) return; envChart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [ { label: 'Température (°C)', data: [], borderColor: '#38bdf8', backgroundColor: 'rgba(56, 189, 248, 0.08)', pointBackgroundColor: '#38bdf8', borderWidth: 2, pointRadius: 3, tension: 0.35, yAxisID: 'y' }, { label: 'Humidité (%)', data: [], borderColor: '#ff5c8a', backgroundColor: 'rgba(255, 92, 138, 0.08)', pointBackgroundColor: '#ff5c8a', borderWidth: 2, pointRadius: 3, tension: 0.35, yAxisID: 'y1' } ] }, options: { responsive: true, maintainAspectRatio: false, animation: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { labels: { color: '#e2e8f0' } } }, scales: { x: { ticks: { color: '#94a3b8' }, grid: { color: 'rgba(148, 163, 184, 0.1)' } }, y: { 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; energyChart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [ { label: 'Puissance calculée (W)', data: [], borderColor: '#facc15', backgroundColor: 'rgba(250, 204, 21, 0.08)', pointBackgroundColor: '#facc15', borderWidth: 2, 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, animation: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { labels: { color: '#e2e8f0' } } }, scales: { x: { ticks: { color: '#94a3b8' }, grid: { color: 'rgba(148, 163, 184, 0.1)' } }, y: { 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 } } } } }); } // ========================================================= // Récupération de l'historique (graphiques) // ========================================================= async function fetchHistory() { try { 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(); // --- 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' }); }); if (envChart) { 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('none'); } if (energyChart) { energyChart.data.labels = labels; 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); } } // ========================================================= // 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) { updateConnectionStatus(false); updateAlert(null); return; } const data = await response.json(); // 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); // 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) : "--"; updateElectrical( data.voltage_pv || 0, data.current_pv || 0, data.power_pv || 0 ); 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); } } // ========================================================= // 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(); 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