Update project echo files

This commit is contained in:
Anismahi
2026-03-19 07:28:11 +01:00
parent 93409b6f71
commit 17026018a9
14 changed files with 1397 additions and 223 deletions

202
static/script.js Normal file
View File

@@ -0,0 +1,202 @@
// --- VARIABLES D'ÉTAT ---
let isOnline = true;
let lastHeartbeat = Date.now();
const TIMEOUT_MS = 5000;
// --- ÉLÉMENTS DU DOM PRINCIPAUX ---
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');
// --- ÉLÉMENTS DOM BATTERIE (Étudiant 2) ---
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');
// --- CONSTANTES SOLAIRE (Étudiant 1) ---
const MAX_POWER = 100; // Puissance maximum du panneau en Watts (à adapter si besoin)
const gaugeCircle = document.querySelector('.gauge-fill circle');
const halfCircumference = 283 / 2;
// =================================================================
// --- GESTION DE LA VRAIE API MÉTÉO WEB (Open-Meteo) ---
// =================================================================
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; // Tu peux changer avec la latitude de ta ville
const lon = 2.3522; // Tu peux changer avec la longitude de ta ville
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);
}
}
// =================================================================
// --- MISE À JOUR VISUELLE DES WIDGETS ---
// =================================================================
function updateBattery(percent) {
batValEl.innerText = percent + "%";
let color = "#34d399";
let shadowColor = "rgba(52, 211, 153, 0.4)";
if (percent <= 20) {
color = "#ef4444";
shadowColor = "rgba(239, 68, 68, 0.6)";
batAlertText.innerText = "Tension 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) {
// On s'assure que ce sont des nombres et on les formate
const v = Number(voltage).toFixed(1);
const c = Number(current).toFixed(2);
const p = Number(power).toFixed(1);
const l = Math.floor(Number(lux));
// Le serveur de l'étudiant 4 ne stocke pas l'efficacité, on simule ~15% quand il y a du soleil
const efficiency = (p > 0) ? (14 + Math.random() * 2).toFixed(1) : "0.0";
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('lux').innerHTML = l.toLocaleString() + ' <span class="stat-unit">Lux</span>';
document.getElementById('efficiency').innerHTML = efficiency + ' <span class="stat-unit">%</span>';
// Animation de la jauge SVG
const ratio = Math.min(p / MAX_POWER, 1);
const offset = halfCircumference - (ratio * halfCircumference);
if (gaugeCircle) {
gaugeCircle.style.strokeDashoffset = offset;
}
}
// =================================================================
// --- NOUVEAU : CONNEXION AU SERVEUR PYTHON (BASE DE DONNÉES) ---
// =================================================================
async function fetchDatabaseData() {
if (!isOnline) return;
try {
// On interroge l'URL magique de l'étudiant 4
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();
// 1. Mise à jour de ton capteur local DHT11
tempEl.innerText = data.temperature_ext !== null ? Number(data.temperature_ext).toFixed(1) : "--";
humEl.innerText = data.humidity_ext !== null ? Math.floor(data.humidity_ext) : "--";
// 2. Mise à jour de la Batterie
if (data.battery_level !== null) {
updateBattery(Math.floor(data.battery_level));
}
// 3. Mise à jour du Solaire
updateSolar(
data.voltage_pv || 0,
data.current_pv || 0,
data.power_pv || 0,
data.luminosity || 0
);
// Si on arrive ici, c'est qu'on a bien communiqué avec le serveur : on met à jour le Heartbeat
lastHeartbeat = Date.now();
} catch (error) {
console.error("En attente de données capteurs...", error);
}
}
// --- FONCTION WATCHDOG ---
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";
}
}
// =================================================================
// --- LANCEMENT DES BOUCLES ---
// =================================================================
// Météo Web (1 fois au lancement, puis toutes les 15 min)
fetchRealWeather();
setInterval(fetchRealWeather, 900000);
// Base de données locale (toutes les 3 secondes)
setInterval(fetchDatabaseData, 3000);
// Watchdog (toutes les secondes)
setInterval(checkSystemStatus, 1000);
// Bouton de simulation de panne
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');
}
});

