Module ESP32-écran avec données météo publiques

De Wiki de Mémoire Vive
Version datée du 28 octobre 2025 à 21:56 par Fred (discussion | contributions)
(diff) ← Version précédente | Voir la version actuelle (diff) | Version suivante → (diff)
Aller à la navigation Aller à la recherche

Une version plus générique de l'utilisation du module écran ESP32, avec des données météo publiques (Openweathermap).


On peut faire fonctionner ce code n'importe où en reparamétrant le wifi.

#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <TFT_eSPI.h>
#include <TimeLib.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "time.h"

// ====== Variables globales ======
WebServer server(80);
Preferences preferences;
TFT_eSPI tft = TFT_eSPI();

String ssid, password;
String apiKey, lat, lon;
bool wifiConfigured = false;
bool weatherConfigured = false;

// ====== Dashboard ======
const int screenW = 480;
const int screenH = 320;
const int cols = 4;
const int rows = 4;
const int boxW = screenW / cols - 10;
const int boxH = screenH / rows - 10;

struct Box {
  const char* label;
  char value[32];
  bool clickable;
};

Box boxes[rows * cols] = {
  {"Temp Locale", "--", false},
  {"Vent", "--", false},
  {"Heure", "00:00", false},
  {"Wi-Fi", "OFF", false},
  {"Lever Soleil", "--", false},
  {"Coucher Soleil", "--", false},
  {"Humid.", "", false},
  {"Pression", "", false},
  {"", "", false},
  {"Ciel", "", false},
  {"Case11", "", false},
  {"Case12", "", false},
  {"Case13", "", false},
  {"Case14", "", false},
  {"Case15", "", false},
  {"Reset Meteo", "Tap", true}   // ← Case tactile

};

// ====== HTML Wi-Fi ======
const char* htmlWiFiForm = R"rawliteral(
<!DOCTYPE html>
<html>
<body>
<h2>Configurer le Wi-Fi</h2>
<form action="/save" method="POST">
  SSID:<br>
  <input type="text" name="ssid"><br>
  Mot de passe:<br>
  <input type="password" name="password"><br><br>
  <input type="submit" value="Sauvegarder">
</form>
</body>
</html>
)rawliteral";

// ====== HTML OpenWeather ======
const char* htmlApiForm = R"rawliteral(
<!DOCTYPE html>
<html>
<body>
<h2>Configurer OpenWeather</h2>
<form action="/saveApi" method="POST">
  API Key:<br>
  <input type="text" name="apikey"><br>
  Latitude:<br>
  <input type="text" name="lat"><br>
  Longitude:<br>
  <input type="text" name="lon"><br><br>
  <input type="submit" value="Sauvegarder">
</form>
</body>
</html>
)rawliteral";

// ====== Fonctions Wi-Fi ======
void saveWiFi(String s, String p){
  preferences.begin("wifi", false);
  preferences.putString("ssid", s);
  preferences.putString("password", p);
  preferences.end();
  Serial.println("[Wi-Fi] Paramètres sauvegardés en flash");
}

bool loadWiFi(){
  preferences.begin("wifi", true);
  ssid = preferences.getString("ssid", "");
  password = preferences.getString("password", "");
  preferences.end();
  Serial.print("[Wi-Fi] Chargement depuis flash -> SSID: "); Serial.println(ssid);
  return ssid != "" && password != "";
}

void setupWiFiAP(){
  WiFi.softAP("ESP32_Config");
  Serial.println("[Wi-Fi] Point d’accès activé : ESP32_Config");
  Serial.println("[Wi-Fi] Connectez-vous à 192.168.4.1 pour configurer le Wi-Fi");

  server.on("/", [](){ server.send(200, "text/html", htmlWiFiForm); });

  server.on("/save", HTTP_POST, [](){
    if(server.hasArg("ssid") && server.hasArg("password")){
      ssid = server.arg("ssid");
      password = server.arg("password");
      saveWiFi(ssid, password);
      server.send(200, "text/html", "<h2>Wi-Fi sauvegarde ok, redemarrage...</h2>");
      Serial.println("[Wi-Fi] Formulaire reçu -> redemarrage");
      delay(2000);
      ESP.restart();
    }
  });

  server.begin();
  Serial.println("[Wi-Fi] Serveur Web démarré pour configuration");
}

void connectWiFi(){
  if(loadWiFi()){
    Serial.print("[Wi-Fi] Tentative de connexion à : "); Serial.println(ssid);
    WiFi.begin(ssid.c_str(), password.c_str());
    int tries = 0;
    while(WiFi.status() != WL_CONNECTED && tries < 20){
      delay(500);
      Serial.print(".");
      tries++;
    }
    if(WiFi.status() == WL_CONNECTED){
      Serial.println("\n[Wi-Fi] Connecté avec succès!");
      wifiConfigured = true;
      strcpy(boxes[3].value, "ON");
      return;
    }
    Serial.println("\n[Wi-Fi] Échec de connexion Wi-Fi");
  }
  setupWiFiAP();
}

