F1ATB forum
Connecter compteur eau impulsion - Version imprimable

+- F1ATB forum (https://f1atb.fr/forum_f1atb)
+-- Forum : Forum de F1ATB (https://f1atb.fr/forum_f1atb/forum-3.html)
+--- Forum : Domotique (https://f1atb.fr/forum_f1atb/forum-6.html)
+--- Sujet : Connecter compteur eau impulsion (/thread-1984.html)

Pages : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25


RE: Connecter compteur eau impulsion - Lolo69 - 19-12-2025

Patientes un peu, la fin de la trêve hivernale d Arduino.
En attendant le problème est il systématique et repétable ou plutôt aléatoire ?


RE: Connecter compteur eau impulsion - Lolo69 - 19-12-2025

J ai repris plusieurs points de blocage possibles
Conserve bien ta version actuelle avant de tester la suivante

Code :
//Version 2.05 19/12/25 - Correction blocage SMTP & Network
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <LittleFS.h>
#include <WiFiClientSecure.h>
#include <ESP_Mail_Client.h>
#include <Update.h>

#define WIFI_CONNECT_TIMEOUT_MS 10000
#define AP_SSID "ESP32_Config"
#define AP_PASSWORD ""

const char* CONFIG_FILE = "/config.txt";

// Valeurs par défaut
String cfg_ssid = "ton SSID";
String cfg_password = "ton mot de passe wifi";
String cfg_smtp = "smtp.orange.fr";
String cfg_email = "tonemail@orange.fr";
String cfg_smtp_pwd = "ton mot de passe Mail";
String cfg_destinataires = "dest1@free.fr;dest2@orange.fr";
int cfg_smtp_port = 465;
int cfg_gpio_number = 2;
int cfg_gpio_number2 = 23;
String cfg_trigger_type = "inondation";
bool cfg_invert_gpio1 = false;
bool cfg_invert_gpio2 = false;

// Réseau statique
String cfg_local_ip = "192.168.1.119";
String cfg_gateway = "192.168.1.1";
String cfg_subnet = "255.255.255.0";
String cfg_primaryDNS = "192.168.1.1";
String cfg_secondaryDNS = "8.8.4.4";

// Admin auth
String admin_user = "admin";
String admin_pwd = "admin";

// Objets globaux
WebServer server(80);
// ⭐ MODIFICATION : On ne garde plus le SMTP en global pour libérer les ressources

// ⭐ AJOUT : Flags pour envoi asynchrone
bool triggerMailPending = false;
String pendingType = "";

// --- Helpers IP ---
IPAddress ipFromString(const String &s) {
    IPAddress ip(0,0,0,0);
    int parts[4] = {0,0,0,0}, idx = 0;
    String tmp = s;
    tmp.trim();
    int start = 0;
    for (int i=0; i<4; ++i) {
        int pos = tmp.indexOf('.', start);
        String part = (pos==-1) ? tmp.substring(start) : tmp.substring(start,pos);
        part.trim();
        parts[idx++] = part.toInt();
        if (pos==-1) break;
        start = pos+1;
    }
    ip = IPAddress(parts[0], parts[1], parts[2], parts[3]);
    return ip;
}

// --- LittleFS ---
void saveConfig() {
    File f = LittleFS.open(CONFIG_FILE, "w");
    if (!f) { Serial.println("Erreur ouverture fichier config"); return; }
    f.printf("ssid=%s\n", cfg_ssid.c_str());
    f.printf("password=%s\n", cfg_password.c_str());
    f.printf("smtp=%s\n", cfg_smtp.c_str());
    f.printf("email=%s\n", cfg_email.c_str());
    f.printf("smtp_pwd=%s\n", cfg_smtp_pwd.c_str());
    f.printf("destinataires=%s\n", cfg_destinataires.c_str());
    f.printf("smtp_port=%d\n", cfg_smtp_port);
    f.printf("gpio_number=%d\n", cfg_gpio_number);
    f.printf("gpio_number2=%d\n", cfg_gpio_number2);
    f.printf("invert_gpio1=%d\n", cfg_invert_gpio1 ? 1 : 0);
    f.printf("invert_gpio2=%d\n", cfg_invert_gpio2 ? 1 : 0);
    f.printf("trigger_type=%s\n", cfg_trigger_type.c_str());
    f.printf("local_ip=%s\n", cfg_local_ip.c_str());
    f.printf("gateway=%s\n", cfg_gateway.c_str());
    f.printf("subnet=%s\n", cfg_subnet.c_str());
    f.printf("primaryDNS=%s\n", cfg_primaryDNS.c_str());
    f.printf("secondaryDNS=%s\n", cfg_secondaryDNS.c_str());
    f.printf("admin_user=%s\n", admin_user.c_str());
    f.printf("admin_pwd=%s\n", admin_pwd.c_str());
    f.close();
    Serial.println("Config sauvegardée dans LittleFS");
}

void loadConfig() {
    if (!LittleFS.exists(CONFIG_FILE)) return;
    File f = LittleFS.open(CONFIG_FILE,"r");
    if (!f) return;
    while(f.available()) {
        String line = f.readStringUntil('\n');
        line.trim();
        if (line.length() == 0) continue;
        int eq = line.indexOf('=');
        if (eq == -1) continue;
        String key = line.substring(0, eq), val = line.substring(eq+1);
        key.trim(); val.trim();
        if(key=="ssid") cfg_ssid=val;
        else if(key=="password") cfg_password=val;
        else if(key=="smtp") cfg_smtp=val;
        else if(key=="email") cfg_email=val;
        else if(key=="smtp_pwd") cfg_smtp_pwd=val;
        else if(key=="destinataires") cfg_destinataires=val;
        else if(key=="smtp_port") cfg_smtp_port=val.toInt();
        else if(key=="gpio_number") cfg_gpio_number=val.toInt();
        else if(key=="gpio_number2") cfg_gpio_number2=val.toInt();
        else if(key=="invert_gpio1") cfg_invert_gpio1=(val.toInt()!=0);
        else if(key=="invert_gpio2") cfg_invert_gpio2=(val.toInt()!=0);
        else if(key=="trigger_type") cfg_trigger_type=val;
        else if(key=="local_ip") cfg_local_ip = val;
        else if(key=="gateway") cfg_gateway = val;
        else if(key=="subnet") cfg_subnet = val;
        else if(key=="primaryDNS") cfg_primaryDNS = val;
        else if(key=="secondaryDNS") cfg_secondaryDNS = val;
        else if(key=="admin_user") admin_user = val;
        else if(key=="admin_pwd") admin_pwd = val;
    }
    f.close();
}

void setGPIO(int gpio, bool on, bool invert) {
    digitalWrite(gpio, invert ? (on ? LOW : HIGH) : (on ? HIGH : LOW));
}

// --- SMTP Core ---
void addAllRecipients(SMTP_Message &message, const String &liste) {
    String s=liste;
    int start=0, pos=0;
    while((pos=s.indexOf(';',start))!=-1) {
        String email=s.substring(start,pos);
        email.trim();
        if(email.length()>0) message.addRecipient("Destinataire",email.c_str());
        start=pos+1;
    }
    String email=s.substring(start);
    email.trim();
    if(email.length()>0) message.addRecipient("Destinataire",email.c_str());
}

// ⭐ MODIFICATION : Fonction d'envoi isolée pour être appelée hors du flux Web
void sendEmailAction(String type) {
    SMTPSession smtpLocal; // Instance locale
    ESP_Mail_Session session;
    session.server.host_name=cfg_smtp.c_str();
    session.server.port=cfg_smtp_port;
    session.login.email=cfg_email.c_str();
    session.login.password=cfg_smtp_pwd.c_str();

    SMTP_Message message;
    message.sender.name="ECODEVICE";
    message.sender.email=cfg_email.c_str();
    addAllRecipients(message,cfg_destinataires);
    message.subject="Alerte : "+type;
    message.text.content="Alerte declenchee sur ESP32.";

    if(!smtpLocal.connect(&session)) {
        Serial.println("Erreur connexion SMTP");
        return;
    }
    if(!MailClient.sendMail(&smtpLocal,&message)) Serial.println("Erreur envoi email");
    else Serial.println("Email envoyé avec succès");
    smtpLocal.closeSession();
}

// --- Page root (Trigger) ---
void handleRoot() {
    if(server.hasArg("type")) {
        pendingType = server.arg("type");
        // Traitement immédiat des sorties
        if(pendingType.equalsIgnoreCase(cfg_trigger_type)){
            setGPIO(cfg_gpio_number, false, cfg_invert_gpio1);
            setGPIO(cfg_gpio_number2, false, cfg_invert_gpio2);
            Serial.println("Trigger : GPIO mis à 0");
        }
        // ⭐ MODIFICATION : On lève le flag et on répond vite au client
        triggerMailPending = true;
    }
    server.send(200,"text/html","<html><body>Trigger reçu</body></html>");
}

bool checkAuth() {
    if(!server.authenticate(admin_user.c_str(),admin_pwd.c_str())) {
        server.requestAuthentication();
        return false;
    }
    return true;
}

String htmlEscape(const String &s){
    String r=s; r.replace("&","&amp;"); r.replace("<","&lt;"); r.replace(">","&gt;"); r.replace("\"","&quot;"); r.replace("'","'");
    return r;
}

// --- Page /admin ---
void handleAdmin() {
    if(!checkAuth()) return;
   
    // ⭐ MODIFICATION : Calcul de l'état "Logique" pour l'interface
    bool s1 = digitalRead(cfg_gpio_number);
    bool stateLogique1 = cfg_invert_gpio1 ? !s1 : s1;
    bool s2 = digitalRead(cfg_gpio_number2);
    bool stateLogique2 = cfg_invert_gpio2 ? !s2 : s2;

    String page = "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1'>"
                  "<style>body{font-family:Arial;padding:12px;} label{display:block;margin-top:8px;} input[type=text], input[type=password], textarea{width:100%;padding:8px;box-sizing:border-box;} .btn{display:inline-block;padding:8px 12px;margin-top:10px;background:#2196F3;color:#fff;text-decoration:none;border-radius:6px;border:none;}</style>"
                  "<title>Admin</title></head><body>"
                  "<h2>Paramètres</h2>"
                  "<h3>GPIO1 : <span id='gpio_state'>" + String(stateLogique1?"ON":"OFF") + "</span></h3>"
                  "<button onclick=\"toggleGPIO('on')\">Ouvrir</button> <button onclick=\"toggleGPIO('off')\">Fermer</button>"
                  "<h3>GPIO2 : <span id='gpio_state2'>" + String(stateLogique2?"ON":"OFF") + "</span></h3>"
                  "<form method='POST' action='/save'>"
                  "<label>Trigger Type<input name='trigger_type' type='text' value='"+htmlEscape(cfg_trigger_type)+"'></label>"
                  "<label>GPIO Principale<input name='gpio_number' type='text' value='"+String(cfg_gpio_number)+"'></label>"
                  "<label>GPIO Secondaire<input name='gpio_number2' type='text' value='"+String(cfg_gpio_number2)+"'></label>"
                  "<label>Inverser G1<input type='checkbox' name='invert_gpio1' "+(cfg_invert_gpio1?"checked":"")+"></label>"
                  "<label>Inverser G2<input type='checkbox' name='invert_gpio2' "+(cfg_invert_gpio2?"checked":"")+"></label>"
                  "<h3>WiFi & Réseau</h3>"
                  "<label>SSID<input name='ssid' type='text' value='"+htmlEscape(cfg_ssid)+"'></label>"
                  "<label>Pass<input name='password' type='password'></label>"
                  "<label>IP Statique<input name='local_ip' type='text' value='"+htmlEscape(cfg_local_ip)+"'></label>"
                  "<h3>SMTP</h3>"
                  "<label>Host<input name='smtp' type='text' value='"+htmlEscape(cfg_smtp)+"'></label>"
                  "<label>Email<input name='email' type='text' value='"+htmlEscape(cfg_email)+"'></label>"
                  "<label>Pass SMTP<input name='smtp_pwd' type='password'></label>"
                  "<button class='btn' type='submit'>Sauvegarder</button>"
                  "</form>"
                  "<script>"
                  "function toggleGPIO(a){"
                  "fetch('/gpio?action='+a+'&json=1').then(r=>r.json()).then(d=>{"
                  "document.getElementById('gpio_state').innerText=d.state?'ON':'OFF';"
                  "document.getElementById('gpio_state2').innerText=d.state?'ON':'OFF';"
                  "});}"
                  "setInterval(()=>{ fetch('/gpio?json=1').then(r=>r.json()).then(d=>{"
                  "document.getElementById('gpio_state').innerText=d.state?'ON':'OFF';"
                  "document.getElementById('gpio_state2').innerText=d.state?'ON':'OFF';"
                  "}).catch(e=>{}); }, 3000);" // ⭐ MODIFICATION : Intervalle à 3s
                  "</script></body></html>";
    server.send(200,"text/html",page);
}

void handleSave() {
    if(!checkAuth()) return;
    if(server.hasArg("ssid")) cfg_ssid = server.arg("ssid");
    if(server.hasArg("password") && server.arg("password")!="") cfg_password = server.arg("password");
    if(server.hasArg("gpio_number")) cfg_gpio_number = server.arg("gpio_number").toInt();
    if(server.hasArg("gpio_number2")) cfg_gpio_number2 = server.arg("gpio_number2").toInt();
    if(server.hasArg("trigger_type")) cfg_trigger_type = server.arg("trigger_type");
    if(server.hasArg("smtp")) cfg_smtp = server.arg("smtp");
    if(server.hasArg("email")) cfg_email = server.arg("email");
    if(server.hasArg("smtp_pwd") && server.arg("smtp_pwd")!="") cfg_smtp_pwd = server.arg("smtp_pwd");
    cfg_invert_gpio1 = server.hasArg("invert_gpio1");
    cfg_invert_gpio2 = server.hasArg("invert_gpio2");
    saveConfig();
    server.send(200,"text/html","Sauvegardé. Reboot...");
    delay(500); ESP.restart();
}

void handleGPIO() {
    if(!checkAuth()) return;
    if(server.hasArg("action")) {
        String action = server.arg("action");
        if(action == "on") { setGPIO(cfg_gpio_number, true, cfg_invert_gpio1); setGPIO(cfg_gpio_number2, true, cfg_invert_gpio2); }
        else if(action == "off") { setGPIO(cfg_gpio_number, false, cfg_invert_gpio1); setGPIO(cfg_gpio_number2, false, cfg_invert_gpio2); }
    }
    // ⭐ MODIFICATION : On renvoie l'état "Logique" au JSON pour que le bouton soit raccord avec l'interface
    bool s = digitalRead(cfg_gpio_number);
    bool logicState = cfg_invert_gpio1 ? !s : s;
    if(server.hasArg("json")) {
        server.send(200,"application/json","{\"state\":"+String(logicState)+"}");
        return;
    }
    server.send(200,"text/html","OK");
}

void handleFirmwareUpload() {
    if (!checkAuth()) return;
    HTTPUpload& upload = server.upload();
    if (upload.status == UPLOAD_FILE_START) {
        if (!Update.begin(UPDATE_SIZE_UNKNOWN)) Update.printError(Serial);
    } else if (upload.status == UPLOAD_FILE_WRITE) {
        if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) Update.printError(Serial);
    } else if (upload.status == UPLOAD_FILE_END) {
        if (!Update.end(true)) Update.printError(Serial);
    }
}

void handleUpdatePage() {
    if (!checkAuth()) return;
    server.send(200, "text/html", Update.hasError()?"Error":"Success. Rebooting...");
    delay(500); ESP.restart();
}

void startAPMode() {
    WiFi.mode(WIFI_AP_STA);
    WiFi.softAP(AP_SSID, AP_PASSWORD);
}

bool attemptWiFiConnect() {
    WiFi.mode(WIFI_STA);
    WiFi.begin(cfg_ssid.c_str(), cfg_password.c_str());
    unsigned long start = millis();
    while(WiFi.status() != WL_CONNECTED && (millis()-start) < WIFI_CONNECT_TIMEOUT_MS) { delay(200); }
    return (WiFi.status() == WL_CONNECTED);
}

void checkWiFiReconnect() {
    if (WiFi.status() != WL_CONNECTED) {
        static unsigned long lastAttempt = 0;
        if (millis() - lastAttempt > 15000) {
            lastAttempt = millis();
            WiFi.begin(cfg_ssid.c_str(), cfg_password.c_str());
        }
    }
}

void setupRoutes() {
    server.on("/", handleAdmin);
    server.on("/actions", handleRoot);
    server.on("/save", HTTP_POST, handleSave);
    server.on("/gpio", handleGPIO);
    server.on("/update", HTTP_POST, handleUpdatePage, handleFirmwareUpload);
    server.begin();
}

void setup() {
    Serial.begin(115200);
    if(!LittleFS.begin(true)) Serial.println("LittleFS Error");
    loadConfig();
    pinMode(cfg_gpio_number, OUTPUT);
    pinMode(cfg_gpio_number2, OUTPUT);
    setGPIO(cfg_gpio_number, true, cfg_invert_gpio1);
    setGPIO(cfg_gpio_number2, true, cfg_invert_gpio2);

    if(cfg_local_ip.length() > 5){
        WiFi.config(ipFromString(cfg_local_ip), ipFromString(cfg_gateway), ipFromString(cfg_subnet), ipFromString(cfg_primaryDNS));
    }

    if(!attemptWiFiConnect()) startAPMode();
    setupRoutes();
}

void loop() {
    server.handleClient();
    checkWiFiReconnect();

    // ⭐ MODIFICATION : Gestion de l'envoi différé
    if (triggerMailPending) {
        Serial.println("Traitement email en arrière-plan...");
        sendEmailAction(pendingType);
        triggerMailPending = false;
        Serial.println("Retour à la boucle principale.");
    }
}



RE: Connecter compteur eau impulsion - tupolev89 - 19-12-2025

(19-12-2025, 12:01 PM)Lolo69 a écrit : J ai repris plusieurs points de blocage possibles
Conserve bien ta version actuelle avant de tester la suivante

Code :
//Version 2.05 19/12/25 - Correction blocage SMTP & Network
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <LittleFS.h>
#include <WiFiClientSecure.h>
#include <ESP_Mail_Client.h>
#include <Update.h>

#define WIFI_CONNECT_TIMEOUT_MS 10000
#define AP_SSID "ESP32_Config"
#define AP_PASSWORD ""

const char* CONFIG_FILE = "/config.txt";

// Valeurs par défaut
String cfg_ssid = "ton SSID";
String cfg_password = "ton mot de passe wifi";
String cfg_smtp = "smtp.orange.fr";
String cfg_email = "tonemail@orange.fr";
String cfg_smtp_pwd = "ton mot de passe Mail";
String cfg_destinataires = "dest1@free.fr;dest2@orange.fr";
int cfg_smtp_port = 465;
int cfg_gpio_number = 2;
int cfg_gpio_number2 = 23;
String cfg_trigger_type = "inondation";
bool cfg_invert_gpio1 = false;
bool cfg_invert_gpio2 = false;

// Réseau statique
String cfg_local_ip = "192.168.1.119";
String cfg_gateway = "192.168.1.1";
String cfg_subnet = "255.255.255.0";
String cfg_primaryDNS = "192.168.1.1";
String cfg_secondaryDNS = "8.8.4.4";

// Admin auth
String admin_user = "admin";
String admin_pwd = "admin";

// Objets globaux
WebServer server(80);
// ⭐ MODIFICATION : On ne garde plus le SMTP en global pour libérer les ressources

// ⭐ AJOUT : Flags pour envoi asynchrone
bool triggerMailPending = false;
String pendingType = "";

// --- Helpers IP ---
IPAddress ipFromString(const String &s) {
    IPAddress ip(0,0,0,0);
    int parts[4] = {0,0,0,0}, idx = 0;
    String tmp = s;
    tmp.trim();
    int start = 0;
    for (int i=0; i<4; ++i) {
        int pos = tmp.indexOf('.', start);
        String part = (pos==-1) ? tmp.substring(start) : tmp.substring(start,pos);
        part.trim();
        parts[idx++] = part.toInt();
        if (pos==-1) break;
        start = pos+1;
    }
    ip = IPAddress(parts[0], parts[1], parts[2], parts[3]);
    return ip;
}

// --- LittleFS ---
void saveConfig() {
    File f = LittleFS.open(CONFIG_FILE, "w");
    if (!f) { Serial.println("Erreur ouverture fichier config"); return; }
    f.printf("ssid=%s\n", cfg_ssid.c_str());
    f.printf("password=%s\n", cfg_password.c_str());
    f.printf("smtp=%s\n", cfg_smtp.c_str());
    f.printf("email=%s\n", cfg_email.c_str());
    f.printf("smtp_pwd=%s\n", cfg_smtp_pwd.c_str());
    f.printf("destinataires=%s\n", cfg_destinataires.c_str());
    f.printf("smtp_port=%d\n", cfg_smtp_port);
    f.printf("gpio_number=%d\n", cfg_gpio_number);
    f.printf("gpio_number2=%d\n", cfg_gpio_number2);
    f.printf("invert_gpio1=%d\n", cfg_invert_gpio1 ? 1 : 0);
    f.printf("invert_gpio2=%d\n", cfg_invert_gpio2 ? 1 : 0);
    f.printf("trigger_type=%s\n", cfg_trigger_type.c_str());
    f.printf("local_ip=%s\n", cfg_local_ip.c_str());
    f.printf("gateway=%s\n", cfg_gateway.c_str());
    f.printf("subnet=%s\n", cfg_subnet.c_str());
    f.printf("primaryDNS=%s\n", cfg_primaryDNS.c_str());
    f.printf("secondaryDNS=%s\n", cfg_secondaryDNS.c_str());
    f.printf("admin_user=%s\n", admin_user.c_str());
    f.printf("admin_pwd=%s\n", admin_pwd.c_str());
    f.close();
    Serial.println("Config sauvegardée dans LittleFS");
}

void loadConfig() {
    if (!LittleFS.exists(CONFIG_FILE)) return;
    File f = LittleFS.open(CONFIG_FILE,"r");
    if (!f) return;
    while(f.available()) {
        String line = f.readStringUntil('\n');
        line.trim();
        if (line.length() == 0) continue;
        int eq = line.indexOf('=');
        if (eq == -1) continue;
        String key = line.substring(0, eq), val = line.substring(eq+1);
        key.trim(); val.trim();
        if(key=="ssid") cfg_ssid=val;
        else if(key=="password") cfg_password=val;
        else if(key=="smtp") cfg_smtp=val;
        else if(key=="email") cfg_email=val;
        else if(key=="smtp_pwd") cfg_smtp_pwd=val;
        else if(key=="destinataires") cfg_destinataires=val;
        else if(key=="smtp_port") cfg_smtp_port=val.toInt();
        else if(key=="gpio_number") cfg_gpio_number=val.toInt();
        else if(key=="gpio_number2") cfg_gpio_number2=val.toInt();
        else if(key=="invert_gpio1") cfg_invert_gpio1=(val.toInt()!=0);
        else if(key=="invert_gpio2") cfg_invert_gpio2=(val.toInt()!=0);
        else if(key=="trigger_type") cfg_trigger_type=val;
        else if(key=="local_ip") cfg_local_ip = val;
        else if(key=="gateway") cfg_gateway = val;
        else if(key=="subnet") cfg_subnet = val;
        else if(key=="primaryDNS") cfg_primaryDNS = val;
        else if(key=="secondaryDNS") cfg_secondaryDNS = val;
        else if(key=="admin_user") admin_user = val;
        else if(key=="admin_pwd") admin_pwd = val;
    }
    f.close();
}

void setGPIO(int gpio, bool on, bool invert) {
    digitalWrite(gpio, invert ? (on ? LOW : HIGH) : (on ? HIGH : LOW));
}

// --- SMTP Core ---
void addAllRecipients(SMTP_Message &message, const String &liste) {
    String s=liste;
    int start=0, pos=0;
    while((pos=s.indexOf(';',start))!=-1) {
        String email=s.substring(start,pos);
        email.trim();
        if(email.length()>0) message.addRecipient("Destinataire",email.c_str());
        start=pos+1;
    }
    String email=s.substring(start);
    email.trim();
    if(email.length()>0) message.addRecipient("Destinataire",email.c_str());
}

// ⭐ MODIFICATION : Fonction d'envoi isolée pour être appelée hors du flux Web
void sendEmailAction(String type) {
    SMTPSession smtpLocal; // Instance locale
    ESP_Mail_Session session;
    session.server.host_name=cfg_smtp.c_str();
    session.server.port=cfg_smtp_port;
    session.login.email=cfg_email.c_str();
    session.login.password=cfg_smtp_pwd.c_str();

    SMTP_Message message;
    message.sender.name="ECODEVICE";
    message.sender.email=cfg_email.c_str();
    addAllRecipients(message,cfg_destinataires);
    message.subject="Alerte : "+type;
    message.text.content="Alerte declenchee sur ESP32.";

    if(!smtpLocal.connect(&session)) {
        Serial.println("Erreur connexion SMTP");
        return;
    }
    if(!MailClient.sendMail(&smtpLocal,&message)) Serial.println("Erreur envoi email");
    else Serial.println("Email envoyé avec succès");
    smtpLocal.closeSession();
}

// --- Page root (Trigger) ---
void handleRoot() {
    if(server.hasArg("type")) {
        pendingType = server.arg("type");
        // Traitement immédiat des sorties
        if(pendingType.equalsIgnoreCase(cfg_trigger_type)){
            setGPIO(cfg_gpio_number, false, cfg_invert_gpio1);
            setGPIO(cfg_gpio_number2, false, cfg_invert_gpio2);
            Serial.println("Trigger : GPIO mis à 0");
        }
        // ⭐ MODIFICATION : On lève le flag et on répond vite au client
        triggerMailPending = true;
    }
    server.send(200,"text/html","<html><body>Trigger reçu</body></html>");
}

bool checkAuth() {
    if(!server.authenticate(admin_user.c_str(),admin_pwd.c_str())) {
        server.requestAuthentication();
        return false;
    }
    return true;
}

String htmlEscape(const String &s){
    String r=s; r.replace("&","&amp;"); r.replace("<","&lt;"); r.replace(">","&gt;"); r.replace("\"","&quot;"); r.replace("'","'");
    return r;
}

// --- Page /admin ---
void handleAdmin() {
    if(!checkAuth()) return;
  
    // ⭐ MODIFICATION : Calcul de l'état "Logique" pour l'interface
    bool s1 = digitalRead(cfg_gpio_number);
    bool stateLogique1 = cfg_invert_gpio1 ? !s1 : s1;
    bool s2 = digitalRead(cfg_gpio_number2);
    bool stateLogique2 = cfg_invert_gpio2 ? !s2 : s2;

    String page = "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1'>"
                  "<style>body{font-family:Arial;padding:12px;} label{display:block;margin-top:8px;} input[type=text], input[type=password], textarea{width:100%;padding:8px;box-sizing:border-box;} .btn{display:inline-block;padding:8px 12px;margin-top:10px;background:#2196F3;color:#fff;text-decoration:none;border-radius:6px;border:none;}</style>"
                  "<title>Admin</title></head><body>"
                  "<h2>Paramètres</h2>"
                  "<h3>GPIO1 : <span id='gpio_state'>" + String(stateLogique1?"ON":"OFF") + "</span></h3>"
                  "<button onclick=\"toggleGPIO('on')\">Ouvrir</button> <button onclick=\"toggleGPIO('off')\">Fermer</button>"
                  "<h3>GPIO2 : <span id='gpio_state2'>" + String(stateLogique2?"ON":"OFF") + "</span></h3>"
                  "<form method='POST' action='/save'>"
                  "<label>Trigger Type<input name='trigger_type' type='text' value='"+htmlEscape(cfg_trigger_type)+"'></label>"
                  "<label>GPIO Principale<input name='gpio_number' type='text' value='"+String(cfg_gpio_number)+"'></label>"
                  "<label>GPIO Secondaire<input name='gpio_number2' type='text' value='"+String(cfg_gpio_number2)+"'></label>"
                  "<label>Inverser G1<input type='checkbox' name='invert_gpio1' "+(cfg_invert_gpio1?"checked":"")+"></label>"
                  "<label>Inverser G2<input type='checkbox' name='invert_gpio2' "+(cfg_invert_gpio2?"checked":"")+"></label>"
                  "<h3>WiFi & Réseau</h3>"
                  "<label>SSID<input name='ssid' type='text' value='"+htmlEscape(cfg_ssid)+"'></label>"
                  "<label>Pass<input name='password' type='password'></label>"
                  "<label>IP Statique<input name='local_ip' type='text' value='"+htmlEscape(cfg_local_ip)+"'></label>"
                  "<h3>SMTP</h3>"
                  "<label>Host<input name='smtp' type='text' value='"+htmlEscape(cfg_smtp)+"'></label>"
                  "<label>Email<input name='email' type='text' value='"+htmlEscape(cfg_email)+"'></label>"
                  "<label>Pass SMTP<input name='smtp_pwd' type='password'></label>"
                  "<button class='btn' type='submit'>Sauvegarder</button>"
                  "</form>"
                  "<script>"
                  "function toggleGPIO(a){"
                  "fetch('/gpio?action='+a+'&json=1').then(r=>r.json()).then(d=>{"
                  "document.getElementById('gpio_state').innerText=d.state?'ON':'OFF';"
                  "document.getElementById('gpio_state2').innerText=d.state?'ON':'OFF';"
                  "});}"
                  "setInterval(()=>{ fetch('/gpio?json=1').then(r=>r.json()).then(d=>{"
                  "document.getElementById('gpio_state').innerText=d.state?'ON':'OFF';"
                  "document.getElementById('gpio_state2').innerText=d.state?'ON':'OFF';"
                  "}).catch(e=>{}); }, 3000);" // ⭐ MODIFICATION : Intervalle à 3s
                  "</script></body></html>";
    server.send(200,"text/html",page);
}

void handleSave() {
    if(!checkAuth()) return;
    if(server.hasArg("ssid")) cfg_ssid = server.arg("ssid");
    if(server.hasArg("password") && server.arg("password")!="") cfg_password = server.arg("password");
    if(server.hasArg("gpio_number")) cfg_gpio_number = server.arg("gpio_number").toInt();
    if(server.hasArg("gpio_number2")) cfg_gpio_number2 = server.arg("gpio_number2").toInt();
    if(server.hasArg("trigger_type")) cfg_trigger_type = server.arg("trigger_type");
    if(server.hasArg("smtp")) cfg_smtp = server.arg("smtp");
    if(server.hasArg("email")) cfg_email = server.arg("email");
    if(server.hasArg("smtp_pwd") && server.arg("smtp_pwd")!="") cfg_smtp_pwd = server.arg("smtp_pwd");
    cfg_invert_gpio1 = server.hasArg("invert_gpio1");
    cfg_invert_gpio2 = server.hasArg("invert_gpio2");
    saveConfig();
    server.send(200,"text/html","Sauvegardé. Reboot...");
    delay(500); ESP.restart();
}

void handleGPIO() {
    if(!checkAuth()) return;
    if(server.hasArg("action")) {
        String action = server.arg("action");
        if(action == "on") { setGPIO(cfg_gpio_number, true, cfg_invert_gpio1); setGPIO(cfg_gpio_number2, true, cfg_invert_gpio2); }
        else if(action == "off") { setGPIO(cfg_gpio_number, false, cfg_invert_gpio1); setGPIO(cfg_gpio_number2, false, cfg_invert_gpio2); }
    }
    // ⭐ MODIFICATION : On renvoie l'état "Logique" au JSON pour que le bouton soit raccord avec l'interface
    bool s = digitalRead(cfg_gpio_number);
    bool logicState = cfg_invert_gpio1 ? !s : s;
    if(server.hasArg("json")) {
        server.send(200,"application/json","{\"state\":"+String(logicState)+"}");
        return;
    }
    server.send(200,"text/html","OK");
}

void handleFirmwareUpload() {
    if (!checkAuth()) return;
    HTTPUpload& upload = server.upload();
    if (upload.status == UPLOAD_FILE_START) {
        if (!Update.begin(UPDATE_SIZE_UNKNOWN)) Update.printError(Serial);
    } else if (upload.status == UPLOAD_FILE_WRITE) {
        if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) Update.printError(Serial);
    } else if (upload.status == UPLOAD_FILE_END) {
        if (!Update.end(true)) Update.printError(Serial);
    }
}

void handleUpdatePage() {
    if (!checkAuth()) return;
    server.send(200, "text/html", Update.hasError()?"Error":"Success. Rebooting...");
    delay(500); ESP.restart();
}

void startAPMode() {
    WiFi.mode(WIFI_AP_STA);
    WiFi.softAP(AP_SSID, AP_PASSWORD);
}

bool attemptWiFiConnect() {
    WiFi.mode(WIFI_STA);
    WiFi.begin(cfg_ssid.c_str(), cfg_password.c_str());
    unsigned long start = millis();
    while(WiFi.status() != WL_CONNECTED && (millis()-start) < WIFI_CONNECT_TIMEOUT_MS) { delay(200); }
    return (WiFi.status() == WL_CONNECTED);
}

void checkWiFiReconnect() {
    if (WiFi.status() != WL_CONNECTED) {
        static unsigned long lastAttempt = 0;
        if (millis() - lastAttempt > 15000) {
            lastAttempt = millis();
            WiFi.begin(cfg_ssid.c_str(), cfg_password.c_str());
        }
    }
}

void setupRoutes() {
    server.on("/", handleAdmin);
    server.on("/actions", handleRoot);
    server.on("/save", HTTP_POST, handleSave);
    server.on("/gpio", handleGPIO);
    server.on("/update", HTTP_POST, handleUpdatePage, handleFirmwareUpload);
    server.begin();
}

void setup() {
    Serial.begin(115200);
    if(!LittleFS.begin(true)) Serial.println("LittleFS Error");
    loadConfig();
    pinMode(cfg_gpio_number, OUTPUT);
    pinMode(cfg_gpio_number2, OUTPUT);
    setGPIO(cfg_gpio_number, true, cfg_invert_gpio1);
    setGPIO(cfg_gpio_number2, true, cfg_invert_gpio2);

    if(cfg_local_ip.length() > 5){
        WiFi.config(ipFromString(cfg_local_ip), ipFromString(cfg_gateway), ipFromString(cfg_subnet), ipFromString(cfg_primaryDNS));
    }

    if(!attemptWiFiConnect()) startAPMode();
    setupRoutes();
}

void loop() {
    server.handleClient();
    checkWiFiReconnect();

    // ⭐ MODIFICATION : Gestion de l'envoi différé
    if (triggerMailPending) {
        Serial.println("Traitement email en arrière-plan...");
        sendEmailAction(pendingType);
        triggerMailPending = false;
        Serial.println("Retour à la boucle principale.");
    }
}
Merci lolo, je sauvegarde ma version 2,04 et installe la 2,05 pour essai encore merci ?. 
Je reviens vers toi pour les conclusions ?


RE: Connecter compteur eau impulsion - tupolev89 - 19-12-2025

Alors lolo après installation de la version 2.05 pas d’amélioration du problème, toujours pas d'accès au serveur après un remise en route de l’ouverture de la vanne suite au push.
juste un allumage très rapide moins d'une seconde de la led gpio2.
la fonction test d’envoi à disparue et de plus on a plus d'accès a tout le paragraphe de la mise a jour par ota , il a entièrement disparu aussi.
ainsi que la possibilité d’ajouter des destinataires.
donc prochaine mise a jour obligé de démonter esp32 de son support ?
merci de ta réponse
cordialement
tupolev


RE: Connecter compteur eau impulsion - Lolo69 - 19-12-2025

Aïe ouep j ai fait de la m…. Si plus OTA désolé pas le choix que de rebranché l usb je ferais un debug plus intelligent, j ai trop fait à l arrache
Mais pour le coup j ai du mal à voir ce qui cloche…
Il faudrait faire des tests avec l usb branché pour avoir accès au log du moniteur série. J essaie de trouver un esp pour retester voir si je reproduis le bug, ce sera plus simple de mettre le doigt dessus


RE: Connecter compteur eau impulsion - tupolev89 - 19-12-2025

(19-12-2025, 06:39 PM)Lolo69 a écrit : Aïe ouep j ai fait de la m…. Si plus OTA désolé pas le choix que de rebranché l usb je ferais un debug plus intelligent, j ai trop fait à l arrache

merci , mais ne t’inquiète pas c’est pas grave, esp32 est accessible ,tu es mille fois meilleur que moi , on progresse ensemble , enfin beaucoup grâce à toi , je ne fait que exécuter tes  infos .
tupolev

Ps: avec la version 2,05 les pushs ne fonctionnent plus, en attendant je repasse sur 2,04

Bonne nuit


RE: Connecter compteur eau impulsion - Lolo69 - 20-12-2025

(14-12-2025, 01:48 AM)tupolev89 a écrit : Bonjour à tous, Mike et lolo. 
Après un petit moment de peaufinage de réglage entre l'ecodevice et esp32 version 2,04, je viens de découvrir un petit bug.
Alors lorsque je ferme ou ouvre la vanne en manuel depuis l'interface de esp32, tout fonctionne parfaitement.
Lorsque esp32 reçoit le push de fermeture suivant le mot du déclencheur type cela fonctionne l'eau se coupe , suite à cela depuis l'interface de esp32 je clique sur ouvrir pour remettre l'eau tout fonctionne bien, l'eau revient Mais juste après cette action volontaire esp32 n'est plus accessible sur le réseau, seul solution faire un reset manuel avec le poussoir que j'ai installé sur son boîtier, et la esp32 redémarre et se reconnecte tout de suite au réseau.
J'espère avoir été assez clair. 
Dans l'attente de votre réponse. 
Bien cordialement 
Tupolev

trop bizarre de mon coté je n'ai pas ce probleme avec la version  2.04 28/11/25
Je reformule....
depuis la page http://192.168.1.119/ tu peux piloter tes GPIO aussi souvent que tu veux, ca fonctionne
tu reçois un http://192.168.1.119/actions?type=inondation , la vanne se ferme bien donc la GPIO bascuke
depui http://192.168.1.119/ tu peux rouvrir la vanne, mais c est la dernière action que tu peux faire , à partir de cet instant tu ne peux plus retourner dans  http://192.168.1.119 tant que tu n as pas fait de reset de l ESP ? 
c est bien ca ?
tu navigues avec quel navigigateur internet, de mon coté je fais avec chrome, edge et safari pas de soucis...


RE: Connecter compteur eau impulsion - tupolev89 - 20-12-2025

(20-12-2025, 10:22 AM)Lolo69 a écrit :
(14-12-2025, 01:48 AM)tupolev89 a écrit : Bonjour à tous, Mike et lolo. 
Après un petit moment de peaufinage de réglage entre l'ecodevice et esp32 version 2,04, je viens de découvrir un petit bug.
Alors lorsque je ferme ou ouvre la vanne en manuel depuis l'interface de esp32, tout fonctionne parfaitement.
Lorsque esp32 reçoit le push de fermeture suivant le mot du déclencheur type cela fonctionne l'eau se coupe , suite à cela depuis l'interface de esp32 je clique sur ouvrir pour remettre l'eau tout fonctionne bien, l'eau revient Mais juste après cette action volontaire esp32 n'est plus accessible sur le réseau, seul solution faire un reset manuel avec le poussoir que j'ai installé sur son boîtier, et la esp32 redémarre et se reconnecte tout de suite au réseau.
J'espère avoir été assez clair. 
Dans l'attente de votre réponse. 
Bien cordialement 
Tupolev

trop bizarre de mon coté je n'ai pas ce probleme avec la version  2.04 28/11/25
Je reformule....
depuis la page http://192.168.1.119/ tu peux piloter tes GPIO aussi souvent que tu veux, ca fonctionne
tu reçois un http://192.168.1.119/actions?type=inondation , la vanne se ferme bien donc la GPIO bascuke
depui http://192.168.1.119/ tu peux rouvrir la vanne, mais c est la dernière action que tu peux faire , à partir de cet instant tu ne peux plus retourner dans  http://192.168.1.119 tant que tu n as pas fait de reset de l ESP ? 
c est bien ca ?
tu navigues avec quel navigigateur internet, de mon coté je fais avec chrome, edge et safari pas de soucis...

Bonjour Lolo, oui c'est bien ça c'est à partir de la dernière action que ça bloque, 
La suis en train de remettre le soft par usb, je reviens dès que c'est fini, navigateur alors depuis le smartphone c'est chrome et sur le pc c'est Firefox


RE: Connecter compteur eau impulsion - Lolo69 - 20-12-2025

Attends avant de repousser le code
mets le suivant j'ai rajouté des petites pauses et des log supplementaires
Code :
//  version  2.06 20/12/25
/*
Parametrages depuis page /admin
envoi à une liste de destinataires séparés par des ;
GPIO configurable depuis admin, pilotable en temps réel
Trigger configurable pour mise à 0 de GPIO
Ajout OTA via /admin
Ajout 2ème GPIO pour recopie état de la vanne
rearrangement cosmetiques des sections de la page /admin
Rearrangement cosmetiques dans le code ( alignements commentaires indentation)
Ajout inversion logique indépendante par GPIO
*/

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <LittleFS.h>
#include <WiFiClientSecure.h>
#include <ESP_Mail_Client.h>
#include <Update.h>

#define WIFI_CONNECT_TIMEOUT_MS 10000
#define AP_SSID "ESP32_Config"
// Eviter NULL pour ne pas appeler strlen(NULL)
#define AP_PASSWORD ""

// si tu veux un AP sécurisé par défaut, mets une chaîne >=8 caractères
// #define AP_PASSWORD "esp32admin"

const char* CONFIG_FILE = "/config.txt";

// Valeurs par défaut
bool isAPMode = false; // Pour savoir si on est en mode secours
String cfg_ssid = "ton SSID";
String cfg_password = "ton mot de passe wifi";
String cfg_smtp = "smtp.orange.fr";
String cfg_email = "tonemail@orange.fr";
String cfg_smtp_pwd = "ton mot de passe Mail";
String cfg_destinataires = "dest1@free.fr;dest2@orange.fr";
int cfg_smtp_port = 465;
int cfg_gpio_number = 2;

// ⭐ AJOUT — GPIO clone
int cfg_gpio_number2 = 23;

String cfg_trigger_type = "inondation";

// ⭐ AJOUT — inversion logique indépendante GPIO
bool cfg_invert_gpio1 = false; // GPIO principale
bool cfg_invert_gpio2 = false; // GPIO clone

// Réseau statique
String cfg_local_ip = "192.168.1.119";
String cfg_gateway = "192.168.1.1";
String cfg_subnet = "255.255.255.0";
String cfg_primaryDNS = "192.168.1.1";
String cfg_secondaryDNS = "8.8.4.4";

// Admin auth
String admin_user = "admin";
String admin_pwd = "admin";

// Objets globaux
WebServer server(80);
SMTPSession smtp;

// --- Helpers IP ---
IPAddress ipFromString(const String &s) {
    IPAddress ip(0,0,0,0);
    int parts[4] = {0,0,0,0}, idx = 0;
    String tmp = s;
    tmp.trim();
    int start = 0;
    for (int i=0; i<4; ++i) {
        int pos = tmp.indexOf('.', start);
        String part = (pos==-1) ? tmp.substring(start) : tmp.substring(start,pos);
        part.trim();
        parts[idx++] = part.toInt();
        if (pos==-1) break;
        start = pos+1;
    }
    ip = IPAddress(parts[0], parts[1], parts[2], parts[3]);
    return ip;
}

// --- LittleFS ---
void saveConfig() {
    File f = LittleFS.open(CONFIG_FILE, "w");
    if (!f) { Serial.println("Erreur ouverture fichier config"); return; }

    f.printf("ssid=%s\n", cfg_ssid.c_str());
    f.printf("password=%s\n", cfg_password.c_str());
    f.printf("smtp=%s\n", cfg_smtp.c_str());
    f.printf("email=%s\n", cfg_email.c_str());
    f.printf("smtp_pwd=%s\n", cfg_smtp_pwd.c_str());
    f.printf("destinataires=%s\n", cfg_destinataires.c_str());
    f.printf("smtp_port=%d\n", cfg_smtp_port);
    f.printf("gpio_number=%d\n", cfg_gpio_number);
    f.printf("gpio_number2=%d\n", cfg_gpio_number2);
    f.printf("invert_gpio1=%d\n", cfg_invert_gpio1 ? 1 : 0);
    f.printf("invert_gpio2=%d\n", cfg_invert_gpio2 ? 1 : 0);
    f.printf("trigger_type=%s\n", cfg_trigger_type.c_str());
    f.printf("local_ip=%s\n", cfg_local_ip.c_str());
    f.printf("gateway=%s\n", cfg_gateway.c_str());
    f.printf("subnet=%s\n", cfg_subnet.c_str());
    f.printf("primaryDNS=%s\n", cfg_primaryDNS.c_str());
    f.printf("secondaryDNS=%s\n", cfg_secondaryDNS.c_str());
    f.printf("admin_user=%s\n", admin_user.c_str());
    f.printf("admin_pwd=%s\n", admin_pwd.c_str());
    f.close();
    Serial.println("Config sauvegardée dans LittleFS");
}

void loadConfig() {
    if (!LittleFS.exists(CONFIG_FILE)) {
        Serial.println("Fichier config absent -> valeurs par défaut");
        return;
    }
    File f = LittleFS.open(CONFIG_FILE,"r");
    if (!f) {
        Serial.println("Erreur ouverture fichier config");
        return;
    }

    while(f.available()) {
        String line = f.readStringUntil('\n');
        line.trim();
        if (line.length() == 0) continue;

        int eq = line.indexOf('=');
        if (eq == -1) continue;

        String key = line.substring(0, eq), val = line.substring(eq+1);
        key.trim();
        val.trim();

        if(key=="ssid") cfg_ssid=val;
        else if(key=="password") cfg_password=val;
        else if(key=="smtp") cfg_smtp=val;
        else if(key=="email") cfg_email=val;
        else if(key=="smtp_pwd") cfg_smtp_pwd=val;
        else if(key=="destinataires") cfg_destinataires=val;
        else if(key=="smtp_port") cfg_smtp_port=val.toInt();
        else if(key=="gpio_number") cfg_gpio_number=val.toInt();
        else if(key=="gpio_number2") cfg_gpio_number2=val.toInt();
        else if(key=="invert_gpio1") cfg_invert_gpio1=(val.toInt()!=0);
        else if(key=="invert_gpio2") cfg_invert_gpio2=(val.toInt()!=0);
        else if(key=="trigger_type") cfg_trigger_type=val;
        else if(key=="local_ip") cfg_local_ip = val;
        else if(key=="gateway") cfg_gateway = val;
        else if(key=="subnet") cfg_subnet = val;
        else if(key=="primaryDNS") cfg_primaryDNS = val;
        else if(key=="secondaryDNS") cfg_secondaryDNS = val;
        else if(key=="admin_user") admin_user = val;
        else if(key=="admin_pwd") admin_pwd = val;
    }

    f.close();
    Serial.println("Config chargée depuis LittleFS");
}

// ⭐ AJOUT — helper GPIO avec inversion
void setGPIO(int gpio, bool on, bool invert) {
    digitalWrite(gpio, invert ? (on ? LOW : HIGH) : (on ? HIGH : LOW));
}

// --- SMTP ---
void addAllRecipients(SMTP_Message &message, const String &liste) {
    String s=liste;
    int start=0, pos=0;
    while((pos=s.indexOf(';',start))!=-1) {
        String email=s.substring(start,pos);
        email.trim();
        if(email.length()>0) message.addRecipient("Destinataire",email.c_str());
        start=pos+1;
    }
    String email=s.substring(start);
    email.trim();
    if(email.length()>0) message.addRecipient("Destinataire",email.c_str());
}

// --- Page root ---
void handleRoot() {
    time_t now=time(nullptr);
    struct tm timeinfo;
    localtime_r(&now,&timeinfo);
    char buffer[20];
    strftime(buffer,sizeof(buffer),"%d/%m/%Y %H:%M:%S",&timeinfo);

    String type="Non défini";
    if(server.hasArg("type")) type=server.arg("type");

    // Vérifie le trigger pour GPIO
    if(type.equalsIgnoreCase(cfg_trigger_type)){
        setGPIO(cfg_gpio_number, false, cfg_invert_gpio1); // GPIO principale à 0 selon logique
        setGPIO(cfg_gpio_number2, false, cfg_invert_gpio2); // GPIO clone
        Serial.println("Trigger activé : GPIOs mis à 0 selon logique");
    }

    ESP_Mail_Session session;
    session.server.host_name=cfg_smtp.c_str();
    session.server.port=cfg_smtp_port;
    session.login.email=cfg_email.c_str();
    session.login.password=cfg_smtp_pwd.c_str();
    session.login.user_domain="";

    SMTP_Message message;
    message.sender.name="ECODEVICE";
    message.sender.email=cfg_email.c_str();
    addAllRecipients(message,cfg_destinataires);
    message.subject="Alerte de type : "+type;
    message.text.content="Bonjour ! Email envoyé depuis ESP32 le "+String(buffer);

    smtp.debug(1);

    String prefixPage="<html><head><meta charset='UTF-8'><meta http-equiv='Cache-Control' content='no-cache, no-store, must-revalidate'><meta name='viewport' content='width=device-width, initial-scale=1'>"
                      "<style>body{font-family:Arial;padding:16px;} .ok{color:green;} .ko{color:red;}</style></head><body>";
    String suffixPage="</body></html>";
    String page=prefixPage+suffixPage;

    if(!smtp.connect(&session)){
        Serial.println("Erreur de connexion SMTP !");
        server.send(200,"text/html",prefixPage+"<p class='ko'>Erreur connexion SMTP</p>"+suffixPage);
        return;
    }

    if(!MailClient.sendMail(&smtp,&message)){
        page=prefixPage+"<p class='ko'>Erreur d'envoi !</p>"+suffixPage;
        Serial.println("Erreur d'envoi !");
    } else {
        page=prefixPage+"<p class='ok'>Email envoyé le "+String(buffer)+"</p>"+suffixPage;
        Serial.println("Email envoyé !");
    }

    smtp.closeSession();
    delay(50); // ⭐ Laisse le temps à la pile réseau de fermer proprement le socket SSL
    message.clear(); // ⭐ AJOUT : Libère la mémoire du message
    server.send(200,"text/html",page);
    // ⭐ AJOUT : Force le vidage du buffer réseau pour éviter le freeze
    server.client().stop();
}

// --- Auth & escape ---
bool checkAuth() {
    if(!server.authenticate(admin_user.c_str(),admin_pwd.c_str())) {
        server.requestAuthentication();
        return false;
    }
    return true;
}

String htmlEscape(const String &s){
    String r=s;
    r.replace("&","&amp;");
    r.replace("<","&lt;");
    r.replace(">","&gt;");
    r.replace("\"","&quot;");
    r.replace("'","'");
    return r;
}

// --- Page /admin ---
void handleAdmin() {
    if(!checkAuth()) return;

    int gpio_state  = digitalRead(cfg_gpio_number);
    int gpio_state2 = digitalRead(cfg_gpio_number2);

    String page = "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta http-equiv='Cache-Control' content='no-cache, no-store, must-revalidate'><meta name='viewport' content='width=device-width, initial-scale=1'>"
                  "<style>body{font-family:Arial;padding:12px;} label{display:block;margin-top:8px;} input[type=text], input[type=password], textarea{width:100%;padding:8px;border-radius:6px;border:1px solid #ccc;box-sizing:border-box;} .row{display:flex;gap:8px;} .col{flex:1;} .btn{display:inline-block;padding:8px 12px;margin-top:10px;border-radius:6px;background:#2196F3;color:#fff;text-decoration:none;}</style>"
                  "<title>Ecodevice Gateway</title></head><body>"
                  "<h2>Paramètres Ecodevice Gateway</h2>"

                  "<h3>GPIO (principale) " + String(cfg_gpio_number) + " : <span id='gpio_state'>" + (gpio_state?"ON":"OFF") + "</span></h3>"
                  "<button onclick=\"toggleGPIO('on')\">Ouvrir</button> "
                  "<button onclick=\"toggleGPIO('off')\">Fermer</button><hr>"
                  "<h3>GPIO2 (secondaire) " + String(cfg_gpio_number2) + " : <span id='gpio_state2'>" + (gpio_state2?"ON":"OFF") + "</span></h3>"
                  "<p>(Cette sortie suit automatiquement la GPIO principale)</p><hr>"

                  "<form method='POST' action='/save'>"
                  "<h3>Paramètres globaux</h3>"
                  "<label>déclencheur type<input name='trigger_type' type='text' value='"+htmlEscape(cfg_trigger_type)+"'></label>"
                  "<label>Numéro GPIO principale<input name='gpio_number' type='text' value='"+String(cfg_gpio_number)+"'></label>"
                  "<label>Numéro GPIO secondaire<input name='gpio_number2' type='text' value='"+String(cfg_gpio_number2)+"'></label>"
                  "<label>Inverser logique GPIO principale<input type='checkbox' name='invert_gpio1' "+(cfg_invert_gpio1?"checked":"")+"></label>"
                  "<label>Inverser logique GPIO secondaire<input type='checkbox' name='invert_gpio2' "+(cfg_invert_gpio2?"checked":"")+"></label>"

                  "<h3>Paramètres Wifi SSID</h3>"
                  "<label>SSID WiFi<input name='ssid' type='text' value='"+htmlEscape(cfg_ssid)+"'></label>"
                  "<label>Mot de passe WiFi<input name='password' type='password' placeholder='(inchangé)'></label>"

                  "<h3>Paramètres Réseau</h3>"
                  "<label>Local IP<input name='local_ip' type='text' value='"+htmlEscape(cfg_local_ip)+"'></label>"
                  "<label>Gateway<input name='gateway' type='text' value='"+htmlEscape(cfg_gateway)+"'></label>"
                  "<label>Subnet<input name='subnet' type='text' value='"+htmlEscape(cfg_subnet)+"'></label>"
                  "<label>Primary DNS<input name='primaryDNS' type='text' value='"+htmlEscape(cfg_primaryDNS)+"'></label>"
                  "<label>Secondary DNS<input name='secondaryDNS' type='text' value='"+htmlEscape(cfg_secondaryDNS)+"'></label>"

                  "<h3>Paramètres SMTP</h3>"
                  "<label>SMTP host<input name='smtp' type='text' value='"+htmlEscape(cfg_smtp)+"'></label>"
                  "<label>SMTP port<input name='smtp_port' type='text' value='"+String(cfg_smtp_port)+"'></label>"
                  "<label>Adresse email (expediteur)<input name='email' type='text' value='"+htmlEscape(cfg_email)+"'></label>"
                  "<label>Mot de passe SMTP<input name='smtp_pwd' type='password' placeholder='(inchangé)'></label>"
                  "<label>Destinataires (séparés par ;) <textarea name='destinataires' rows='3'>"+htmlEscape(cfg_destinataires)+"</textarea></label>"

                  "<h3>Paramètres accès admin</h3>"
                  "<label>Admin user<input name='admin_user' type='text' value='"+htmlEscape(admin_user)+"'></label>"
                  "<label>Admin pwd<input name='admin_pwd' type='password' placeholder='(inchangé)'></label>"

                  "<div class='row'>"
                  "  <div class='col'><button class='btn' type='submit'>Sauvegarder</button></div>"
                  "  <div class='col'><button class='btn' type='button' onclick=\"window.location.href='/testsend'\">Tester envoi</button></div>"
                  "</div>"

                  "</form>"

                  "<hr><h3>Mise à jour OTA</h3>"
                  "<form method='POST' action='/update' enctype='multipart/form-data'>"
                  "<input type='file' name='firmware' accept='.bin' required>"
                  "<button class='btn' type='submit'>Mettre à jour</button>"
                  "</form>"

                  "<script>"
                  "function toggleGPIO(action){"
                  "  fetch('/gpio?action='+action+'&json=1')"
                  "  .then(response=>response.json())"
                  "  .then(data=>{"
                  "     document.getElementById('gpio_state').innerText=data.state?'ON':'OFF';"
                  "     document.getElementById('gpio_state2').innerText=data.state?'ON':'OFF';"
                  "  })"
                  "  .catch(err=>console.error(err));"
                  "} "
                  "setInterval(()=>{"
                  "  fetch('/gpio?json=1')"
                  "  .then(r=>r.json())"
                  "  .then(d=>{"
                  "     document.getElementById('gpio_state').innerText=d.state?'ON':'OFF';"
                  "     document.getElementById('gpio_state2').innerText=d.state?'ON':'OFF';"
                  "  });"
                  "},1000);"
                  "</script>"
                  "</body></html>";

    server.send(200,"text/html",page);
}

// --- Save ---
void handleSave() {
    if(!checkAuth()) return;

    if(server.hasArg("ssid")) cfg_ssid = server.arg("ssid");
    if(server.hasArg("password") && server.arg("password") != "") cfg_password = server.arg("password");

    if(server.hasArg("gpio_number")) {
        cfg_gpio_number = server.arg("gpio_number").toInt();
        pinMode(cfg_gpio_number, OUTPUT);     
        setGPIO(cfg_gpio_number, true, cfg_invert_gpio1);
    }

    if(server.hasArg("gpio_number2")) {
        cfg_gpio_number2 = server.arg("gpio_number2").toInt();
        pinMode(cfg_gpio_number2, OUTPUT);     
        setGPIO(cfg_gpio_number2, true, cfg_invert_gpio2);
    }

    if(server.hasArg("trigger_type")) cfg_trigger_type = server.arg("trigger_type");
    if(server.hasArg("smtp")) cfg_smtp = server.arg("smtp");
    if(server.hasArg("smtp_port")) cfg_smtp_port = server.arg("smtp_port").toInt();
    if(server.hasArg("email")) cfg_email = server.arg("email");
    if(server.hasArg("smtp_pwd") && server.arg("smtp_pwd") != "") cfg_smtp_pwd = server.arg("smtp_pwd");
    if(server.hasArg("destinataires")) cfg_destinataires = server.arg("destinataires");
    if(server.hasArg("local_ip")) cfg_local_ip = server.arg("local_ip");
    if(server.hasArg("gateway")) cfg_gateway = server.arg("gateway");
    if(server.hasArg("subnet")) cfg_subnet = server.arg("subnet");
    if(server.hasArg("primaryDNS")) cfg_primaryDNS = server.arg("primaryDNS");
    if(server.hasArg("secondaryDNS")) cfg_secondaryDNS = server.arg("secondaryDNS");
    if(server.hasArg("admin_user")) admin_user = server.arg("admin_user");
    if(server.hasArg("admin_pwd") && server.arg("admin_pwd") != "") admin_pwd = server.arg("admin_pwd");

    // ⭐ AJOUT — lecture inversion logique
    cfg_invert_gpio1 = server.hasArg("invert_gpio1");
    cfg_invert_gpio2 = server.hasArg("invert_gpio2");

    saveConfig();
    server.send(200,"text/html","<html><body><p>Config sauvegardée. Redémarrage...</p></body></html>");
    delay(200);
    ESP.restart();
}

// --- Test send ---
void handleTestSend() {
    if(!checkAuth()) return;

    ESP_Mail_Session session;
    session.server.host_name = cfg_smtp.c_str();
    session.server.port = cfg_smtp_port;
    session.login.email = cfg_email.c_str();
    session.login.password = cfg_smtp_pwd.c_str();
    session.login.user_domain = "";

    SMTP_Message message;
    message.sender.name = "ECODEVICE";
    message.sender.email = cfg_email.c_str();
    addAllRecipients(message, cfg_destinataires);
    message.subject = "Test d'envoi depuis ESP32 (UI)";
    message.text.content = "Ceci est un test envoyé depuis /admin";

    smtp.debug(1);
    String pageStart = "<html><body>";
    String pageEnd = "</body></html>";

    if(!smtp.connect(&session)) server.send(200,"text/html",pageStart+"<p style='color:red'>Erreur connexion SMTP</p>"+pageEnd);
    else if(!MailClient.sendMail(&smtp,&message)) server.send(200,"text/html",pageStart+"<p style='color:red'>Erreur envoi</p>"+pageEnd);
    else server.send(200,"text/html",pageStart+"<p style='color:green'>Email envoyé</p>"+pageEnd);

    smtp.closeSession();
}

// --- GPIO route ---
void handleGPIO() {
    if(!checkAuth()) return;
    if(server.hasArg("action")) {
        String action = server.arg("action");
        if(action == "on") {
            setGPIO(cfg_gpio_number, true, cfg_invert_gpio1);
            setGPIO(cfg_gpio_number2, true, cfg_invert_gpio2);
        } else if(action == "off") {
            setGPIO(cfg_gpio_number, false, cfg_invert_gpio1);
            setGPIO(cfg_gpio_number2, false, cfg_invert_gpio2);
        }
    }
    int state = digitalRead(cfg_gpio_number);
    if(server.hasArg("json") && server.arg("json")=="1") {
        server.send(200,"application/json","{\"state\":"+String(state)+"}");
        return;
    }
    server.send(200,"text/html","<html><body><h3>GPIO "+String(cfg_gpio_number)+" : "+(state?"ON":"OFF")+"</h3>"
                "<a href='/gpio?action=on'>Ouvrir</a> | <a href='/gpio?action=off'>Fermer</a></body></html>");
}

// --- OTA upload handler ---
void handleFirmwareUpload() {
    if (!checkAuth()) return;

    HTTPUpload& upload = server.upload();

    if (upload.status == UPLOAD_FILE_START) {
        Serial.setDebugOutput(true);
        Serial.printf("Mise à jour OTA commencée : %s\n", upload.filename.c_str());
        if (!Update.begin(UPDATE_SIZE_UNKNOWN)) Update.printError(Serial);

    } else if (upload.status == UPLOAD_FILE_WRITE) {
        if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) Update.printError(Serial);

    } else if (upload.status == UPLOAD_FILE_END) {
        if (Update.end(true)) Serial.printf("Mise à jour OTA terminée : %u octets\n", upload.totalSize);
        else Update.printError(Serial);
        Serial.setDebugOutput(false);

    } else if (upload.status == UPLOAD_FILE_ABORTED) {
        Update.end();
        Serial.println("Mise à jour OTA annulée.");
    }
}

