From 17026018a9bbe19de4cc8fe57020c677bfac1389 Mon Sep 17 00:00:00 2001 From: Anismahi Date: Thu, 19 Mar 2026 07:28:11 +0100 Subject: [PATCH] Update project echo files --- .DS_Store | Bin 10244 -> 10244 bytes Dockerfile | 23 +++ app.py | 288 +++++++++++++++++++++++++++++ config.py | 10 + database.sql | 46 +++++ docker-compose.yml | 37 ++++ requirements.txt | 3 + script.js | 115 ------------ static/script.js | 202 ++++++++++++++++++++ static/style.css | 248 +++++++++++++++++++++++++ style.css | 81 -------- templates/admin_dashboard.html | 283 ++++++++++++++++++++++++++++ index.html => templates/index.html | 110 ++++++++--- templates/login.html | 174 +++++++++++++++++ 14 files changed, 1397 insertions(+), 223 deletions(-) create mode 100644 Dockerfile create mode 100644 app.py create mode 100644 config.py create mode 100644 database.sql create mode 100644 docker-compose.yml create mode 100644 requirements.txt delete mode 100644 script.js create mode 100644 static/script.js create mode 100644 static/style.css delete mode 100644 style.css create mode 100644 templates/admin_dashboard.html rename index.html => templates/index.html (50%) create mode 100644 templates/login.html diff --git a/.DS_Store b/.DS_Store index ae0901d485ccb92ff7fb4b4507b3952ec3c3daed..0821d24502a046d92fae9fef5456da546e43b6f0 100644 GIT binary patch delta 14 VcmZn(XbIR5A;`$MIZ|-77yu>(1fl={ delta 14 VcmZn(XbIR5A;`$EIZ|-77yu>z1fc)` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..efb9005 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# On part d'une version légère de Python +FROM python:3.10-slim + +# On se place dans le dossier /app du conteneur +WORKDIR /app + +# On installe les outils système nécessaires pour la base de données +RUN apt-get update && apt-get install -y libpq-dev gcc + +# On copie d'abord le fichier des dépendances +COPY requirements.txt . + +# On installe les bibliothèques Python +RUN pip install --no-cache-dir -r requirements.txt + +# On copie tout le reste du projet (tes HTML, CSS, app.py, etc.) +COPY . . + +# On expose le port 5000 pour ton Mac +EXPOSE 5000 + +# La commande pour démarrer le serveur +CMD ["python", "app.py"] \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..9a48576 --- /dev/null +++ b/app.py @@ -0,0 +1,288 @@ +from flask import Flask, jsonify, render_template, request, redirect, url_for, session +from flask_cors import CORS +from config import Config +import psycopg2 + +app = Flask(__name__) +app.config.from_object(Config) +app.secret_key = app.config["SECRET_KEY"] + +CORS(app) + + +def get_connection(): + return psycopg2.connect( + host=app.config["DB_HOST"], + port=app.config["DB_PORT"], + database=app.config["DB_NAME"], + user=app.config["DB_USER"], + password=app.config["DB_PASSWORD"] + ) + + +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 + + +# ======================================================== +# MODIFICATION ÉTUDIANT 3 : Sécurisation de l'accueil +# ======================================================== +@app.route("/") +def home(): + # Si l'utilisateur n'est pas connecté en tant qu'admin, + # on le force à aller sur la page de connexion. + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + + # S'il est connecté, on lui affiche le Dashboard. + return render_template("index.html") +# ======================================================== + + +@app.route("/api/status") +def status(): + return jsonify({ + "project": "EcoCharge", + "api": "ok", + "database": "postgresql_connected" + }) + + +@app.route("/api/data", methods=["POST"]) +def receive_data(): + data = request.get_json() + + if not data: + return jsonify({"error": "Aucune donnée JSON reçue"}), 400 + + # Données environnement + temperature_ext = data.get("temperature_ext") + humidity_ext = data.get("humidity_ext") + system_status_msg = data.get("system_status") + + # Données panneau solaire + voltage_pv = data.get("voltage_pv") + current_pv = data.get("current_pv") + + # compatible solar_power + power_pv = data.get("power_pv", data.get("solar_power")) + + luminosity = data.get("luminosity") + + # Données batterie + voltage_battery = data.get("voltage_battery", data.get("battery_voltage")) + current_battery = data.get("current_battery") + + # compatible battery_temperature + battery_temp = data.get("battery_temp", data.get("battery_temperature")) + + battery_level = data.get("battery_level") + + 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")) + + battery_alert = "none" + + 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" + + cur.execute(""" + INSERT INTO telemetry ( + temperature_ext, + humidity_ext, + system_status_msg, + voltage_pv, + current_pv, + power_pv, + luminosity, + voltage_battery, + current_battery, + battery_temp, + battery_level, + battery_alert + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + """, ( + temperature_ext, + humidity_ext, + system_status_msg, + voltage_pv, + current_pv, + power_pv, + luminosity, + voltage_battery, + current_battery, + battery_temp, + battery_level, + battery_alert + )) + + conn.commit() + cur.close() + conn.close() + + return jsonify({ + "message": "Données enregistrées avec succès", + "battery_alert": battery_alert + }), 201 + + +@app.route("/api/latest", methods=["GET"]) +def latest_data(): + + conn = get_connection() + cur = conn.cursor() + + cur.execute(""" + SELECT + id, + created_at, + temperature_ext, + humidity_ext, + system_status_msg, + voltage_pv, + current_pv, + power_pv, + luminosity, + voltage_battery, + current_battery, + battery_temp, + battery_level, + battery_alert + FROM telemetry + ORDER BY created_at DESC + LIMIT 1 + """) + + row = cur.fetchone() + + cur.close() + conn.close() + + if not row: + return jsonify({"message": "Aucune donnée disponible"}), 404 + + 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] + }) + + +@app.route("/admin/login", methods=["GET", "POST"]) +def admin_login(): + + if request.method == "POST": + + username = request.form.get("username") + password = request.form.get("password") + + conn = get_connection() + cur = conn.cursor() + + cur.execute( + "SELECT id FROM users WHERE username = %s AND password = %s AND is_admin = TRUE", + (username, password) + ) + + user = cur.fetchone() + + cur.close() + conn.close() + + if user: + session["admin_logged_in"] = True + + # ======================================================== + # MODIFICATION ÉTUDIANT 3 : Redirection vers le Dashboard + # ======================================================== + return redirect(url_for("home")) + # ======================================================== + + return render_template("login.html", error="Identifiants incorrects") + + return render_template("login.html") + + +@app.route("/admin/dashboard", methods=["GET", "POST"]) +def admin_dashboard(): + + if not session.get("admin_logged_in"): + return redirect(url_for("admin_login")) + + conn = get_connection() + 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") + + 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,)) + + conn.commit() + + 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") + } + + cur.close() + conn.close() + + return render_template("admin_dashboard.html", settings=settings) + + +@app.route("/admin/logout") +def admin_logout(): + + session.pop("admin_logged_in", None) + + return redirect(url_for("admin_login")) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000, debug=True) \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..bc68608 --- /dev/null +++ b/config.py @@ -0,0 +1,10 @@ +import os + +class Config: + # S'il trouve la variable Docker, il l'utilise. Sinon, il met "localhost" par défaut. + DB_HOST = os.environ.get("DB_HOST", "localhost") + DB_PORT = os.environ.get("DB_PORT", "5432") + DB_NAME = os.environ.get("DB_NAME", "ecocharge") + DB_USER = os.environ.get("DB_USER", "ecocharge_user") + DB_PASSWORD = os.environ.get("DB_PASSWORD", "ecocharge_password") + SECRET_KEY = os.environ.get("SECRET_KEY", "cle_par_defaut") \ No newline at end of file diff --git a/database.sql b/database.sql new file mode 100644 index 0000000..416e784 --- /dev/null +++ b/database.sql @@ -0,0 +1,46 @@ +DROP TABLE IF EXISTS telemetry CASCADE; +DROP TABLE IF EXISTS system_status CASCADE; +DROP TABLE IF EXISTS settings CASCADE; +DROP TABLE IF EXISTS users CASCADE; + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + is_admin BOOLEAN DEFAULT FALSE +); + +CREATE TABLE settings ( + id SERIAL PRIMARY KEY, + key VARCHAR(50) UNIQUE NOT NULL, + value VARCHAR(100) NOT NULL +); + +CREATE TABLE telemetry ( + id SERIAL PRIMARY KEY, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + temperature_ext DOUBLE PRECISION, + humidity_ext DOUBLE PRECISION, + system_status_msg VARCHAR(100), + + voltage_pv DOUBLE PRECISION, + current_pv DOUBLE PRECISION, + power_pv DOUBLE PRECISION, + luminosity DOUBLE PRECISION, + + voltage_battery DOUBLE PRECISION, + current_battery DOUBLE PRECISION, + battery_temp DOUBLE PRECISION, + battery_level DOUBLE PRECISION, + battery_alert VARCHAR(100) +); + +INSERT INTO users (username, password, is_admin) +VALUES ('admin', 'admin123', TRUE); + +INSERT INTO settings (key, value) +VALUES + ('min_battery_voltage', '11'), + ('max_battery_temperature', '60'), + ('min_solar_power', '5'); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cf9dd0e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.8' + +services: + # 1. LA BASE DE DONNÉES (PostgreSQL) + db: + image: postgres:15-alpine + container_name: ecocharge_db + environment: + POSTGRES_USER: ecocharge_user + POSTGRES_PASSWORD: ecocharge_password + POSTGRES_DB: ecocharge + ports: + - "5432:5432" # <-- Très important : ça te permet de t'y connecter avec DBeaver ! + volumes: + - postgres_data:/var/lib/postgresql/data + # La ligne magique : Docker va lire ton database.sql et créer les tables tout seul au premier lancement ! + - ./database.sql:/docker-entrypoint-initdb.d/init.sql + + # 2. TON SERVEUR WEB (Flask) + web: + build: . + container_name: ecocharge_web + ports: + - "5001:5000" # Ton Dashboard sera sur http://localhost:5000 + depends_on: + - db + environment: + # On donne les identifiants au serveur Python pour qu'il trouve la BDD + - DB_HOST=db + - DB_PORT=5432 + - DB_NAME=ecocharge + - DB_USER=ecocharge_user + - DB_PASSWORD=ecocharge_password + - SECRET_KEY=super_cle_secrete_projet + +volumes: + postgres_data: \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bb4eee6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask==3.0.0 +Flask-Cors==4.0.0 +psycopg2-binary==2.9.9 \ No newline at end of file diff --git a/script.js b/script.js deleted file mode 100644 index 02da469..0000000 --- a/script.js +++ /dev/null @@ -1,115 +0,0 @@ -// --- VARIABLES D'ÉTAT --- -let isOnline = true; -let lastHeartbeat = Date.now(); -const TIMEOUT_MS = 5000; // 5 secondes pour les tests - -// --- É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'); - -// ================================================================= -// --- NOUVEAU : GESTION DE LA VRAIE API MÉTÉO (OPEN-METEO) --- -// ================================================================= - -// 1. Traduction des codes météo de l'API en Icônes et Textes -function getWeatherDetails(code) { - if (code === 0) return { desc: "Dégagé", icon: "bi-brightness-high", color: "text-warning" }; - if (code === 1 || code === 2 || 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" }; -} - -// 2. Fonction qui va chercher les données sur Internet -async function fetchRealWeather() { - try { - // --- METS LES COORDONNÉES DE TA VILLE ICI --- - const lat = 48.8566; // Latitude (Ex: Paris) - const lon = 2.3522; // Longitude (Ex: Paris) - - // L'URL magique de l'API Open-Meteo - const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,weather_code`; - - // On lance la requête - const response = await fetch(url); - const data = await response.json(); // On convertit la réponse en JSON - - // On extrait la température et le code météo - const temp = data.current.temperature_2m; - const code = data.current.weather_code; - - // On récupère la bonne icône et le bon texte - const weather = getWeatherDetails(code); - - // On met à jour l'interface HTML ! - apiTempEl.innerText = temp + "°C"; - apiDescEl.innerText = weather.desc; - apiIconEl.className = "bi " + weather.icon + " " + weather.color + " display-4 me-3"; - - } catch (error) { - console.error("Erreur avec l'API Météo:", error); - apiDescEl.innerText = "Erreur Web"; - apiIconEl.className = "bi bi-wifi-off text-danger display-4 me-3"; - } -} - -// ================================================================= - -// --- FONCTION DE MISE À JOUR DES DONNÉES LOCALES (Ton DHT11) --- -function fetchLocalData() { - if (!isOnline) return; - - // Simulation de ton capteur DHT11 local - tempEl.innerText = (22 + (Math.random() * 2 - 1)).toFixed(1); - humEl.innerText = Math.floor(45 + Math.random() * 5); - - lastHeartbeat = Date.now(); -} - -// --- FONCTION WATCHDOG (LE HEARTBEAT) --- -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 PROGRAMMES --- - -// 1. Lancer la météo Web une première fois tout de suite -fetchRealWeather(); - -// 2. Les boucles automatiques -setInterval(fetchLocalData, 3000); // Mise à jour de ton capteur local toutes les 3s -setInterval(checkSystemStatus, 1000); // Surveillance de la connexion - -// ATTENTION: On ne met à jour la météo Web que toutes les 15 minutes (900 000 ms) -// pour ne pas saturer l'API gratuite et se faire bloquer ! -setInterval(fetchRealWeather, 900000); - -// --- GESTION DU BOUTON DE TEST --- -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'); - } -}); \ No newline at end of file diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..a314bbc --- /dev/null +++ b/static/script.js @@ -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}¤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); + } +} + +// ================================================================= +// --- 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 + ' V'; + document.getElementById('current').innerHTML = c + ' A'; + document.getElementById('lux').innerHTML = l.toLocaleString() + ' Lux'; + document.getElementById('efficiency').innerHTML = efficiency + ' %'; + + // 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 = ' 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'); + } +}); \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..3e85bb7 --- /dev/null +++ b/static/style.css @@ -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; +} \ No newline at end of file diff --git a/style.css b/style.css deleted file mode 100644 index ea6e33e..0000000 --- a/style.css +++ /dev/null @@ -1,81 +0,0 @@ -/* --- 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 --- */ -.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; -} \ No newline at end of file diff --git a/templates/admin_dashboard.html b/templates/admin_dashboard.html new file mode 100644 index 0000000..cea1dde --- /dev/null +++ b/templates/admin_dashboard.html @@ -0,0 +1,283 @@ + + + + + + + EcoCharge — Dashboard Admin + + + + + +
+ + +
+
+ +
+

