diff --git a/data/index.html b/data/index.html index 625bea8..4c0c47e 100644 --- a/data/index.html +++ b/data/index.html @@ -74,20 +74,16 @@ margin-bottom: 20px; } - .error { - background: #fee; - color: #c33; - padding: 10px; + .status-bar { + background: #f0f0f0; + padding: 10px 15px; border-radius: 5px; - margin-bottom: 20px; - display: none; - text-align: center; - } - - .loading { - text-align: center; - color: #667eea; - font-style: italic; + margin-top: 20px; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12px; + color: #555; } .data-grid { @@ -187,17 +183,7 @@ font-size: 14px; opacity: 0.9; } - .status-bar { - background: #f0f0f0; - padding: 10px 15px; - border-radius: 5px; - margin-top: 20px; - display: flex; - justify-content: space-between; - align-items: center; - font-size: 12px; - color: #555; - } + .status-indicator { display: flex; align-items: center; @@ -210,6 +196,12 @@ background: #27ae60; animation: pulse 2s infinite; } + .status-dot.red { + background: #e74c3c; + } + .status-dot.orange { + background: #f39c12; + } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } @@ -251,13 +243,10 @@

☀️ Contrôleur Solaire pour Chauffe-Eau

-
-
Chargement des données...
- -
+
-
- Connecté +
+ Chargement...
🌅 --:-- @@ -290,7 +279,7 @@
--
%
-
0%
+
@@ -323,13 +312,29 @@ let currentMode = 0; // Mode par défaut (OFF) function showError(message) { - document.getElementById('error').textContent = message; - document.getElementById('error').style.display = 'block'; + const statusDot = document.getElementById('statusDot'); + const statusText = document.getElementById('statusText'); + statusDot.className = 'status-dot red'; + statusText.textContent = message; setTimeout(() => { - document.getElementById('error').style.display = 'none'; + updateStatusBar(); }, 5000); } + function updateStatusBar() { + const statusDot = document.getElementById('statusDot'); + const statusText = document.getElementById('statusText'); + + // Vérifier l'état d'erreur Enphase + if (window.enphaseConnectionError) { + statusDot.className = 'status-dot orange'; + statusText.textContent = 'Erreur connexion Enphase'; + } else { + statusDot.className = 'status-dot'; + statusText.textContent = 'Connecté'; + } + } + function updateModeDisplay() { // Mettre à jour l'affichage des boutons actifs document.querySelectorAll('.mode-btn').forEach(btn => { @@ -438,7 +443,6 @@ fetch('/api/data') .then(response => { if (response.status === 401) { - // Session expirée, rediriger vers login window.location.href = '/login'; return; } @@ -448,9 +452,10 @@ return response.json(); }) .then(data => { - if (!data) return; // Éviter l'erreur si redirection 401 - // Masquer le loading et afficher les données - document.getElementById('loading').style.display = 'none'; + if (!data) return; + // Mettre à jour le flag global d'erreur Enphase + window.enphaseConnectionError = !!data.enphase_connection_error; + updateStatusBar(); document.getElementById('dataDisplay').style.display = 'block'; // Mettre à jour la production solaire @@ -476,8 +481,7 @@ // Mettre à jour la barre de progression const progressBar = document.getElementById('heaterProgress'); progressBar.style.width = data.heater_power + '%'; - progressBar.textContent = data.heater_power + '%'; - + // Mettre à jour l'heure de dernière mise à jour const now = new Date(); const timeString = now.toLocaleTimeString('fr-FR'); diff --git a/data/settings.html b/data/settings.html index 3571ce4..951505c 100644 --- a/data/settings.html +++ b/data/settings.html @@ -126,29 +126,46 @@ background: #7f8c8d; } - .message { - padding: 15px; + .notification { + position: fixed; + top: 20px; + right: 20px; + padding: 15px 20px; border-radius: 5px; - margin-bottom: 20px; + box-shadow: 0 4px 12px rgba(0,0,0,0.2); + z-index: 1000; display: none; + min-width: 300px; + animation: slideIn 0.3s ease-out; + } + + @keyframes slideIn { + from { + transform: translateX(400px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } } - .message.success { + .notification.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; } - .message.error { + .notification.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } - .loading { - text-align: center; - color: #667eea; - font-style: italic; + .notification.info { + background: #e8f4f8; + color: #0c5460; + border: 1px solid #bee5eb; } @@ -156,8 +173,7 @@

⚙️ Configuration

-
- +
@@ -304,7 +320,7 @@
- +
@@ -318,24 +334,21 @@ // Charger les paramètres au démarrage window.addEventListener('DOMContentLoaded', loadSettings); - function showMessage(text, type) { - const messageDiv = document.getElementById('message'); - messageDiv.textContent = text; - messageDiv.className = 'message ' + type; - messageDiv.style.display = 'block'; + function showNotification(text, type = 'info') { + const notification = document.getElementById('notification'); + notification.textContent = text; + notification.className = 'notification ' + type; + notification.style.display = 'block'; setTimeout(() => { - messageDiv.style.display = 'none'; - }, 5000); + notification.style.display = 'none'; + }, 3000); } function loadSettings() { - const loading = document.getElementById('loading'); - loading.style.display = 'block'; fetch('/api/settings') .then(response => response.json()) .then(data => { - loading.style.display = 'none'; document.getElementById('username').value = data.auth.username || 'admin'; @@ -372,8 +385,7 @@ document.getElementById('confirm_password').value = ''; }) .catch(error => { - loading.style.display = 'none'; - showMessage('Erreur lors du chargement des paramètres', 'error'); + showNotification('Erreur lors du chargement des paramètres', 'error'); console.error('Erreur:', error); }); } @@ -387,17 +399,17 @@ // Validation du mot de passe if (newPassword && newPassword !== confirmPassword) { - showMessage('Les mots de passe ne correspondent pas', 'error'); + showNotification('Les mots de passe ne correspondent pas', 'error'); return; } if (newPassword && !currentPassword) { - showMessage('Le mot de passe actuel est requis pour changer le mot de passe', 'error'); + showNotification('Le mot de passe actuel est requis pour changer le mot de passe', 'error'); return; } if (newPassword && newPassword.length < 4) { - showMessage('Le nouveau mot de passe doit contenir au moins 4 caractères', 'error'); + showNotification('Le nouveau mot de passe doit contenir au moins 4 caractères', 'error'); return; } @@ -434,8 +446,7 @@ }; // Envoyer les données - const loading = document.getElementById('loading'); - loading.style.display = 'block'; + showNotification('Enregistrement en cours...', 'info'); fetch('/api/settings', { method: 'POST', @@ -445,7 +456,6 @@ body: JSON.stringify(settings) }) .then(response => { - loading.style.display = 'none'; if (response.ok) { return response.json(); } else { @@ -453,15 +463,13 @@ } }) .then(data => { - const wifiChanged = document.getElementById('wifi_ssid').value || document.getElementById('wifi_password').value; + showNotification(data.message || 'Paramètres sauvegardés avec succès!', 'success'); - if (wifiChanged) { - showMessage(data.message + ' - L\'ESP va redémarrer pour appliquer les changements WiFi.', 'success'); + // Le backend indique via data.restart si un redémarrage est nécessaire + if (data.restart) { setTimeout(() => { - showMessage('Redémarrage en cours... Reconnexion au nouveau réseau nécessaire.', 'error'); - }, 3000); - } else { - showMessage(data.message || 'Paramètres sauvegardés avec succès!', 'success'); + showNotification('L\'ESP va redémarrer pour appliquer les changements WiFi. Reconnexion nécessaire.', 'info'); + }, 2000); } // Si le mot de passe a changé, rediriger vers login après 2 secondes @@ -477,8 +485,7 @@ } }) .catch(error => { - loading.style.display = 'none'; - showMessage('Erreur lors de la sauvegarde des paramètres', 'error'); + showNotification('Erreur lors de la sauvegarde des paramètres', 'error'); console.error('Erreur:', error); }); }); diff --git a/src/main.cpp b/src/main.cpp index 3255ec5..16839f5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -55,6 +55,11 @@ float solarProduction = 0.0; // Production solaire en Watts float powerConsumption = 0.0; // Consommation électrique en Watts int heaterPower = 0; // Puissance de chauffe du chauffe-eau en % (0-100) float waterTemperature = 0.0; // Température de l'eau du chauffe-eau en °C +// Flag pour l'état de la connexion Enphase +bool enphaseConnectionError = false; + +// Configuration GPIO +#define HEATER_GPIO 12 // GPIO12 pour contrôle chauffe-eau // Configuration GPIO #define HEATER_GPIO 12 // GPIO12 pour contrôle chauffe-eau @@ -63,6 +68,9 @@ float waterTemperature = 0.0; // Température de l'eau du chauffe-eau en °C bool otaRebootPending = false; unsigned long otaRebootTime = 0; +// Variable pour le mode JOUR - mémoriser si la température max a été atteinte aujourd'hui +bool maxTempReachedToday = false; + // Variables pour la gestion PWM avec timer (période 1s) const unsigned long PWM_PERIOD = 1000; // Période de 1 seconde en ms volatile unsigned long pwmCycleCount = 0; // Compteur de cycles PWM @@ -409,50 +417,38 @@ void IRAM_ATTR onTimerISR() { void fetchEnphaseData() { if (enphaseGatewayIP.length() == 0 || enphaseToken.length() == 0) { Serial.println("Configuration Enphase manquante"); + enphaseConnectionError = true; return; } - WiFiClientSecure client; client.setInsecure(); // Ignorer la vérification du certificat SSL HTTPClient http; - String url = "https://" + enphaseGatewayIP + "/ivp/meters/reports/consumption"; - http.begin(client, url); http.addHeader("Accept", "application/json"); http.addHeader("Authorization", "Bearer " + enphaseToken); http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); // Suivre les redirections - int httpCode = http.GET(); - if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); - JsonDocument doc; DeserializationError error = deserializeJson(doc, payload); - if (!error && doc.is() && doc.size() >= 2) { - // Selon le code Python: - // json[0]['cumulative']['currW'] = total (consommation totale) - // json[1]['cumulative']['currW'] = net (réseau Enedis) - // panneaux = total - net (production solaire) - float total = doc[0]["cumulative"]["currW"].as(); float net = doc[1]["cumulative"]["currW"].as(); - solarProduction = round(total - net); // Production des panneaux arrondie powerConsumption = round(total); // Consommation totale arrondie - - Serial.printf("Enphase - Total: %.1fW, Panneaux: %.1fW, Net: %.1fW\n", total, solarProduction, net); + enphaseConnectionError = false; } else { Serial.println("Erreur parsing JSON Enphase"); + enphaseConnectionError = true; } } else { Serial.printf("Erreur HTTP Enphase: %d\n", httpCode); + enphaseConnectionError = true; } - http.end(); } @@ -482,6 +478,23 @@ void calculateHeaterPower() { isNightPeriod = (currentHour >= nightStartHour || currentHour < nightEndHour); } + // Vérifier si on est dans la période jour (entre lever et coucher du soleil) + bool isDayPeriod = false; + if (sunriseTime != "--:--" && sunsetTime != "--:--") { + // Parser les heures de lever et coucher + int sunriseHour = sunriseTime.substring(0, 2).toInt(); + int sunriseMinute = sunriseTime.substring(3, 5).toInt(); + int sunsetHour = sunsetTime.substring(0, 2).toInt(); + int sunsetMinute = sunsetTime.substring(3, 5).toInt(); + + // Convertir en minutes depuis minuit pour faciliter la comparaison + int currentTimeInMinutes = currentHour * 60 + currentMinute; + int sunriseTimeInMinutes = sunriseHour * 60 + sunriseMinute; + int sunsetTimeInMinutes = sunsetHour * 60 + sunsetMinute; + + isDayPeriod = (currentTimeInMinutes >= sunriseTimeInMinutes && currentTimeInMinutes <= sunsetTimeInMinutes); + } + // Calculer l'excédent solaire et ajuster le pourcentage de chauffe float solarExcess = solarProduction - powerConsumption; int calculatedHeaterPowerJour, calculatedHeaterPowerSoleil = 0; @@ -511,40 +524,50 @@ void calculateHeaterPower() { } } else if (modeSOLEIL || modeJOUR) { - if (modeJOUR) { - // Mode JOUR: calculer selon la formule + if (modeJOUR && isDayPeriod) { + // Mode JOUR: calculer selon la formule (uniquement pendant la journée) // calculatedHeaterPowerJour = 100 * CoeffJour * (max_water_temp - water_temperature) / (secondes_restantes) - // Calculer le temps actuel en secondes depuis minuit - int currentTimeInSeconds = currentHour * 3600 + currentMinute * 60 + currentSecond; - - // Calculer l'heure cible en secondes depuis minuit - int targetTimeInSeconds = targetMaxWaterTempHour * 3600; - - // Calculer les secondes restantes - int secondsRemaining = targetTimeInSeconds - currentTimeInSeconds; - - // // Si on est après l'heure cible ou à l'heure cible, calculer jusqu'au lendemain - // if (secondsRemaining < 0) { - // secondsRemaining = 86400 + secondsRemaining; // 86400 = 24h en secondes - // } - - float tempDiff = maxWaterTemp - waterTemperature; + // Vérifier si la température max a déjà été atteinte aujourd'hui + if (waterTemperature >= maxWaterTemp) { + maxTempReachedToday = true; + } - if (tempDiff > 0 && secondsRemaining > 0) { - calculatedHeaterPowerJour = (int)(100.0 * coeffJour * tempDiff / secondsRemaining); - if (calculatedHeaterPowerJour > 100) calculatedHeaterPowerJour = 100; - if (calculatedHeaterPowerJour < 0) calculatedHeaterPowerJour = 0; - - // Afficher avec les heures et minutes pour la lisibilité - // int hoursDisplay = secondsRemaining / 3600; - // int minutesDisplay = (secondsRemaining % 3600) / 60; - // Serial.printf("[JOUR] TempDiff: %.1f°C, Temps restant: %dh%02dm, Chauffe: %d%%\n", - // tempDiff, hoursDisplay, minutesDisplay, calculatedHeaterPowerJour); - } else { + if (maxTempReachedToday) { + // La température max a été atteinte aujourd'hui, ne pas chauffer calculatedHeaterPowerJour = 0; - // Serial.printf("[JOUR] Température atteinte ou dépassée: %.1f°C >= %.1f°C\n", - // waterTemperature, maxWaterTemp); + } else { + // Calculer le temps actuel en secondes depuis minuit + int currentTimeInSeconds = currentHour * 3600 + currentMinute * 60 + currentSecond; + + // Calculer l'heure cible en secondes depuis minuit + int targetTimeInSeconds = targetMaxWaterTempHour * 3600; + + // Calculer les secondes restantes + int secondsRemaining = targetTimeInSeconds - currentTimeInSeconds; + + // // Si on est après l'heure cible ou à l'heure cible, calculer jusqu'au lendemain + // if (secondsRemaining < 0) { + // secondsRemaining = 86400 + secondsRemaining; // 86400 = 24h en secondes + // } + + float tempDiff = maxWaterTemp - waterTemperature; + + if (tempDiff > 0 && secondsRemaining > 0) { + calculatedHeaterPowerJour = (int)(100.0 * coeffJour * tempDiff / secondsRemaining); + if (calculatedHeaterPowerJour > 100) calculatedHeaterPowerJour = 100; + if (calculatedHeaterPowerJour < 0) calculatedHeaterPowerJour = 0; + + // Afficher avec les heures et minutes pour la lisibilité + // int hoursDisplay = secondsRemaining / 3600; + // int minutesDisplay = (secondsRemaining % 3600) / 60; + // Serial.printf("[JOUR] TempDiff: %.1f°C, Temps restant: %dh%02dm, Chauffe: %d%%\n", + // tempDiff, hoursDisplay, minutesDisplay, calculatedHeaterPowerJour); + } else { + calculatedHeaterPowerJour = 0; + // Serial.printf("[JOUR] Température atteinte ou dépassée: %.1f°C >= %.1f°C\n", + // waterTemperature, maxWaterTemp); + } } } @@ -774,12 +797,17 @@ void setup() { // API POST /api/settings server.on("/api/settings", HTTP_POST, [](AsyncWebServerRequest *request){ + Serial.println("[SETTINGS] POST /api/settings appelé"); + // Handler appelé quand la requête est complète if (!checkAuthentication(request)) { + Serial.println("[SETTINGS] Authentification échouée"); request->send(401, "text/plain", "Non autorisé"); return; } + Serial.println("[SETTINGS] Authentification OK"); + // Récupérer le body depuis _tempObject if (request->_tempObject == NULL) { Serial.println("[SETTINGS] Erreur: Pas de body reçu"); @@ -852,16 +880,22 @@ void setup() { if (requestDoc["wifi"]["ssid"] && requestDoc["wifi"]["ssid"].as().length() > 0) { String newSSID = requestDoc["wifi"]["ssid"].as(); - configDoc["wifi"]["ssid"] = newSSID; - wifiSSID = newSSID; - wifiChanged = true; + if (newSSID != wifiSSID) { + configDoc["wifi"]["ssid"] = newSSID; + wifiSSID = newSSID; + wifiChanged = true; + Serial.println("[SETTINGS] SSID modifié, redémarrage nécessaire"); + } } if (requestDoc["wifi"]["password"] && requestDoc["wifi"]["password"].as().length() > 0) { String newWifiPass = requestDoc["wifi"]["password"].as(); - configDoc["wifi"]["password"] = newWifiPass; - wifiPassword = newWifiPass; - wifiChanged = true; + if (newWifiPass != wifiPassword) { + configDoc["wifi"]["password"] = newWifiPass; + wifiPassword = newWifiPass; + wifiChanged = true; + Serial.println("[SETTINGS] Mot de passe WiFi modifié, redémarrage nécessaire"); + } } if (requestDoc["wifi"]["ap_password"] && requestDoc["wifi"]["ap_password"].as().length() > 0) { @@ -988,6 +1022,7 @@ void setup() { JsonDocument responseDoc; responseDoc["success"] = true; + responseDoc["restart"] = wifiChanged; if (wifiChanged) { responseDoc["message"] = "Paramètres sauvegardés. Redémarrage pour appliquer les changements WiFi..."; @@ -1013,12 +1048,21 @@ void setup() { if (index == 0) { // Premier chunk: créer le buffer Serial.printf("[SETTINGS] Réception body: %d bytes total\n", total); + if (total > 8192) { + Serial.println("[SETTINGS] ERREUR: Body trop grand!"); + return; + } request->_tempObject = malloc(total + 1); + if (request->_tempObject == NULL) { + Serial.println("[SETTINGS] ERREUR: Échec allocation mémoire!"); + return; + } } if (request->_tempObject != NULL) { // Copier ce chunk memcpy((uint8_t*)request->_tempObject + index, data, len); + Serial.printf("[SETTINGS] Chunk reçu: index=%d len=%d total=%d\n", index, len, total); if (index + len == total) { // Dernier chunk: terminer la chaîne @@ -1043,7 +1087,7 @@ void setup() { doc["sunrise_time"] = sunriseTime; doc["sunset_time"] = sunsetTime; doc["timestamp"] = millis(); - + doc["enphase_connection_error"] = enphaseConnectionError; String response; serializeJson(doc, response); request->send(200, "application/json", response); @@ -1412,5 +1456,8 @@ void loop() { if (timeinfo.tm_mday != lastDay) { lastDay = timeinfo.tm_mday; calculateSunriseSunset(); + // Réinitialiser le flag de température max atteinte pour le nouveau jour + maxTempReachedToday = false; + Serial.println("[JOUR] Nouveau jour - réinitialisation du flag température max"); } } \ No newline at end of file