void handleUpdatePage() {
    if (!checkAuth()) return;

    if (Update.hasError()) {
        server.send(200, "text/html",
          "<html><body><p style='color:red'>Échec de la mise à jour</p></body></html>");
    } else {
        server.send(200, "text/html",
          "<html><body><p style='color:green'>Mise à jour réussie, redémarrage...</p></body></html>");
        delay(500);
        ESP.restart();
    }
}

// --- AP fallback ---
void startAPMode() {
    isAPMode = true; // ⭐ Ajout
    Serial.println("Demarrage AP...");
    WiFi.disconnect(true);
    delay(200);
    WiFi.mode(WIFI_AP_STA);
    delay(50);

    IPAddress apIP(192,168,4,1);
    IPAddress apGateway = apIP;
    IPAddress apMask(255,255,255,0);
    if(!WiFi.softAPConfig(apIP, apGateway, apMask)) Serial.println("softAPConfig FAILED");
    else Serial.println("softAPConfig OK");
    delay(50);

    if(strlen(AP_PASSWORD) == 0) WiFi.softAP(AP_SSID);
    else WiFi.softAP(AP_SSID, AP_PASSWORD);

    delay(300);
    Serial.print("AP SSID: "); Serial.println(AP_SSID);
    Serial.print("AP IP (softAPIP): "); Serial.println(WiFi.softAPIP());
    Serial.print("Num stations connected: "); Serial.println(WiFi.softAPgetStationNum());
}

