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 - 20-12-2025

Si tu changes les destinataires il faut bien penser à faire un ctrl F5 pour faire un refresh et faire une sauvegarde
Si j ai bien compris , cela a fonctionné… 1 fois ? C est surprenant de mon côté j ai fait plein de tests sans soucis.
Oui il n y a que les logs usb qui m indiqueront ou ça plante…
En 2.03 tu n as pas le soucis c est bien ça ? Juste le soucis de ne pas se reconnecter au wifi si celui ci tombe ? Si c est le cas c est une bonne piste…


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

(20-12-2025, 01:47 PM)Lolo69 a écrit : Si tu changes les destinataires il faut bien penser à faire un ctrl F5 pour faire un refresh et faire une sauvegarde
Si j ai bien compris , cela a fonctionné… 1 fois ? C est surprenant de mon côté j ai fait plein de tests sans soucis.
Oui il n y a que les logs usb qui m indiqueront ou ça plante…
En 2.03 tu n as pas le soucis c est bien ça ? Juste le soucis de ne pas se reconnecter au wifi si celui ci tombe ? Si c est le cas c est une bonne piste…

Oui pour la version 2,.03 c'est exactement ça,.
On ne sais jamais mais je vais mettre une alimentation plus puissance actuellement 1a  vais essayer avec 2

alors j’ai rebranché esp32 en usb , sans rien d’autre de raccordé dessus la il fonctionne niquel a chaque requête manuel il on et off sans perte du serveur.
problème de raccordement ?


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

Je pense pas à un problème d alimentation.
Relève moi plutôt les logs de la 2.04
Je regarde de plus pres ce qui peut clocher sur la reconnexion wifi


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

(20-12-2025, 02:13 PM)Lolo69 a écrit : Je pense pas à un problème d alimentation.
Relève moi plutôt les logs de la 2.04
Je regarde de plus pres ce qui peut clocher sur la reconnexion wifi

alors après avoir vu que esp32 fonctionnait bien sans rien de raccordé a ces broches , j’ai revu mon raccordement,
a savoir que javais été chercher le ground coté gauche du bornier entre les gpio 12 et 13 
, alors que les gpio utilisés le 2 et 23 sont du coté droit du bornier , j’ai donc repris le ground sur le bornier droit en haut le premier juste au dessus du 23  , et bien apparemment le problème semble en parti résolu ça fonctionne a chaque demande manuel douverture ou fermeture .

je ne crie pas victoire tout de suite mais je pense que c’est la solution , je te tiens au courant en fin daperm pour des nouvelles après essais.

si seulement c’était  ça enfin !!!!


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

tu me tiendras au courant, j'ai du mal à percevoir qu'un problème hard pourrais generer un probleme soft... mais bon .... tient moi au courant car effectivement dans mes tests ca fonctionne tout nickel mais il est vrai que je n 'ai pas de connection hw


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

(20-12-2025, 02:51 PM)Lolo69 a écrit : tu me tiendras au courant, j'ai du mal à percevoir qu'un problème hard pourrais generer un probleme soft... mais bon .... tient moi au courant car effectivement dans mes tests ca fonctionne tout nickel mais il est vrai que je n 'ai pas de connection hw

Oui pas de soucis, je te tiens au courant d'un éventuel retournement de situation ???


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

Alors dernière version installée la 2.06. Poussée par usb, aucunes erreurs dans les logs j'ai juste vu un ping toutes les 5 secondes, je pense pour le wifi ?
le problème semble résolu, j’ai fait quelques modifications hardware : choix du gpio22 en primaire et 23 en secondaire + branchement du ground coté droit esp32 pour les deux gpio .
pour le bouton reset raccordement sur EN et ground coté gauche de esp32 , et nouveau chargeur 2A.
Avec toutes ces modifications les pushs fonctionnent bien, le wifi se reconnecte mème après une coupure d’alimentation box.
Ouverture et fermeture manuel fonctionne aussi mais il faut attendre plusieurs secondes (environ 10) quitter le navigateur et rafraichir entre ces deux actions, comme ça aucun problème, sans rafraichir toujours perte du serveur, ce n’est pas très grave car on y va que très rarement en fait juste une fois semaine pour vérifier le fonctionnement de l'ensemble.
Voila cette version est la plus aboutie je pense. le fait d'attendre quelques secondes doit être un réglage dans le soft ?
Encore merci Lolo pour tout le temps consacré à des débutants comme moi , mais j’ai déjà appris beaucoup pendant nos échanges.
Bonne fête de fin d'année à toi et à tous le monde .
on garde le contact au cas ou.
Tupolev


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

Je n'ai pas tout compris tes explications mais si tu veux essayer la 2.07 suivante , cette version devrait etre à la fois plus stable et plus performante et gommer les petits inconvénients que tu as relevé
Code :
//  version  2.07 FIXED 20/12/25
/*
Parametrages depuis page /
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

CORRECTIONS v2.06 FIXED:
- Reconnexion WiFi NON BLOQUANTE (machine à états)
- Suppression delay(50) et server.client().stop() dans handleRoot()
- Remplacement delay(1) par yield() dans loop()
*/

#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
bool isAPMode = false;
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";

// ⭐ CORRECTION : Variables pour reconnexion NON BLOQUANTE
int retryCount = 0;
unsigned long lastReconnectAttempt = 0;
bool reconnectInProgress = false;
unsigned long reconnectStartTime = 0;

