@@ -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
-
-
Chargement...
+
@@ -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