// --- WiFi connect ---
bool attemptWiFiConnect() {
    Serial.printf("Connexion a %s\n", cfg_ssid.c_str());
    WiFi.mode(WIFI_STA);
    WiFi.begin(cfg_ssid.c_str(), cfg_password.c_str());
    unsigned long start = millis();
    while(WiFi.status() != WL_CONNECTED && (millis()-start) < WIFI_CONNECT_TIMEOUT_MS) {
        Serial.print(".");
        delay(200);
    }
    Serial.println();
    if(WiFi.status() == WL_CONNECTED) {
        Serial.println("WiFi connecté");
        Serial.print("IP: "); Serial.println(WiFi.localIP());
        return true;
    }
    Serial.println("WiFi non connecté -> fallback AP");
    return false;
}

// ⭐ AJOUT — Reconnexion automatique WiFi
int retryCount = 0; // À mettre en haut du code avec les globales

void checkWiFiReconnect() {
    if (isAPMode) return; // Si on est déjà en AP, on ne fait rien

    if (WiFi.status() != WL_CONNECTED) {
        static unsigned long lastAttempt = 0;
        if (millis() - lastAttempt > 10000) { // Tentative toutes les 10s
            lastAttempt = millis();
            Serial.println("WiFi perdu -> tentative de reconnexion...");
           
            if (attemptWiFiConnect()) {
                retryCount = 0; // Succès : on réinitialise
            } else {
                retryCount++;
                delay(100); // ⭐ Petit repos après un échec de négociation WiFi
                Serial.printf("Échec de reconnexion n°%d\n", retryCount);
            }

            if (retryCount >= 30) { // Après 30 échecs (300s)
                Serial.println("Trop d'échecs -> Bascule en mode AP pour configuration");
                startAPMode();
            }
        }
    } else {
        retryCount = 0; // On est connecté, on remet le compteur à zéro
    }
}
// --- Routes ---
void setupRoutes() {
    server.on("/", handleAdmin);          // <-- admin devient la page racine
    server.on("/actions", handleRoot);    // <-- ancien root va dans /actions
    server.on("/save", HTTP_POST, handleSave);
    server.on("/testsend", HTTP_GET, handleTestSend);
    server.on("/gpio", handleGPIO);
    server.on("/update", HTTP_POST, handleUpdatePage, handleFirmwareUpload);
    server.begin();
    Serial.println("Serveur web démarré");
}