// 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");
}

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);
        setGPIO(cfg_gpio_number2, false, cfg_invert_gpio2);
        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();
    // ⭐ CORRECTION : Suppression de delay(50) et server.client().stop()
    message.clear();
    server.send(200,"text/html",page);
}

// --- 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");

    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;
    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;
}

// ⭐ CORRECTION : Reconnexion WiFi NON BLOQUANTE
void checkWiFiReconnect() {
    if (isAPMode) return;

    if (!reconnectInProgress) {
        // État 1 : Vérification déconnexion
        if (WiFi.status() != WL_CONNECTED) {
            if (millis() - lastReconnectAttempt > 10000) {
                Serial.println("WiFi perdu -> tentative de reconnexion...");
                WiFi.disconnect();
                WiFi.begin(cfg_ssid.c_str(), cfg_password.c_str());
                reconnectInProgress = true;
                reconnectStartTime = millis();
                lastReconnectAttempt = millis();
            }
        } else {
            retryCount = 0;
        }
    } else {
        // État 2 : En cours de reconnexion (non bloquant)
        if (WiFi.status() == WL_CONNECTED) {
            Serial.println("WiFi reconnecté !");
            reconnectInProgress = false;
            retryCount = 0;
        } else if (millis() - reconnectStartTime > 10000) {
            Serial.printf("Échec de reconnexion n°%d\n", retryCount + 1);
            reconnectInProgress = false;
            retryCount++;

            if (retryCount >= 30) {
                Serial.println("Trop d'échecs -> Bascule en mode AP pour configuration");
                startAPMode();
            }
        }
    }
}

// --- Routes ---
void setupRoutes() {
    server.on("/", handleAdmin);
    server.on("/actions", handleRoot);
    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();

    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();
    yield();
}



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

(20-12-2025, 09:59 PM)Lolo69 a écrit : Je n'ai pas tout compris tes explications mais si tu veux essayer la 2.07 suivante , cette version devrait etre à la fois plus stable et plus performante et gommer les petits inconvénients que tu as relevé
Code :
//  version  2.07 FIXED 20/12/25
/*
Parametrages depuis page /
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

CORRECTIONS v2.06 FIXED:
- Reconnexion WiFi NON BLOQUANTE (machine à états)
- Suppression delay(50) et server.client().stop() dans handleRoot()
- Remplacement delay(1) par yield() dans loop()
*/

#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
bool isAPMode = false;
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";

// ⭐ CORRECTION : Variables pour reconnexion NON BLOQUANTE
int retryCount = 0;
unsigned long lastReconnectAttempt = 0;
bool reconnectInProgress = false;
unsigned long reconnectStartTime = 0;

// 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");
}

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);
        setGPIO(cfg_gpio_number2, false, cfg_invert_gpio2);
        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();
    // ⭐ CORRECTION : Suppression de delay(50) et server.client().stop()
    message.clear();
    server.send(200,"text/html",page);
}

// --- 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");

    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;
    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;
}

// ⭐ CORRECTION : Reconnexion WiFi NON BLOQUANTE
void checkWiFiReconnect() {
    if (isAPMode) return;

    if (!reconnectInProgress) {
        // État 1 : Vérification déconnexion
        if (WiFi.status() != WL_CONNECTED) {
            if (millis() - lastReconnectAttempt > 10000) {
                Serial.println("WiFi perdu -> tentative de reconnexion...");
                WiFi.disconnect();
                WiFi.begin(cfg_ssid.c_str(), cfg_password.c_str());
                reconnectInProgress = true;
                reconnectStartTime = millis();
                lastReconnectAttempt = millis();
            }
        } else {
            retryCount = 0;
        }
    } else {
        // État 2 : En cours de reconnexion (non bloquant)
        if (WiFi.status() == WL_CONNECTED) {
            Serial.println("WiFi reconnecté !");
            reconnectInProgress = false;
            retryCount = 0;
        } else if (millis() - reconnectStartTime > 10000) {
            Serial.printf("Échec de reconnexion n°%d\n", retryCount + 1);
            reconnectInProgress = false;
            retryCount++;

            if (retryCount >= 30) {
                Serial.println("Trop d'échecs -> Bascule en mode AP pour configuration");
                startAPMode();
            }
        }
    }
}

// --- Routes ---
void setupRoutes() {
    server.on("/", handleAdmin);
    server.on("/actions", handleRoot);
    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();

    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();
    yield();
}

Oui lolo bien volontiers j'essaie en rentrant dans la soirée te tiens au courant dès que possible ??


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

Bonjour Lolo,
petit retour après l'installation de la dernière version 2.07 FIXED 20/12/25
Tout fonctionne parfaitement, les pushs OK, ouverture et fermeture manuel OK, plus de perte de serveur après la réouverture suite à un push de fermeture, plus de temps d'attente obligatoire entre les actions, récupération du wifi instantané après redémarrage box ou même coupure de courant.
Donc effectivement cette version est la plus aboutie.

Je vais pouvoir proposer ce système à mon entourage, mais il faudrait que je puisse remplacer l'écodevice (car assez cher, moi je l'avais déjà) directement pas esp32 qui du coup ferait le comptage d'impulsion eau et l’envoie de l’alerte mail , mais c’est une autre histoire et je ne sais même pas si c’est possible?

Merci encore pour ton aide précieuse et ton savoir, j’ai appris de petits trucs avec toi c’est vraiment passionnant.
Je te souhaites de bonnes fêtes de fin d'année.
Bien cordialement
Tupolev