// ====== Fonctions OpenWeather ======
void saveWeatherConfig(String key, String latitude, String longitude){
  preferences.begin("openweather", false);
  preferences.putString("apikey", key);
  preferences.putString("lat", latitude);
  preferences.putString("lon", longitude);
  preferences.end();
  Serial.println("[OpenWeather] Config sauvegardée en flash");
}

bool loadWeatherConfig(){
  preferences.begin("openweather", true);
  apiKey = preferences.getString("apikey", "");
  lat = preferences.getString("lat", "");
  lon = preferences.getString("lon", "");
  preferences.end();
  Serial.print("[OpenWeather] Chargement depuis flash -> API: "); Serial.println(apiKey);
  return apiKey != "" && lat != "" && lon != "";
}

void configWeather(){
  WiFi.softAP("ESP32_Config_API");
  Serial.println("[OpenWeather] Point d’accès activé : ESP32_Config_API");
  Serial.println("[OpenWeather] Connectez-vous à 192.168.4.1 pour configurer OpenWeather");

  server.on("/", [](){ server.send(200, "text/html", htmlApiForm); });

  server.on("/saveApi", HTTP_POST, [](){
    if(server.hasArg("apikey") && server.hasArg("lat") && server.hasArg("lon")){
      apiKey = server.arg("apikey");
      lat = server.arg("lat");
      lon = server.arg("lon");
      saveWeatherConfig(apiKey, lat, lon);
      server.send(200, "text/html", "<h2>Config meteo sauvegarde ok, redemarrage...</h2>");
      Serial.println("[OpenWeather] Formulaire reçu -> redemarrage");
      delay(2000);
      ESP.restart();
    }
  });

  server.begin();
  Serial.println("[OpenWeather] Serveur Web démarré pour configuration");
}

// ====== OpenWeather ======
void updateWeather() {
  if (WiFi.status() != WL_CONNECTED || apiKey == "") return;

  HTTPClient http;
  String url = "http://api.openweathermap.org/data/2.5/weather?lat=" + lat + "&lon=" + lon +
               "&units=metric&appid=" + apiKey + "&lang=fr";
  Serial.print("[OpenWeather] URL: "); Serial.println(url);

  http.begin(url);
  int httpCode = http.GET();
  Serial.print("[OpenWeather] HTTP code: "); Serial.println(httpCode);

  if (httpCode == 200) {
    String payload = http.getString();
    Serial.println("[OpenWeather] JSON complet:");
    Serial.println(payload);

    StaticJsonDocument<2048> doc;
    DeserializationError error = deserializeJson(doc, payload);
    if (error) {
      Serial.print("[OpenWeather] Erreur JSON: "); Serial.println(error.c_str());
      http.end();
      return;
    }

    // Données météo
    float temp       = doc["main"]["temp"];
    float feels_like = doc["main"]["feels_like"];
    float wind       = doc["wind"]["speed"];
    int windDeg      = doc["wind"]["deg"];
    int humidity     = doc["main"]["humidity"];
    int pressure     = doc["main"]["pressure"];
    long sunrise     = doc["sys"]["sunrise"];
    long sunset      = doc["sys"]["sunset"];
    const char* city = doc["name"];
    const char* weatherDesc = doc["weather"][0]["description"];

// Conversion lever/coucher soleil en heure locale (NTP + fuseau pris en compte)
struct tm ts;
char buf[10];

time_t sunrise_ts = (time_t)sunrise;
time_t sunset_ts  = (time_t)sunset;

// Lever du soleil
localtime_r(&sunrise_ts, &ts);
strftime(buf, sizeof(buf), "%H:%M", &ts);
snprintf(boxes[4].value, sizeof(boxes[4].value), "%s", buf);

// Coucher du soleil
localtime_r(&sunset_ts, &ts);
strftime(buf, sizeof(buf), "%H:%M", &ts);
snprintf(boxes[5].value, sizeof(boxes[5].value), "%s", buf);


    // Mettre à jour le tableau de bord
    snprintf(boxes[0].value, sizeof(boxes[0].value), "%.1f°C", temp);
    snprintf(boxes[1].value, sizeof(boxes[1].value), "%.1f m/s %d°", wind, windDeg);
    snprintf(boxes[6].value, sizeof(boxes[6].value), "%d%%", humidity);
    snprintf(boxes[7].value, sizeof(boxes[7].value), "%dhPa", pressure);
    snprintf(boxes[8].value, sizeof(boxes[8].value), "%s", city);
    snprintf(boxes[9].value, sizeof(boxes[9].value), "%s", weatherDesc);

    // Traces console
    Serial.print("[OpenWeather] Température: "); Serial.println(temp);
    Serial.print("[OpenWeather] Vent: "); Serial.print(wind); Serial.print(" m/s "); Serial.println(windDeg);
    Serial.print("[OpenWeather] Humidité: "); Serial.println(humidity);
    Serial.print("[OpenWeather] Pression: "); Serial.println(pressure);
    Serial.print("[OpenWeather] Lever soleil: "); Serial.println(boxes[4].value);
    Serial.print("[OpenWeather] Coucher soleil: "); Serial.println(boxes[5].value);
    Serial.print("[OpenWeather] Ville: "); Serial.println(city);
    Serial.print("[OpenWeather] Description: "); Serial.println(weatherDesc);

  } else {
    Serial.println("[OpenWeather] Erreur HTTP ou clé API invalide");
  }

  http.end();
}