// --- Setup ---
void setup() {
    Serial.begin(115200);
    delay(1000);

    if(!LittleFS.begin(true)) Serial.println("LittleFS mount failed!");
    else Serial.println("LittleFS OK");

    loadConfig();

    // Configuration des deux GPIO (GPIO2 est un clone)
    pinMode(cfg_gpio_number, OUTPUT);
    setGPIO(cfg_gpio_number, true, cfg_invert_gpio1);

    pinMode(cfg_gpio_number2, OUTPUT);
    setGPIO(cfg_gpio_number2, true, cfg_invert_gpio2);

    if(cfg_local_ip.length() > 0){
        IPAddress local = ipFromString(cfg_local_ip);
        IPAddress gw = ipFromString(cfg_gateway);
        IPAddress mask = ipFromString(cfg_subnet);
        IPAddress pdns = ipFromString(cfg_primaryDNS);
        IPAddress sdns = ipFromString(cfg_secondaryDNS);
        if(!WiFi.config(local, gw, mask, pdns, sdns)) Serial.println("Echec config IP statique");
        else Serial.println("Config IP statique appliquée");
    }

    bool wifiOk = attemptWiFiConnect();
    if(!wifiOk) {
        startAPMode();
        setupRoutes();
    } else {
        setupRoutes();
        configTzTime("CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00","fr.pool.ntp.org","time.nist.gov");
        unsigned long t0 = millis();
        const unsigned long NTPTIMEOUT = 5000;
        while (millis() - t0 < NTPTIMEOUT && time(nullptr) < ESP_MAIL_CLIENT_VALID_TS) { delay(100);}
        if (time(nullptr) >= ESP_MAIL_CLIENT_VALID_TS) Serial.println("NTP ok");
        else Serial.println("NTP non synchro (timeout)");
    }
}