248
static/style.css Normal file
View File

@@ -0,0 +1,248 @@
/* --- COULEURS GLOBALES ET THEME SOMBRE --- */
body {
background-color: #0f172a;
color: #e2e8f0;
}
.navbar {
background-color: #1e293b !important;
border-bottom: 1px solid #334155;
}
/* --- DESIGN DES WIDGETS (CARTES) --- */
.card {
background-color: #1e293b;
border: 1px solid #334155;
min-height: 320px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
}
.card-meteo {
border-top: 4px solid #38bdf8;
}
.card-solaire {
border-top: 4px solid #fbbf24;
}
.card-batterie {
border-top: 4px solid #34d399;
}
.card-admin {
border-top: 4px solid #f87171;
}
/* --- ANIMATION DU HEARTBEAT PRINCIPAL --- */
.status-dot {
width: 14px;
height: 14px;
display: inline-block;
}
.pulse-online {
background-color: #22c55e;
box-shadow: 0 0 12px #22c55e;
animation: pulse-green 2s infinite;
}
.pulse-offline {
background-color: #ef4444;
box-shadow: 0 0 15px #ef4444;
}
@keyframes pulse-green {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 10px rgba(34, 197, 94, 0);
}
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
}
}
/* --- TYPOGRAPHIE ET ELEMENTS VISUELS --- */
.data-value {
color: #e0f2fe;
text-shadow: 0 0 10px rgba(56, 189, 248, 0.3);
}
.placeholder-box {
border: 2px dashed #475569;
background-color: #0f172a;
color: #94a3b8;
}
/* =========================================
WIDGET BATTERIE (Code de l'Étudiant 2)
========================================= */
.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;
}
.battery-inner {
width: 110px;
height: 110px;
border-radius: 50%;
background: #1e293b;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.bat-percent {
font-size: 32px;
font-weight: bold;
color: #e0f2fe;
text-shadow: 0 0 10px rgba(52, 211, 153, 0.5);
line-height: 1;
}
/* =========================================
WIDGET SOLAIRE (Code de l'Étudiant 1 adapté et corrigé)
========================================= */
/* CORRECTION ICI : La boîte est maintenant carrée et ne déborde plus */
.gauge-container {
position: relative;
width: 160px;
height: 160px;
margin: 0 auto 15px;
}
.gauge-bg,
.gauge-fill {
position: absolute;
top: 0;
left: 0;
width: 160px;
height: 160px;
}
.gauge-bg circle,
.gauge-fill circle {
fill: none;
stroke-width: 18;
stroke-linecap: round;
}
.gauge-bg circle {
stroke: #0f172a;
}
.gauge-fill circle {
stroke: url(#gaugeGradient);
stroke-dasharray: 283;
stroke-dashoffset: 283;
transition: stroke-dashoffset 1s ease;
transform: rotate(180deg);
transform-origin: center;
}
/* CORRECTION ICI : Le texte est parfaitement centré au milieu du cercle */
.gauge-value {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
width: 100%;
}
.gauge-value .number {
font-size: 32px;
font-weight: 700;
color: #facc15;
line-height: 1;
}
.gauge-value .unit {
font-size: 14px;
color: #94a3b8;
font-weight: 600;
}
.stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.stat-card {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 10px;
padding: 10px;
}
.stat-label {
font-size: 10px;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
}
.stat-value {
font-size: 16px;
font-weight: 700;
color: #e2e8f0;
}
.stat-value .stat-unit {
font-size: 11px;
color: #64748b;
font-weight: 600;
}
.sol-status-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
background: rgba(34, 197, 94, 0.08);
border: 1px solid rgba(34, 197, 94, 0.15);
border-radius: 12px;
}
.sol-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
animation: blink-sol 1.5s ease-in-out infinite;
}
@keyframes blink-sol {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
.sol-status-text {
font-size: 12px;
color: #22c55e;
font-weight: 600;
}