let isOnline = true; let lastHeartbeat = Date.now(); const TIMEOUT_MS = 5000; 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 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 MAX_POWER = 100; const gaugeCircle = document.querySelector('.gauge-fill circle'); const halfCircumference = 283 / 2; let envChart = null; let energyChart = null; 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); } } function updateBattery(percent, alertTextFromApi = null) { batValEl.innerText = percent + "%"; let color = "#34d399"; let shadowColor = "rgba(52, 211, 153, 0.4)"; 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"; 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) { 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 + ' %'; const ratio = Math.min(p / MAX_POWER, 1); const offset = halfCircumference - (ratio * halfCircumference); if (gaugeCircle) { gaugeCircle.style.strokeDashoffset = offset; } } 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.2)', pointBackgroundColor: '#38bdf8', pointBorderColor: '#38bdf8', borderWidth: 2, tension: 0.3 }, { label: 'Humidité (%)', data: [], borderColor: '#ff5c8a', backgroundColor: 'rgba(255, 92, 138, 0.2)', pointBackgroundColor: '#ff5c8a', pointBorderColor: '#ff5c8a', borderWidth: 2, tension: 0.3 } ] }, options: { responsive: true, maintainAspectRatio: 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: { ticks: { color: '#94a3b8' }, grid: { color: 'rgba(148, 163, 184, 0.1)' } } } } }); } function createEnergyChart() { const ctx = document.getElementById('energyChart'); if (!ctx) return; energyChart = new Chart(ctx, { type: 'line', data: { 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)', data: [], borderColor: '#facc15', backgroundColor: 'rgba(250, 204, 21, 0.2)', pointBackgroundColor: '#facc15', pointBorderColor: '#facc15', borderWidth: 2, tension: 0.3 } ] }, options: { responsive: true, maintainAspectRatio: 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: { ticks: { color: '#94a3b8' }, grid: { color: 'rgba(148, 163, 184, 0.1)' } } } } }); } async function fetchHistoryData() { try { const response = await fetch('/api/history?limit=10'); 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', { 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(); } 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(); } } catch (error) { console.error("Erreur historique :", error); } } async function fetchDatabaseData() { if (!isOnline) return; try { const response = await fetch('/api/latest'); if (!response.ok) { throw new Error("Base de données vide ou serveur injoignable"); } 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) : "--"; if (data.battery_level !== null) { updateBattery(Math.floor(data.battery_level), data.battery_alert); } updateSolar( data.voltage_pv || 0, data.current_pv || 0, data.power_pv || 0, data.luminosity || 0 ); lastHeartbeat = Date.now(); } catch (error) { console.error("En attente de données capteurs...", error); } } 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'); } }); createEnvChart(); createEnergyChart(); fetchRealWeather(); fetchDatabaseData(); fetchHistoryData(); checkSystemStatus(); setInterval(fetchRealWeather, 900000); setInterval(fetchDatabaseData, 3000); setInterval(fetchHistoryData, 5000); setInterval(checkSystemStatus, 1000);