// --- Loop ---
void loop() {
    static unsigned long lastHeartbeat = 0;
    if (millis() - lastHeartbeat > 5000) {
        lastHeartbeat = millis();
        Serial.printf("Heartbeat: RAM libre = %u octets, WiFi Status = %d\n", ESP.getFreeHeap(), WiFi.status());
    }

    server.handleClient();
    checkWiFiReconnect();
    delay(1); // ⭐ Crucial : rend la main au système (FreeRTOS) pendant 1ms
}
charge cette version 2.06 20/12/25, et avant de tout fermer fait des tests en gardant le moniteur serie arduino ouvert pour voir les logs qui s 'affichent
de mon coté ca fonctionne nickel , la grosse difference peut etre avec toi c est que de mon coté j envoie les push depuis un navigateur et non pas depuis ecodevice, si de ton coté ca ne fonctionne toujours pas , essaie d'envoyer les push depuis un navigateur voir si ca se comporte pareil


RE: Connecter compteur eau impulsion - tupolev89 - 20-12-2025

(20-12-2025, 10:43 AM)Lolo69 a écrit : Attends avant de repousser le code
mets le suivant j'ai rajouté des petites pauses et des log supplementaires
Code :
//  version  2.06 20/12/25
/*
Parametrages depuis page /admin
envoi à une liste de destinataires séparés par des ;
GPIO configurable depuis admin, pilotable en temps réel
Trigger configurable pour mise à 0 de GPIO
Ajout OTA via /admin
Ajout 2ème GPIO pour recopie état de la vanne
rearrangement cosmetiques des sections de la page /admin
Rearrangement cosmetiques dans le code ( alignements commentaires indentation)
Ajout inversion logique indépendante par GPIO
*/

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <LittleFS.h>
#include <WiFiClientSecure.h>
#include <ESP_Mail_Client.h>
#include <Update.h>