// ====== TFT ======
void drawBox(int x, int y, int w, int h, Box b){
  tft.drawRect(x, y, w, h, b.clickable ? TFT_GREEN : TFT_WHITE);  

  // ===== Label =====
  tft.setTextFont(2);
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.setCursor(x + 5, y + 5);
  tft.print(b.label);

  // ===== Valeur =====
  int fontSize = 4; // taille initiale
  tft.setTextFont(fontSize);
  int tw = tft.textWidth(b.value);

  // Réduire la taille si le texte dépasse la largeur de la case
  while(tw > w - 10 && fontSize > 1){
    fontSize--;
    tft.setTextFont(fontSize);
    tw = tft.textWidth(b.value);
  }

  int th = tft.fontHeight();
  int cx = x + (w - tw) / 2;
  int cy = y + (h - th) / 2 + th / 4;
  tft.setCursor(cx, cy);

  // === Couleur spéciale pour Wi-Fi ===
  if (strcmp(b.label, "Wi-Fi") == 0) {
    if (strcmp(b.value, "ON") == 0)
      tft.setTextColor(TFT_GREEN, TFT_BLACK);
    else
      tft.setTextColor(TFT_RED, TFT_BLACK);
  } else {
    tft.setTextColor(TFT_CYAN, TFT_BLACK);
  }

  tft.print(b.value);
}

void drawAllBoxes(){
  for (int r = 0; r < rows; r++){
    for (int c = 0; c < cols; c++){
      int index = r * cols + c;
      int x = c * (boxW + 10) + 5;
      int y = r * (boxH + 10) + 5;
      drawBox(x, y, boxW, boxH, boxes[index]);
    }
  }
}

// ====== Setup ======
void setup(){
  Serial.begin(115200);
  delay(2000);
  Serial.println("=== ESP32 Tableau de bord météo ===");
  Serial.print("Nom du fichier source : "); Serial.println(__FILE__);

  tft.init();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);

  connectWiFi();

  if(!loadWeatherConfig()){
    configWeather();
  } else {
    weatherConfigured = true;
    Serial.println("[Setup] OpenWeather déjà configuré");
    // Appel immédiat pour récupérer la météo dès le démarrage
    Serial.println("[Setup] Premier appel OpenWeather immédiat");
    updateWeather();    
  }

// Config NTP et attente de synchronisation
configTime(3600, 3600, "pool.ntp.org", "time.nist.gov");
Serial.println("[Setup] NTP configuré, attente de synchronisation...");

struct tm timeinfo;
int retry = 0;
const int retry_max = 10;
while(!getLocalTime(&timeinfo) && retry < retry_max){
    Serial.println("[Setup] En attente de la synchronisation NTP...");
    delay(1000);
    retry++;
}

if(retry == retry_max){
    Serial.println("[Setup] Attention: NTP non synchronisé !");
} else {
    Serial.println("[Setup] Heure NTP synchronisée !");
    if(weatherConfigured){
        Serial.println("[Setup] Premier appel OpenWeather immédiat");
        updateWeather();
    }
}

  drawAllBoxes();
}

// ====== Loop ======
unsigned long lastUpdate = 0;
unsigned long lastWeather = 0;

void loop() {
  server.handleClient(); // Toujours gérer le serveur

  if (wifiConfigured && weatherConfigured) {
    // Heure toutes les 10s
    if (millis() - lastUpdate > 10000) {
      lastUpdate = millis();
      struct tm timeinfo;
      if (getLocalTime(&timeinfo)) {
        snprintf(boxes[2].value, sizeof(boxes[2].value), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
      }
    }

    // Météo toutes les 10 min
    if (millis() - lastWeather > 600000) {
      lastWeather = millis();
      Serial.println("[Loop] Mise à jour météo...");
      updateWeather();
    }

    drawAllBoxes();

    // === Détection tactile ===
    uint16_t tx, ty;
    if (tft.getTouch(&tx, &ty)) {
      for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
          int index = r * cols + c;
          int boxX = c * (boxW + 10) + 5;
          int boxY = r * (boxH + 10) + 5;
          if (tx >= boxX && tx <= boxX + boxW &&
              ty >= boxY && ty <= boxY + boxH &&
              boxes[index].clickable) {

            if (strcmp(boxes[index].label, "Reset Meteo") == 0) {
              Serial.println("[Touch] Réinitialisation OpenWeather !");
              preferences.begin("openweather", false);
              preferences.clear();
              preferences.end();

              weatherConfigured = false; // la météo n'est plus configurée

              // Affiche message TFT
              tft.fillScreen(TFT_BLACK);
              tft.setTextColor(TFT_RED, TFT_BLACK);
              tft.setTextDatum(MC_DATUM); // centre le texte
              tft.drawString("Reinit Config Meteo\nConnect Wi-Fi ESP32_Config_API\nPuis 192.168.4.1", 
                             screenW/2, screenH/2, 2);
            }
          }
        }
      }
    }

    delay(200); // anti-rebond
  } 
  else if (!weatherConfigured) {
    // Affiche le message de réinitialisation en continu si besoin
    tft.fillScreen(TFT_BLACK);
    tft.setTextColor(TFT_RED, TFT_BLACK);
    tft.setTextDatum(MC_DATUM);
    tft.drawString("Reinit Config Meteo\nConnect Wi-Fi ESP32_Config_API\nPuis 192.168.4.1", 
                   screenW/2, screenH/2, 2);
  }
}

