Initial commit projet IoT Flask ESP32

This commit is contained in:
Safouane Bazzi
2026-03-23 17:04:58 +01:00
parent 17026018a9
commit 06597bd9d3
7 changed files with 397 additions and 112 deletions

View File

@@ -1,9 +1,7 @@
// --- 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');
@@ -14,21 +12,19 @@ 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 MAX_POWER = 100;
const gaugeCircle = document.querySelector('.gauge-fill circle');
const halfCircumference = 283 / 2;
// =================================================================
// --- GESTION DE LA VRAIE API MÉTÉO WEB (Open-Meteo) ---
// =================================================================
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" };
@@ -41,8 +37,8 @@ function getWeatherDetails(code) {
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 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);
@@ -55,31 +51,32 @@ async function fetchRealWeather() {
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) {
function updateBattery(percent, alertTextFromApi = null) {
batValEl.innerText = percent + "%";
let color = "#34d399";
let shadowColor = "rgba(52, 211, 153, 0.4)";
if (percent <= 20) {
if (alertTextFromApi && alertTextFromApi !== "none") {
color = "#ef4444";
shadowColor = "rgba(239, 68, 68, 0.6)";
batAlertText.innerText = "Tension Critique !";
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";
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 {
@@ -95,14 +92,12 @@ function updateBattery(percent) {
}
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";
const efficiency = (p > 0) ? ((p / MAX_POWER) * 100).toFixed(1) : "0.0";
document.getElementById('powerValue').textContent = p;
document.getElementById('voltage').innerHTML = v + ' <span class="stat-unit">V</span>';
@@ -110,7 +105,6 @@ function updateSolar(voltage, current, power, lux) {
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);
@@ -119,15 +113,182 @@ function updateSolar(voltage, current, power, lux) {
}
}
// =================================================================
// --- NOUVEAU : CONNEXION AU SERVEUR PYTHON (BASE DE DONNÉES) ---
// =================================================================
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 {
// On interroge l'URL magique de l'étudiant 4
const response = await fetch('/api/latest');
if (!response.ok) {
@@ -136,16 +297,13 @@ async function fetchDatabaseData() {
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));
updateBattery(Math.floor(data.battery_level), data.battery_alert);
}
// 3. Mise à jour du Solaire
updateSolar(
data.voltage_pv || 0,
data.current_pv || 0,
@@ -153,15 +311,12 @@ async function fetchDatabaseData() {
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";
@@ -174,21 +329,6 @@ function checkSystemStatus() {
}
}
// =================================================================
// --- 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) {
@@ -199,4 +339,16 @@ document.getElementById('btn-test').addEventListener('click', function () {
this.innerHTML = '<i class="bi bi-wifi"></i> 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);