#define WIFI_CONNECT_TIMEOUT_MS 10000
#define AP_SSID "ESP32_Config"
// Eviter NULL pour ne pas appeler strlen(NULL)
#define AP_PASSWORD ""

// si tu veux un AP sécurisé par défaut, mets une chaîne >=8 caractères
// #define AP_PASSWORD "esp32admin"

const char* CONFIG_FILE = "/config.txt";

// Valeurs par défaut
bool isAPMode = false; // Pour savoir si on est en mode secours
String cfg_ssid = "ton SSID";
String cfg_password = "ton mot de passe wifi";
String cfg_smtp = "smtp.orange.fr";
String cfg_email = "tonemail@orange.fr";
String cfg_smtp_pwd = "ton mot de passe Mail";
String cfg_destinataires = "dest1@free.fr;dest2@orange.fr";
int cfg_smtp_port = 465;
int cfg_gpio_number = 2;

// ⭐ AJOUT — GPIO clone
int cfg_gpio_number2 = 23;

String cfg_trigger_type = "inondation";

// ⭐ AJOUT — inversion logique indépendante GPIO
bool cfg_invert_gpio1 = false; // GPIO principale
bool cfg_invert_gpio2 = false; // GPIO clone

// Réseau statique
String cfg_local_ip = "192.168.1.119";
String cfg_gateway = "192.168.1.1";
String cfg_subnet = "255.255.255.0";
String cfg_primaryDNS = "192.168.1.1";
String cfg_secondaryDNS = "8.8.4.4";

// Admin auth
String admin_user = "admin";
String admin_pwd = "admin";

// Objets globaux
WebServer server(80);
SMTPSession smtp;

// --- Helpers IP ---
IPAddress ipFromString(const String &s) {
    IPAddress ip(0,0,0,0);
    int parts[4] = {0,0,0,0}, idx = 0;
    String tmp = s;
    tmp.trim();
    int start = 0;
    for (int i=0; i<4; ++i) {
        int pos = tmp.indexOf('.', start);
        String part = (pos==-1) ? tmp.substring(start) : tmp.substring(start,pos);
        part.trim();
        parts[idx++] = part.toInt();
        if (pos==-1) break;
        start = pos+1;
    }
    ip = IPAddress(parts[0], parts[1], parts[2], parts[3]);
    return ip;
}

// --- LittleFS ---
void saveConfig() {
    File f = LittleFS.open(CONFIG_FILE, "w");
    if (!f) { Serial.println("Erreur ouverture fichier config"); return; }

    f.printf("ssid=%s\n", cfg_ssid.c_str());
    f.printf("password=%s\n", cfg_password.c_str());
    f.printf("smtp=%s\n", cfg_smtp.c_str());
    f.printf("email=%s\n", cfg_email.c_str());
    f.printf("smtp_pwd=%s\n", cfg_smtp_pwd.c_str());
    f.printf("destinataires=%s\n", cfg_destinataires.c_str());
    f.printf("smtp_port=%d\n", cfg_smtp_port);
    f.printf("gpio_number=%d\n", cfg_gpio_number);
    f.printf("gpio_number2=%d\n", cfg_gpio_number2);
    f.printf("invert_gpio1=%d\n", cfg_invert_gpio1 ? 1 : 0);
    f.printf("invert_gpio2=%d\n", cfg_invert_gpio2 ? 1 : 0);
    f.printf("trigger_type=%s\n", cfg_trigger_type.c_str());
    f.printf("local_ip=%s\n", cfg_local_ip.c_str());
    f.printf("gateway=%s\n", cfg_gateway.c_str());
    f.printf("subnet=%s\n", cfg_subnet.c_str());
    f.printf("primaryDNS=%s\n", cfg_primaryDNS.c_str());
    f.printf("secondaryDNS=%s\n", cfg_secondaryDNS.c_str());
    f.printf("admin_user=%s\n", admin_user.c_str());
    f.printf("admin_pwd=%s\n", admin_pwd.c_str());
    f.close();
    Serial.println("Config sauvegardée dans LittleFS");
}