Code avec 3 écrans, météo, heure et réveil.

#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <TFT_eSPI.h>
#include <TimeLib.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "time.h"
#define PAGE_MAIN     0
#define PAGE_Heure    1
#define PAGE_METEO    2
#define PAGE_Reveil   3

// ====== Variables globales ======
WebServer server(80);
Preferences preferences;
TFT_eSPI tft = TFT_eSPI();

String ssid, password;
String apiKey, lat, lon;
bool wifiConfigured = false;
bool weatherConfigured = false;

// Variables pour le réveil
int heureReveil = 7;  // Heure par défaut
int minuteReveil = 0; // Minute par défaut
bool reveilActif = false;
char timeStrReveil[6]; // Format "HH:MM" pour l'affichage du réveil

// ====== Dashboard ======
const int screenW = 480;
const int screenH = 320;
const int cols = 4;
const int rows = 4;
const int boxW = screenW / cols - 10;
const int boxH = screenH / rows - 10;

struct Box {
  const char* label;
  char value[32];
  bool clickable;
};

Box boxes[rows * cols] = {
  {"Temp Locale", "--", false},
  {"Vent", "--", false},
  {"Heure", "00:00", true},
  {"Wi-Fi", "OFF", false},
  {"Lever Soleil", "--", false},
  {"Coucher Soleil", "--", false},
  {"Humid.", "", false},
  {"Pression", "", false},
  {"", "", false},
  {"Ciel", "", false},
  {"Case11", "", false},
  {"Case12", "", false},
  {"Reveil", "", true},     // La valeur sera mise à jour avec timeStrReveil
  {"Case14", "", false},
  {"Reset Wifi", "Tap", true},
  {"Reset Meteo", "Tap", true}   // ← Case tactile

};

// ====== HTML Wi-Fi ======
const char* htmlWiFiForm = R"rawliteral(
<!DOCTYPE html>
<html>
<body>
<h2>Configurer le Wi-Fi</h2>
<form action="/save" method="POST">
  SSID:<br>
  <input type="text" name="ssid"><br>
  Mot de passe:<br>
  <input type="password" name="password"><br><br>
  <input type="submit" value="Sauvegarder">
</form>
</body>
</html>
)rawliteral";

// ====== HTML OpenWeather ======
const char* htmlApiForm = R"rawliteral(
<!DOCTYPE html>
<html>
<body>
<h2>Configurer OpenWeather</h2>
<form action="/saveApi" method="POST">
  API Key:<br>
  <input type="text" name="apikey"><br>
  Latitude:<br>
  <input type="text" name="lat"><br>
  Longitude:<br>
  <input type="text" name="lon"><br><br>
  <input type="submit" value="Sauvegarder">
</form>
</body>
</html>
)rawliteral";

// ====== Fonctions Wi-Fi ======
void saveWiFi(String s, String p){
  preferences.begin("wifi", false);
  preferences.putString("ssid", s);
  preferences.putString("password", p);
  preferences.end();
  Serial.println("[Wi-Fi] Paramètres sauvegardés en flash");
}

bool loadWiFi(){
  preferences.begin("wifi", true);
  ssid = preferences.getString("ssid", "");
  password = preferences.getString("password", "");
  preferences.end();
  Serial.print("[Wi-Fi] Chargement depuis flash -> SSID: "); Serial.println(ssid);
  return ssid != "" && password != "";
}

void setupWiFiAP(){
  WiFi.softAP("ESP32_Config");
  Serial.println("[Wi-Fi] Point d’accès activé : ESP32_Config");
  Serial.println("[Wi-Fi] Connectez-vous à 192.168.4.1 pour configurer le Wi-Fi");

  server.on("/", [](){ server.send(200, "text/html", htmlWiFiForm); });

  server.on("/save", HTTP_POST, [](){
    if(server.hasArg("ssid") && server.hasArg("password")){
      ssid = server.arg("ssid");
      password = server.arg("password");
      saveWiFi(ssid, password);
      server.send(200, "text/html", "<h2>Wi-Fi sauvegarde ok, redemarrage...</h2>");
      Serial.println("[Wi-Fi] Formulaire reçu -> redemarrage");
      delay(2000);
      ESP.restart();
    }
  });

  server.begin();
  Serial.println("[Wi-Fi] Serveur Web démarré pour configuration");
}

