Browse Source

Mise à jour notif dans config

Amélioration de la barre d'info sur la page index
Mise en place d'un flag de redémarrage si modif wifi
Modification du mode JOUR avec intégration d'un flag si température de consigne atteinte
master
scayac 2 months ago
parent
commit
afccd66ace
  1. 84
      data/index.html
  2. 85
      data/settings.html
  3. 153
      src/main.cpp

84
data/index.html

@ -74,20 +74,16 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -251,13 +243,10 @@
<h1> Contrôleur Solaire pour Chauffe-Eau</h1>
<div id="error" class="error"></div>
<div id="loading" class="loading">Chargement des données...</div>
<div class="status-bar">
<div id="statusBar" class="status-bar">
<div class="status-indicator">
<div class="status-dot"></div>
<span>Connecté</span>
<div class="status-dot" id="statusDot"></div>
<span id="statusText">Chargement...</span>
</div>
<div style="display: flex; gap: 18px; align-items: center;">
<span id="sunriseStatus">🌅 --:--</span>
@ -290,7 +279,7 @@ @@ -290,7 +279,7 @@
<div class="data-value" id="heaterValue">--</div>
<div class="data-unit">%</div>
<div class="progress-bar">
<div class="progress-fill" id="heaterProgress" style="width: 0%">0%</div>
<div class="progress-fill" id="heaterProgress" style="width: 0%"></div>
</div>
</div>
@ -323,13 +312,29 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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');

85
data/settings.html

@ -126,29 +126,46 @@ @@ -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;
}
</style>
</head>
@ -156,8 +173,7 @@ @@ -156,8 +173,7 @@
<div class="container">
<h1> Configuration</h1>
<div id="message" class="message"></div>
<div id="loading" class="loading" style="display:none;">Chargement...</div>
<div id="notification" class="notification"></div>
<form id="settingsForm">
<!-- Section Authentification -->
@ -304,7 +320,7 @@ @@ -304,7 +320,7 @@
<!-- Boutons d'action -->
<div style="margin-top: 30px;">
<button type="submit">💾 Enregistrer les modifications</button>
<button type="button" class="secondary" onclick="loadSettings()">🔄 Recharger</button>
<button type="button" onclick="loadSettings()">🔄 Recharger</button>
</div>
</form>
@ -318,24 +334,21 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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);
});
});

153
src/main.cpp

@ -55,6 +55,11 @@ float solarProduction = 0.0; // Production solaire en Watts @@ -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 @@ -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() { @@ -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<JsonArray>() && 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>();
float net = doc[1]["cumulative"]["currW"].as<float>();
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() { @@ -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() { @@ -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() { @@ -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() { @@ -852,16 +880,22 @@ void setup() {
if (requestDoc["wifi"]["ssid"] && requestDoc["wifi"]["ssid"].as<String>().length() > 0) {
String newSSID = requestDoc["wifi"]["ssid"].as<String>();
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<String>().length() > 0) {
String newWifiPass = requestDoc["wifi"]["password"].as<String>();
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<String>().length() > 0) {
@ -988,6 +1022,7 @@ void setup() { @@ -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() { @@ -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() { @@ -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() { @@ -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");
}
}
Loading…
Cancel
Save