void loadConfig() {
    if (!LittleFS.exists(CONFIG_FILE)) {
        Serial.println("Fichier config absent -> valeurs par défaut");
        return;
    }
    File f = LittleFS.open(CONFIG_FILE,"r");
    if (!f) {
        Serial.println("Erreur ouverture fichier config");
        return;
    }

    while(f.available()) {
        String line = f.readStringUntil('\n');
        line.trim();
        if (line.length() == 0) continue;

        int eq = line.indexOf('=');
        if (eq == -1) continue;

        String key = line.substring(0, eq), val = line.substring(eq+1);
        key.trim();
        val.trim();

        if(key=="ssid") cfg_ssid=val;
        else if(key=="password") cfg_password=val;
        else if(key=="smtp") cfg_smtp=val;
        else if(key=="email") cfg_email=val;
        else if(key=="smtp_pwd") cfg_smtp_pwd=val;
        else if(key=="destinataires") cfg_destinataires=val;
        else if(key=="smtp_port") cfg_smtp_port=val.toInt();
        else if(key=="gpio_number") cfg_gpio_number=val.toInt();
        else if(key=="gpio_number2") cfg_gpio_number2=val.toInt();
        else if(key=="invert_gpio1") cfg_invert_gpio1=(val.toInt()!=0);
        else if(key=="invert_gpio2") cfg_invert_gpio2=(val.toInt()!=0);
        else if(key=="trigger_type") cfg_trigger_type=val;
        else if(key=="local_ip") cfg_local_ip = val;
        else if(key=="gateway") cfg_gateway = val;
        else if(key=="subnet") cfg_subnet = val;
        else if(key=="primaryDNS") cfg_primaryDNS = val;
        else if(key=="secondaryDNS") cfg_secondaryDNS = val;
        else if(key=="admin_user") admin_user = val;
        else if(key=="admin_pwd") admin_pwd = val;
    }

    f.close();
    Serial.println("Config chargée depuis LittleFS");
}

// ⭐ AJOUT — helper GPIO avec inversion
void setGPIO(int gpio, bool on, bool invert) {
    digitalWrite(gpio, invert ? (on ? LOW : HIGH) : (on ? HIGH : LOW));
}

// --- SMTP ---
void addAllRecipients(SMTP_Message &message, const String &liste) {
    String s=liste;
    int start=0, pos=0;
    while((pos=s.indexOf(';',start))!=-1) {
        String email=s.substring(start,pos);
        email.trim();
        if(email.length()>0) message.addRecipient("Destinataire",email.c_str());
        start=pos+1;
    }
    String email=s.substring(start);
    email.trim();
    if(email.length()>0) message.addRecipient("Destinataire",email.c_str());
}

// --- Page root ---
void handleRoot() {
    time_t now=time(nullptr);
    struct tm timeinfo;
    localtime_r(&now,&timeinfo);
    char buffer[20];
    strftime(buffer,sizeof(buffer),"%d/%m/%Y %H:%M:%S",&timeinfo);

    String type="Non défini";
    if(server.hasArg("type")) type=server.arg("type");

    // Vérifie le trigger pour GPIO
    if(type.equalsIgnoreCase(cfg_trigger_type)){
        setGPIO(cfg_gpio_number, false, cfg_invert_gpio1); // GPIO principale à 0 selon logique
        setGPIO(cfg_gpio_number2, false, cfg_invert_gpio2); // GPIO clone
        Serial.println("Trigger activé : GPIOs mis à 0 selon logique");
    }

    ESP_Mail_Session session;
    session.server.host_name=cfg_smtp.c_str();
    session.server.port=cfg_smtp_port;
    session.login.email=cfg_email.c_str();
    session.login.password=cfg_smtp_pwd.c_str();
    session.login.user_domain="";

    SMTP_Message message;
    message.sender.name="ECODEVICE";
    message.sender.email=cfg_email.c_str();
    addAllRecipients(message,cfg_destinataires);
    message.subject="Alerte de type : "+type;
    message.text.content="Bonjour ! Email envoyé depuis ESP32 le "+String(buffer);

    smtp.debug(1);

    String prefixPage="<html><head><meta charset='UTF-8'><meta http-equiv='Cache-Control' content='no-cache, no-store, must-revalidate'><meta name='viewport' content='width=device-width, initial-scale=1'>"
                      "<style>body{font-family:Arial;padding:16px;} .ok{color:green;} .ko{color:red;}</style></head><body>";
    String suffixPage="</body></html>";
    String page=prefixPage+suffixPage;

    if(!smtp.connect(&session)){
        Serial.println("Erreur de connexion SMTP !");
        server.send(200,"text/html",prefixPage+"<p class='ko'>Erreur connexion SMTP</p>"+suffixPage);
        return;
    }

    if(!MailClient.sendMail(&smtp,&message)){
        page=prefixPage+"<p class='ko'>Erreur d'envoi !</p>"+suffixPage;
        Serial.println("Erreur d'envoi !");
    } else {
        page=prefixPage+"<p class='ok'>Email envoyé le "+String(buffer)+"</p>"+suffixPage;
        Serial.println("Email envoyé !");
    }

    smtp.closeSession();
    delay(50); // ⭐ Laisse le temps à la pile réseau de fermer proprement le socket SSL
    message.clear(); // ⭐ AJOUT : Libère la mémoire du message
    server.send(200,"text/html",page);
    // ⭐ AJOUT : Force le vidage du buffer réseau pour éviter le freeze
    server.client().stop();
}

// --- Auth & escape ---
bool checkAuth() {
    if(!server.authenticate(admin_user.c_str(),admin_pwd.c_str())) {
        server.requestAuthentication();
        return false;
    }
    return true;
}

String htmlEscape(const String &s){
    String r=s;
    r.replace("&","&amp;");
    r.replace("<","&lt;");
    r.replace(">","&gt;");
    r.replace("\"","&quot;");
    r.replace("'","'");
    return r;
}

// --- Page /admin ---
void handleAdmin() {
    if(!checkAuth()) return;

    int gpio_state  = digitalRead(cfg_gpio_number);
    int gpio_state2 = digitalRead(cfg_gpio_number2);

    String page = "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta http-equiv='Cache-Control' content='no-cache, no-store, must-revalidate'><meta name='viewport' content='width=device-width, initial-scale=1'>"
                  "<style>body{font-family:Arial;padding:12px;} label{display:block;margin-top:8px;} input[type=text], input[type=password], textarea{width:100%;padding:8px;border-radius:6px;border:1px solid #ccc;box-sizing:border-box;} .row{display:flex;gap:8px;} .col{flex:1;} .btn{display:inline-block;padding:8px 12px;margin-top:10px;border-radius:6px;background:#2196F3;color:#fff;text-decoration:none;}</style>"
                  "<title>Ecodevice Gateway</title></head><body>"
                  "<h2>Paramètres Ecodevice Gateway</h2>"

                  "<h3>GPIO (principale) " + String(cfg_gpio_number) + " : <span id='gpio_state'>" + (gpio_state?"ON":"OFF") + "</span></h3>"
                  "<button onclick=\"toggleGPIO('on')\">Ouvrir</button> "
                  "<button onclick=\"toggleGPIO('off')\">Fermer</button><hr>"
                  "<h3>GPIO2 (secondaire) " + String(cfg_gpio_number2) + " : <span id='gpio_state2'>" + (gpio_state2?"ON":"OFF") + "</span></h3>"
                  "<p>(Cette sortie suit automatiquement la GPIO principale)</p><hr>"

                  "<form method='POST' action='/save'>"
                  "<h3>Paramètres globaux</h3>"
                  "<label>déclencheur type<input name='trigger_type' type='text' value='"+htmlEscape(cfg_trigger_type)+"'></label>"
                  "<label>Numéro GPIO principale<input name='gpio_number' type='text' value='"+String(cfg_gpio_number)+"'></label>"
                  "<label>Numéro GPIO secondaire<input name='gpio_number2' type='text' value='"+String(cfg_gpio_number2)+"'></label>"
                  "<label>Inverser logique GPIO principale<input type='checkbox' name='invert_gpio1' "+(cfg_invert_gpio1?"checked":"")+"></label>"
                  "<label>Inverser logique GPIO secondaire<input type='checkbox' name='invert_gpio2' "+(cfg_invert_gpio2?"checked":"")+"></label>"

                  "<h3>Paramètres Wifi SSID</h3>"
                  "<label>SSID WiFi<input name='ssid' type='text' value='"+htmlEscape(cfg_ssid)+"'></label>"
                  "<label>Mot de passe WiFi<input name='password' type='password' placeholder='(inchangé)'></label>"

                  "<h3>Paramètres Réseau</h3>"
                  "<label>Local IP<input name='local_ip' type='text' value='"+htmlEscape(cfg_local_ip)+"'></label>"
                  "<label>Gateway<input name='gateway' type='text' value='"+htmlEscape(cfg_gateway)+"'></label>"
                  "<label>Subnet<input name='subnet' type='text' value='"+htmlEscape(cfg_subnet)+"'></label>"
                  "<label>Primary DNS<input name='primaryDNS' type='text' value='"+htmlEscape(cfg_primaryDNS)+"'></label>"
                  "<label>Secondary DNS<input name='secondaryDNS' type='text' value='"+htmlEscape(cfg_secondaryDNS)+"'></label>"

                  "<h3>Paramètres SMTP</h3>"
                  "<label>SMTP host<input name='smtp' type='text' value='"+htmlEscape(cfg_smtp)+"'></label>"
                  "<label>SMTP port<input name='smtp_port' type='text' value='"+String(cfg_smtp_port)+"'></label>"
                  "<label>Adresse email (expediteur)<input name='email' type='text' value='"+htmlEscape(cfg_email)+"'></label>"
                  "<label>Mot de passe SMTP<input name='smtp_pwd' type='password' placeholder='(inchangé)'></label>"
                  "<label>Destinataires (séparés par ;) <textarea name='destinataires' rows='3'>"+htmlEscape(cfg_destinataires)+"</textarea></label>"

                  "<h3>Paramètres accès admin</h3>"
                  "<label>Admin user<input name='admin_user' type='text' value='"+htmlEscape(admin_user)+"'></label>"
                  "<label>Admin pwd<input name='admin_pwd' type='password' placeholder='(inchangé)'></label>"

                  "<div class='row'>"
                  "  <div class='col'><button class='btn' type='submit'>Sauvegarder</button></div>"
                  "  <div class='col'><button class='btn' type='button' onclick=\"window.location.href='/testsend'\">Tester envoi</button></div>"
                  "</div>"

                  "</form>"

                  "<hr><h3>Mise à jour OTA</h3>"
                  "<form method='POST' action='/update' enctype='multipart/form-data'>"
                  "<input type='file' name='firmware' accept='.bin' required>"
                  "<button class='btn' type='submit'>Mettre à jour</button>"
                  "</form>"

                  "<script>"
                  "function toggleGPIO(action){"
                  "  fetch('/gpio?action='+action+'&json=1')"
                  "  .then(response=>response.json())"
                  "  .then(data=>{"
                  "     document.getElementById('gpio_state').innerText=data.state?'ON':'OFF';"
                  "     document.getElementById('gpio_state2').innerText=data.state?'ON':'OFF';"
                  "  })"
                  "  .catch(err=>console.error(err));"
                  "} "
                  "setInterval(()=>{"
                  "  fetch('/gpio?json=1')"
                  "  .then(r=>r.json())"
                  "  .then(d=>{"
                  "     document.getElementById('gpio_state').innerText=d.state?'ON':'OFF';"
                  "     document.getElementById('gpio_state2').innerText=d.state?'ON':'OFF';"
                  "  });"
                  "},1000);"
                  "</script>"
                  "</body></html>";

    server.send(200,"text/html",page);
}