void connectWiFi(){
  if(loadWiFi()){
    Serial.print("[Wi-Fi] Tentative de connexion à : "); Serial.println(ssid);
    WiFi.begin(ssid.c_str(), password.c_str());
    int tries = 0;
    while(WiFi.status() != WL_CONNECTED && tries < 20){
      delay(500);
      Serial.print(".");
      tries++;
    }
    if(WiFi.status() == WL_CONNECTED){
      Serial.println("\n[Wi-Fi] Connecté avec succès!");
      wifiConfigured = true;
      strcpy(boxes[3].value, "ON");
      return;
    }
    Serial.println("\n[Wi-Fi] Échec de connexion Wi-Fi");
  }
  setupWiFiAP();
}

// ====== Fonctions OpenWeather ======
void saveWeatherConfig(String key, String latitude, String longitude){
  preferences.begin("openweather", false);
  preferences.putString("apikey", key);
  preferences.putString("lat", latitude);
  preferences.putString("lon", longitude);
  preferences.end();
  Serial.println("[OpenWeather] Config sauvegardée en flash");
}

bool loadWeatherConfig(){
  preferences.begin("openweather", true);
  apiKey = preferences.getString("apikey", "");
  lat = preferences.getString("lat", "");
  lon = preferences.getString("lon", "");
  preferences.end();
  Serial.print("[OpenWeather] Chargement depuis flash -> API: "); Serial.println(apiKey);
  return apiKey != "" && lat != "" && lon != "";
}

void configWeather(){
  WiFi.softAP("ESP32_Config_API");
  Serial.println("[OpenWeather] Point d’accès activé : ESP32_Config_API");
  Serial.println("[OpenWeather] Connectez-vous à 192.168.4.1 pour configurer OpenWeather");

  server.on("/", [](){ server.send(200, "text/html", htmlApiForm); });

  server.on("/saveApi", HTTP_POST, [](){
    if(server.hasArg("apikey") && server.hasArg("lat") && server.hasArg("lon")){
      apiKey = server.arg("apikey");
      lat = server.arg("lat");
      lon = server.arg("lon");
      saveWeatherConfig(apiKey, lat, lon);
      server.send(200, "text/html", "<h2>Config meteo sauvegarde ok, redemarrage...</h2>");
      Serial.println("[OpenWeather] Formulaire reçu -> redemarrage");
      delay(2000);
      ESP.restart();
    }
  });

  server.begin();
  Serial.println("[OpenWeather] Serveur Web démarré pour configuration");
}

// ====== OpenWeather ======
void updateWeather() {
  if (WiFi.status() != WL_CONNECTED || apiKey == "") return;

  HTTPClient http;
  String url = "https://api.openweathermap.org/data/2.5/weather?lat=" + lat + "&lon=" + lon +
               "&units=metric&appid=" + apiKey + "&lang=fr";
  Serial.print("[OpenWeather] URL: "); Serial.println(url);

  http.begin(url);
  int httpCode = http.GET();
  Serial.print("[OpenWeather] HTTP code: "); Serial.println(httpCode);

  if (httpCode == 200) {
    String payload = http.getString();
    Serial.println("[OpenWeather] JSON complet:");
    Serial.println(payload);

    StaticJsonDocument<2048> doc;
    DeserializationError error = deserializeJson(doc, payload);
    if (error) {
      Serial.print("[OpenWeather] Erreur JSON: "); Serial.println(error.c_str());
      http.end();
      return;
    }

    // Données météo
    float temp       = doc["main"]["temp"];
    float feels_like = doc["main"]["feels_like"];
    float wind       = doc["wind"]["speed"];
    int windDeg      = doc["wind"]["deg"];
    int humidity     = doc["main"]["humidity"];
    int pressure     = doc["main"]["pressure"];
    long sunrise     = doc["sys"]["sunrise"];
    long sunset      = doc["sys"]["sunset"];
    const char* city = doc["name"];
    const char* weatherDesc = doc["weather"][0]["description"];

// Conversion lever/coucher soleil en heure locale (NTP + fuseau pris en compte)
struct tm ts;
char buf[10];

time_t sunrise_ts = (time_t)sunrise;
time_t sunset_ts  = (time_t)sunset;

// Lever du soleil
localtime_r(&sunrise_ts, &ts);
strftime(buf, sizeof(buf), "%H:%M", &ts);
snprintf(boxes[4].value, sizeof(boxes[4].value), "%s", buf);

// Coucher du soleil
localtime_r(&sunset_ts, &ts);
strftime(buf, sizeof(buf), "%H:%M", &ts);
snprintf(boxes[5].value, sizeof(boxes[5].value), "%s", buf);


    // Mettre à jour le tableau de bord
    snprintf(boxes[0].value, sizeof(boxes[0].value), "%.1f°C", temp);
    snprintf(boxes[1].value, sizeof(boxes[1].value), "%.1f m/s %d°", wind, windDeg);
    snprintf(boxes[6].value, sizeof(boxes[6].value), "%d%%", humidity);
    snprintf(boxes[7].value, sizeof(boxes[7].value), "%dhPa", pressure);
    snprintf(boxes[8].value, sizeof(boxes[8].value), "%s", city);
    snprintf(boxes[9].value, sizeof(boxes[9].value), "%s", weatherDesc);

    // Traces console
    Serial.print("[OpenWeather] Température: "); Serial.println(temp);
    Serial.print("[OpenWeather] Vent: "); Serial.print(wind); Serial.print(" m/s "); Serial.println(windDeg);
    Serial.print("[OpenWeather] Humidité: "); Serial.println(humidity);
    Serial.print("[OpenWeather] Pression: "); Serial.println(pressure);
    Serial.print("[OpenWeather] Lever soleil: "); Serial.println(boxes[4].value);
    Serial.print("[OpenWeather] Coucher soleil: "); Serial.println(boxes[5].value);
    Serial.print("[OpenWeather] Ville: "); Serial.println(city);
    Serial.print("[OpenWeather] Description: "); Serial.println(weatherDesc);

  } else {
    Serial.println("[OpenWeather] Erreur HTTP ou clé API invalide");
  }

  http.end();
}