ECOCHARGE

+

Panneau d'administration

+
+
+ + 🚪 Déconnexion +
+ + +
+ +

Seuils d'alerte

+

Modifiez les valeurs puis cliquez sur "Sauvegarder".

+ + +
+ + +
+
+ +
+

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

+
+
+ + V +
+
+ +
+ + +
+
+ +
+

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

+
+
+ + °C +
+
+ +
+ + +
+
+ +
+

Alerte si la production solaire descend sous ce seuil.

+
+
+ + W +
+
+ + + +
+
+ +
+ + + + \ No newline at end of file diff --git a/index.html b/templates/index.html similarity index 50% rename from index.html rename to templates/index.html index 71f2787..c2749cb 100644 --- a/index.html +++ b/templates/index.html @@ -9,7 +9,7 @@ - + @@ -78,48 +78,104 @@
-
-
Production Solaire
-
-
- Espace Étudiant - 1 +
+
+ Production Solaire +
+ +
+ + + + + + + + + + + + +
+ 0 + W
+ +
+
+
Tension (Upv)
+
0 V
+
+
+
Courant (Ipv)
+
0 A
+
+
+
Luminosité
+
0 Lux
+
+
+
Rendement
+
0 %
+
+
+ +
+
+ Système connecté — MPPT actif +
-
-
État Batterie
-
-
- Espace Étudiant - 2 +
+
+ État Batterie +
+ +
+
+
+ --% + SoC +
+ +
+
+ Système Normal +
+
+
-
-
Administration
-
-
- Espace Étudiant - 4 -
+
+
+ Administration +
+ + +
@@ -127,7 +183,7 @@
- + \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..aa4a23c --- /dev/null +++ b/templates/login.html @@ -0,0 +1,174 @@ + + + + + + + EcoCharge — Connexion + + + + + +
+ + + + + + {% if error %} +
+ {{ error }} +
+ {% endif %} + + +
+ +
+ + +
+ +
+ + +
+ + + +
+ +
+ + + + \ No newline at end of file