Files
ecocharge/static/script.js

393 lines
15 KiB
JavaScript

// =========================================================
// 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}&current=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 + ' <span class="stat-unit">V</span>';
document.getElementById('current').innerHTML = c + ' <span class="stat-unit">A</span>';
document.getElementById('efficiency').innerHTML = p + ' <span class="stat-unit">W</span>';
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