// ====== TFT ======
void drawBox(int x, int y, int w, int h, Box b){
  tft.drawRect(x, y, w, h, b.clickable ? TFT_GREEN : TFT_WHITE);  

  // ===== Label =====
  tft.setTextFont(2);
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.setCursor(x + 5, y + 5);
  tft.print(b.label);

  // ===== Valeur =====
  int fontSize = 4; // taille initiale
  tft.setTextFont(fontSize);
  int tw = tft.textWidth(b.value);

  // Réduire la taille si le texte dépasse la largeur de la case
  while(tw > w - 10 && fontSize > 1){
    fontSize--;
    tft.setTextFont(fontSize);
    tw = tft.textWidth(b.value);
  }

  int th = tft.fontHeight();
  int cx = x + (w - tw) / 2;
  int cy = y + (h - th) / 2 + th / 4;
  tft.setCursor(cx, cy);

  // === Couleur spéciale pour Wi-Fi ===
  if (strcmp(b.label, "Wi-Fi") == 0) {
    if (strcmp(b.value, "ON") == 0)
      tft.setTextColor(TFT_GREEN, TFT_BLACK);
    else
      tft.setTextColor(TFT_RED, TFT_BLACK);
  } else {
    tft.setTextColor(TFT_CYAN, TFT_BLACK);
  }

  tft.print(b.value);
}

void drawAllBoxes(){
  tft.fillScreen(TFT_BLACK);
  for (int r = 0; r < rows; r++){
    for (int c = 0; c < cols; c++){
      int index = r * cols + c;
      int x = c * (boxW + 10) + 5;
      int y = r * (boxH + 10) + 5;
      drawBox(x, y, boxW, boxH, boxes[index]);
    }
  }
}

void drawPageHeure() {
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(MC_DATUM);
  tft.drawString("Page Heure", screenW/2, 20, 2);

  // Affiche l'heure (case "Heure") au centre de la page
  tft.setTextColor(TFT_CYAN, TFT_BLACK);
  tft.drawString(boxes[2].value, screenW/2, 80, 8);

  // Dessine un bouton "Retour" (rectangle + texte)
  tft.drawRect(10, screenH - 30, 80, 25, TFT_WHITE);
  tft.setTextDatum(TL_DATUM);
  tft.drawString("<- Retour", 15, screenH - 28, 2);
}

// ====== Setup ======
void setup(){
  Serial.begin(115200);
  delay(2000);
  Serial.println("=== ESP32 Tableau de bord météo ===");
  Serial.print("Nom du fichier source : "); Serial.println(__FILE__);

  tft.init();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  
  // Charger les paramètres du réveil
  chargerReveil();

// Use this calibration code in setup():
  uint16_t calData[5] = { 308, 3404, 466, 3148, 3 };
  tft.setTouch(calData);

  connectWiFi();

  if(!loadWeatherConfig()){
    configWeather();
  } else {
    weatherConfigured = true;
    Serial.println("[Setup] OpenWeather déjà configuré");
    // Appel immédiat pour récupérer la météo dès le démarrage
    Serial.println("[Setup] Premier appel OpenWeather immédiat");
    updateWeather();    
  }

// Config NTP et attente de synchronisation
configTime(3600, 3600, "pool.ntp.org", "time.nist.gov");
Serial.println("[Setup] NTP configuré, attente de synchronisation...");

struct tm timeinfo;
int retry = 0;
const int retry_max = 10;
while(!getLocalTime(&timeinfo) && retry < retry_max){
    Serial.println("[Setup] En attente de la synchronisation NTP...");
    delay(1000);
    retry++;
}

if(retry == retry_max){
    Serial.println("[Setup] Attention: NTP non synchronisé !");
} else {
    Serial.println("[Setup] Heure NTP synchronisée !");
    if(weatherConfigured){
        Serial.println("[Setup] Premier appel OpenWeather immédiat");
        updateWeather();
    }
}

  drawAllBoxes();
}