// --- Save ---
void handleSave() {
    if(!checkAuth()) return;

    if(server.hasArg("ssid")) cfg_ssid = server.arg("ssid");
    if(server.hasArg("password") && server.arg("password") != "") cfg_password = server.arg("password");

    if(server.hasArg("gpio_number")) {
        cfg_gpio_number = server.arg("gpio_number").toInt();
        pinMode(cfg_gpio_number, OUTPUT);    
        setGPIO(cfg_gpio_number, true, cfg_invert_gpio1);
    }

    if(server.hasArg("gpio_number2")) {
        cfg_gpio_number2 = server.arg("gpio_number2").toInt();
        pinMode(cfg_gpio_number2, OUTPUT);    
        setGPIO(cfg_gpio_number2, true, cfg_invert_gpio2);
    }

    if(server.hasArg("trigger_type")) cfg_trigger_type = server.arg("trigger_type");
    if(server.hasArg("smtp")) cfg_smtp = server.arg("smtp");
    if(server.hasArg("smtp_port")) cfg_smtp_port = server.arg("smtp_port").toInt();
    if(server.hasArg("email")) cfg_email = server.arg("email");
    if(server.hasArg("smtp_pwd") && server.arg("smtp_pwd") != "") cfg_smtp_pwd = server.arg("smtp_pwd");
    if(server.hasArg("destinataires")) cfg_destinataires = server.arg("destinataires");
    if(server.hasArg("local_ip")) cfg_local_ip = server.arg("local_ip");
    if(server.hasArg("gateway")) cfg_gateway = server.arg("gateway");
    if(server.hasArg("subnet")) cfg_subnet = server.arg("subnet");
    if(server.hasArg("primaryDNS")) cfg_primaryDNS = server.arg("primaryDNS");
    if(server.hasArg("secondaryDNS")) cfg_secondaryDNS = server.arg("secondaryDNS");
    if(server.hasArg("admin_user")) admin_user = server.arg("admin_user");
    if(server.hasArg("admin_pwd") && server.arg("admin_pwd") != "") admin_pwd = server.arg("admin_pwd");

    // ⭐ AJOUT — lecture inversion logique
    cfg_invert_gpio1 = server.hasArg("invert_gpio1");
    cfg_invert_gpio2 = server.hasArg("invert_gpio2");

    saveConfig();
    server.send(200,"text/html","<html><body><p>Config sauvegardée. Redémarrage...</p></body></html>");
    delay(200);
    ESP.restart();
}

// --- Test send ---
void handleTestSend() {
    if(!checkAuth()) return;

    ESP_Mail_Session session;
    session.server.host_name = cfg_smtp.c_str();
    session.server.port = cfg_smtp_port;
    session.login.email = cfg_email.c_str();
    session.login.password = cfg_smtp_pwd.c_str();
    session.login.user_domain = "";

    SMTP_Message message;
    message.sender.name = "ECODEVICE";
    message.sender.email = cfg_email.c_str();
    addAllRecipients(message, cfg_destinataires);
    message.subject = "Test d'envoi depuis ESP32 (UI)";
    message.text.content = "Ceci est un test envoyé depuis /admin";

    smtp.debug(1);
    String pageStart = "<html><body>";
    String pageEnd = "</body></html>";

    if(!smtp.connect(&session)) server.send(200,"text/html",pageStart+"<p style='color:red'>Erreur connexion SMTP</p>"+pageEnd);
    else if(!MailClient.sendMail(&smtp,&message)) server.send(200,"text/html",pageStart+"<p style='color:red'>Erreur envoi</p>"+pageEnd);
    else server.send(200,"text/html",pageStart+"<p style='color:green'>Email envoyé</p>"+pageEnd);

    smtp.closeSession();
}

// --- GPIO route ---
void handleGPIO() {
    if(!checkAuth()) return;
    if(server.hasArg("action")) {
        String action = server.arg("action");
        if(action == "on") {
            setGPIO(cfg_gpio_number, true, cfg_invert_gpio1);
            setGPIO(cfg_gpio_number2, true, cfg_invert_gpio2);
        } else if(action == "off") {
            setGPIO(cfg_gpio_number, false, cfg_invert_gpio1);
            setGPIO(cfg_gpio_number2, false, cfg_invert_gpio2);
        }
    }
    int state = digitalRead(cfg_gpio_number);
    if(server.hasArg("json") && server.arg("json")=="1") {
        server.send(200,"application/json","{\"state\":"+String(state)+"}");
        return;
    }
    server.send(200,"text/html","<html><body><h3>GPIO "+String(cfg_gpio_number)+" : "+(state?"ON":"OFF")+"</h3>"
                "<a href='/gpio?action=on'>Ouvrir</a> | <a href='/gpio?action=off'>Fermer</a></body></html>");
}

// --- OTA upload handler ---
void handleFirmwareUpload() {
    if (!checkAuth()) return;

    HTTPUpload& upload = server.upload();

    if (upload.status == UPLOAD_FILE_START) {
        Serial.setDebugOutput(true);
        Serial.printf("Mise à jour OTA commencée : %s\n", upload.filename.c_str());
        if (!Update.begin(UPDATE_SIZE_UNKNOWN)) Update.printError(Serial);

    } else if (upload.status == UPLOAD_FILE_WRITE) {
        if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) Update.printError(Serial);

    } else if (upload.status == UPLOAD_FILE_END) {
        if (Update.end(true)) Serial.printf("Mise à jour OTA terminée : %u octets\n", upload.totalSize);
        else Update.printError(Serial);
        Serial.setDebugOutput(false);

    } else if (upload.status == UPLOAD_FILE_ABORTED) {
        Update.end();
        Serial.println("Mise à jour OTA annulée.");
    }
}

void handleUpdatePage() {
    if (!checkAuth()) return;

    if (Update.hasError()) {
        server.send(200, "text/html",
          "<html><body><p style='color:red'>Échec de la mise à jour</p></body></html>");
    } else {
        server.send(200, "text/html",
          "<html><body><p style='color:green'>Mise à jour réussie, redémarrage...</p></body></html>");
        delay(500);
        ESP.restart();
    }
}

// --- AP fallback ---
void startAPMode() {
    isAPMode = true; // ⭐ Ajout
    Serial.println("Demarrage AP...");
    WiFi.disconnect(true);
    delay(200);
    WiFi.mode(WIFI_AP_STA);
    delay(50);

    IPAddress apIP(192,168,4,1);
    IPAddress apGateway = apIP;
    IPAddress apMask(255,255,255,0);
    if(!WiFi.softAPConfig(apIP, apGateway, apMask)) Serial.println("softAPConfig FAILED");
    else Serial.println("softAPConfig OK");
    delay(50);

    if(strlen(AP_PASSWORD) == 0) WiFi.softAP(AP_SSID);
    else WiFi.softAP(AP_SSID, AP_PASSWORD);

    delay(300);
    Serial.print("AP SSID: "); Serial.println(AP_SSID);
    Serial.print("AP IP (softAPIP): "); Serial.println(WiFi.softAPIP());
    Serial.print("Num stations connected: "); Serial.println(WiFi.softAPgetStationNum());
}

// --- WiFi connect ---
bool attemptWiFiConnect() {
    Serial.printf("Connexion a %s\n", cfg_ssid.c_str());
    WiFi.mode(WIFI_STA);
    WiFi.begin(cfg_ssid.c_str(), cfg_password.c_str());
    unsigned long start = millis();
    while(WiFi.status() != WL_CONNECTED && (millis()-start) < WIFI_CONNECT_TIMEOUT_MS) {
        Serial.print(".");
        delay(200);
    }
    Serial.println();
    if(WiFi.status() == WL_CONNECTED) {
        Serial.println("WiFi connecté");
        Serial.print("IP: "); Serial.println(WiFi.localIP());
        return true;
    }
    Serial.println("WiFi non connecté -> fallback AP");
    return false;
}

// ⭐ AJOUT — Reconnexion automatique WiFi
int retryCount = 0; // À mettre en haut du code avec les globales

void checkWiFiReconnect() {
    if (isAPMode) return; // Si on est déjà en AP, on ne fait rien

    if (WiFi.status() != WL_CONNECTED) {
        static unsigned long lastAttempt = 0;
        if (millis() - lastAttempt > 10000) { // Tentative toutes les 10s
            lastAttempt = millis();
            Serial.println("WiFi perdu -> tentative de reconnexion...");
           
            if (attemptWiFiConnect()) {
                retryCount = 0; // Succès : on réinitialise
            } else {
                retryCount++;
                delay(100); // ⭐ Petit repos après un échec de négociation WiFi
                Serial.printf("Échec de reconnexion n°%d\n", retryCount);
            }

            if (retryCount >= 30) { // Après 30 échecs (300s)
                Serial.println("Trop d'échecs -> Bascule en mode AP pour configuration");
                startAPMode();
            }
        }
    } else {
        retryCount = 0; // On est connecté, on remet le compteur à zéro
    }
}
// --- Routes ---
void setupRoutes() {
    server.on("/", handleAdmin);          // <-- admin devient la page racine
    server.on("/actions", handleRoot);    // <-- ancien root va dans /actions
    server.on("/save", HTTP_POST, handleSave);
    server.on("/testsend", HTTP_GET, handleTestSend);
    server.on("/gpio", handleGPIO);
    server.on("/update", HTTP_POST, handleUpdatePage, handleFirmwareUpload);
    server.begin();
    Serial.println("Serveur web démarré");
}

// --- Setup ---
void setup() {
    Serial.begin(115200);
    delay(1000);

    if(!LittleFS.begin(true)) Serial.println("LittleFS mount failed!");
    else Serial.println("LittleFS OK");

    loadConfig();

    // Configuration des deux GPIO (GPIO2 est un clone)
    pinMode(cfg_gpio_number, OUTPUT);
    setGPIO(cfg_gpio_number, true, cfg_invert_gpio1);

    pinMode(cfg_gpio_number2, OUTPUT);
    setGPIO(cfg_gpio_number2, true, cfg_invert_gpio2);

    if(cfg_local_ip.length() > 0){
        IPAddress local = ipFromString(cfg_local_ip);
        IPAddress gw = ipFromString(cfg_gateway);
        IPAddress mask = ipFromString(cfg_subnet);
        IPAddress pdns = ipFromString(cfg_primaryDNS);
        IPAddress sdns = ipFromString(cfg_secondaryDNS);
        if(!WiFi.config(local, gw, mask, pdns, sdns)) Serial.println("Echec config IP statique");
        else Serial.println("Config IP statique appliquée");
    }

    bool wifiOk = attemptWiFiConnect();
    if(!wifiOk) {
        startAPMode();
        setupRoutes();
    } else {
        setupRoutes();
        configTzTime("CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00","fr.pool.ntp.org","time.nist.gov");
        unsigned long t0 = millis();
        const unsigned long NTPTIMEOUT = 5000;
        while (millis() - t0 < NTPTIMEOUT && time(nullptr) < ESP_MAIL_CLIENT_VALID_TS) { delay(100);}
        if (time(nullptr) >= ESP_MAIL_CLIENT_VALID_TS) Serial.println("NTP ok");
        else Serial.println("NTP non synchro (timeout)");
    }
}

// --- Loop ---
void loop() {
    static unsigned long lastHeartbeat = 0;
    if (millis() - lastHeartbeat > 5000) {
        lastHeartbeat = millis();
        Serial.printf("Heartbeat: RAM libre = %u octets, WiFi Status = %d\n", ESP.getFreeHeap(), WiFi.status());
    }

    server.handleClient();
    checkWiFiReconnect();
    delay(1); // ⭐ Crucial : rend la main au système (FreeRTOS) pendant 1ms
}
charge cette version  2.06 20/12/25, et avant de tout fermer fait des tests en gardant le moniteur serie arduino ouvert pour voir les logs qui s 'affichent
de mon coté ca fonctionne nickel , la grosse difference peut etre avec toi c est que de mon coté j envoie les push depuis un navigateur et non pas depuis ecodevice, si de ton coté ca ne fonctionne toujours pas , essaie d'envoyer les push depuis un navigateur voir si ca se comporte pareil
alors retour apres la 2.05 foireuse 

réinstallation de la 2.03 : resultat les pushs fonctionnent , les ouvertures et fermetures en manuel fonctionnent , le seul probleme pas de réconnexion si perte du wifi ( coupore de courant ou autre)

ensuite en ota version 2.04 : resultat : les pushs fonctionnent , le wifi se reconncte seul meme apres coupûre de courant , probleme apparu avec cette version perte du serveur apres commande manuel d'ouverture.

ensuite version 2.06 resultat :  les pushs fonctionnent , le wifi egalement , la réouverture à fonctionnée SEULEMENT UNE FOIS! depuis on retrouve le probleme de la 2.04.
ce que je trouve bizarre c'est que le serveur garde en mémoire la liste des destinataires alors que dans le fichier ino , il n'y en a qun seul de renseigné ( il doit y avoir une cache sur esp32 qui ne sefface pas entre les versions)

le probleme doit se situer en tre la version 2.03 et la 2.04.

jessai de remettre en usb et voir les logs dans arduino.

qu'en penses tu?