// ====== Fonctions Réveil ======
void sauvegarderReveil() {
  preferences.begin("reveil", false);
  preferences.putInt("heure", heureReveil);
  preferences.putInt("minute", minuteReveil);
  preferences.putBool("actif", reveilActif);
  preferences.end();
}

void chargerReveil() {
  preferences.begin("reveil", true);
  heureReveil = preferences.getInt("heure", 7);
  minuteReveil = preferences.getInt("minute", 0);
  reveilActif = preferences.getBool("actif", false);
  preferences.end();
  
  // Formater l'heure du réveil
  snprintf(timeStrReveil, sizeof(timeStrReveil), "%02d:%02d", heureReveil, minuteReveil);
}

void drawPageReveil() {
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(MC_DATUM);
  tft.drawString("Reglage Reveil", screenW/2, 20, 2);
  
  // Affichage de l'heure de réveil
  char timeStr[6];
  snprintf(timeStr, sizeof(timeStr), "%02d:%02d", heureReveil, minuteReveil);
  tft.setTextColor(TFT_CYAN, TFT_BLACK);
  tft.drawString(timeStr, screenW/2, 140, 8);
  
  // Boutons + et - pour les heures
  tft.drawRect(screenW/2 - 170, 150, 40, 40, TFT_WHITE);
  tft.drawString("-", screenW/2 - 150, 170, 4);
  tft.drawRect(screenW/2 - 170, 100, 40, 40, TFT_WHITE);
  tft.drawString("+", screenW/2 - 150, 120, 4);
  
  // Boutons + et - pour les minutes
  tft.drawRect(screenW/2 + 130, 150, 40, 40, TFT_WHITE);
  tft.drawString("-", screenW/2 + 150, 170, 4);
  tft.drawRect(screenW/2 + 130, 100, 40, 40, TFT_WHITE);
  tft.drawString("+", screenW/2 + 150, 120, 4);
  
  // Bouton ON/OFF
  tft.drawRect(screenW/2 - 50, 240, 100, 40, TFT_WHITE);
  tft.setTextColor(reveilActif ? TFT_GREEN : TFT_RED, TFT_BLACK);
  tft.drawString(reveilActif ? "ON" : "OFF", screenW/2, 260, 4);
  
  // Bouton Retour
  tft.drawRect(10, screenH - 30, 80, 25, TFT_WHITE);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(TL_DATUM);
  tft.drawString("<- Retour", 15, screenH - 28, 2);
}

// ====== Loop ======
unsigned long lastUpdate = 0;
unsigned long lastWeather = 0;
unsigned long lastMessageDisplay = 0;

void loop() {
  server.handleClient();
  static int currentPage = PAGE_MAIN;
  static bool pageChanged = true; // Flag pour éviter les redessins inutiles

  // Vérification du réveil toutes les minutes
  if (reveilActif) {
    struct tm timeinfo;
    if (getLocalTime(&timeinfo)) {
      if (timeinfo.tm_hour == heureReveil && timeinfo.tm_min == minuteReveil) {
        // Ajouter ici votre code pour l'alarme (buzzer, LED, etc.)
      }
    }
  }

  // === Gestion de l'affichage ===
  if (pageChanged) {
    switch (currentPage) {
      case PAGE_MAIN:
        drawAllBoxes();
        break;
      case PAGE_Heure:
        drawPageHeure();
        break;
      case PAGE_Reveil:
        drawPageReveil();
        break;
    }
    pageChanged = false;
  }

  if (wifiConfigured && weatherConfigured) {
    // Heure toutes les 10s
    if (millis() - lastUpdate > 10000) {
      lastUpdate = millis();
      struct tm timeinfo;
      if (getLocalTime(&timeinfo)) {
        snprintf(boxes[2].value, sizeof(boxes[2].value), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
        pageChanged = true; // <--- AJOUT : force le rafraîchissement
      }
    }
    // Météo toutes les 10 min
    if (millis() - lastWeather > 600000) {
      lastWeather = millis();
      Serial.println("[Loop] Mise à jour météo...");
      updateWeather();
      pageChanged = true; // <--- AJOUT : force le rafraîchissement
    }

    // === Détection tactile ===
    uint16_t tx, ty;
    if (tft.getTouch(&tx, &ty)) {
      // Vérifie d'abord le bouton "Retour" sur la page Heure
      if (currentPage == PAGE_Heure && tx >= 10 && tx <= 90 && ty >= (screenH - 30) && ty <= screenH) {
        currentPage = PAGE_MAIN;
        pageChanged = true;
        delay(200);
      }
      // Sinon, vérifie les autres boxes
      else {
        for (int r = 0; r < rows; r++) {
          for (int c = 0; c < cols; c++) {
            int index = r * cols + c;
            int boxX = c * (boxW + 10) + 5;
            int boxY = r * (boxH + 10) + 5;
            if (tx >= boxX && tx <= boxX + boxW &&
                ty >= boxY && ty <= boxY + boxH &&
                boxes[index].clickable) {
              // Action pour Heure
              if (strcmp(boxes[index].label, "Heure") == 0) {
                currentPage = PAGE_Heure;
                pageChanged = true;
                delay(200);
              }
              // Action pour Reveil
              else if (strcmp(boxes[index].label, "Reveil") == 0) {
                currentPage = PAGE_Reveil;
                pageChanged = true;
                delay(200);
              }
              // Action pour "Reset Meteo" -> efface la config meteo puis redémarre l'ESP
              else if (strcmp(boxes[index].label, "Reset Meteo") == 0) {
                Serial.println("[Touch] Réinitialisation OpenWeather ! -- redemarrage");
                preferences.begin("openweather", false);
                preferences.clear();
                preferences.end();
                weatherConfigured = false;
                tft.fillScreen(TFT_BLACK);
                tft.setTextColor(TFT_RED, TFT_BLACK);
                tft.setTextDatum(MC_DATUM);
                tft.drawString("Redémmarrage de l'ESP pour reconfigurer la météo",
                               screenW/2, screenH/2, 2);
                delay(2000);
                ESP.restart();
              }
              // Action pour "Reset Wifi" -> efface la config wifi puis redémarre l'ESP
              else if (strcmp(boxes[index].label, "Reset Wifi") == 0) {
                Serial.println("[Touch] Réinitialisation Wifi ! -- redemarrage");
                preferences.begin("wifi", false);
                preferences.clear();
                preferences.end();
                weatherConfigured = false;
                tft.fillScreen(TFT_BLACK);
                tft.setTextColor(TFT_RED, TFT_BLACK);
                tft.setTextDatum(MC_DATUM);
                tft.drawString("Redémmarrage de l'ESP pour reconfigurer le wifi",
                               screenW/2, screenH/2, 2);
                delay(2000);
                ESP.restart();
            }
          }
        }
      }
      if (currentPage == PAGE_Reveil) {
        if (tx >= 10 && tx <= 90 && ty >= (screenH - 30) && ty <= screenH) {
          // Bouton Retour
          currentPage = PAGE_MAIN;
          pageChanged = true;
        sauvegarderReveil();
        } else if (tx >= screenW/2 - 170 && tx <= screenW/2 - 130) {
          if (ty >= 100 && ty <= 140) {
            // Augmenter les heures
            heureReveil = (heureReveil + 1) % 24;
            snprintf(timeStrReveil, sizeof(timeStrReveil), "%02d:%02d", heureReveil, minuteReveil);
            strcpy(boxes[12].value, timeStrReveil);  // Mise à jour de la case réveil
            pageChanged = true;
          } else if (ty >= 150 && ty <= 190) {
            // Diminuer les heures
            heureReveil = (heureReveil - 1 + 24) % 24;
            snprintf(timeStrReveil, sizeof(timeStrReveil), "%02d:%02d", heureReveil, minuteReveil);
            strcpy(boxes[12].value, timeStrReveil);  // Mise à jour de la case réveil
            pageChanged = true;
          }
        } else if (tx >= screenW/2 + 130 && tx <= screenW/2 + 170) {
          if (ty >= 100 && ty <= 140) {
            // Augmenter les minutes
            minuteReveil = (minuteReveil + 1) % 60;
            snprintf(timeStrReveil, sizeof(timeStrReveil), "%02d:%02d", heureReveil, minuteReveil);
            strcpy(boxes[12].value, timeStrReveil);  // Mise à jour de la case réveil
            pageChanged = true;
          } else if (ty >= 150 && ty <= 190) {
            // Diminuer les minutes
            minuteReveil = (minuteReveil - 1 + 60) % 60;
            snprintf(timeStrReveil, sizeof(timeStrReveil), "%02d:%02d", heureReveil, minuteReveil);
            strcpy(boxes[12].value, timeStrReveil);  // Mise à jour de la case réveil
            pageChanged = true;
          }
        } else if (tx >= screenW/2 - 50 && tx <= screenW/2 + 50 && 
                   ty >= 240 && ty <= 280) {
          // Toggle ON/OFF
          reveilActif = !reveilActif;
          pageChanged = true;
        }
      }
      delay(200); // anti-rebond
    }
  }
  else if (!weatherConfigured) {
    // Affiche le message de réinitialisation seulement toutes les 5 secondes
    if (millis() - lastMessageDisplay > 5000) {
      lastMessageDisplay = millis();
      tft.fillScreen(TFT_BLACK);
      tft.setTextColor(TFT_RED, TFT_BLACK);
      tft.setTextDatum(MC_DATUM);
      tft.drawString("Reinit Config Meteo\nConnect Wi-Fi ESP32_Config_API\nPuis 192.168.4.1",
                     screenW/2, screenH/2, 2);
      }
    }
  }
}