#include <WiFi.h>
#include <NetworkClientSecure.h> // to work with WiFi or Ethernet
#include <HTTPClient.h> // gere échange HTTP GET pour sortir les éléments utiles
#include <ArduinoJson.h> // traitement du JSON fournit par RTE
#define MAX_SIZE_T 80
#define HFROMRTE 7
byte Horloge = 7;
String STGEt = "0";
String RTE_Jour;
String RTE_Demain;
String LTARF = "?";
int HeureCouranteDeci = 700;
int LastHeureRTE = 600;
byte TempoRTEon = 1;
byte ModeReseau = 0;
bool HeureValide = false;
String Source = "UxI";
// *******************************************************
// * Recherche Info Tempo RTE pour les sources non Linky *
// *******************************************************
// attend une chaine texte Date: conforme RFC 7231 (§7.1.1.2, §7.1.1.1 Prefererd format) obtenu de l'entete d'un serveur
// si la chaine est correct, avec une date postérieur a 2025, et qu'il y a une écart de plus de 2mn avec la l'heure en RTC, on ajuste RTC
// retourne :
// 1 si mise a jour de la RTC
// 0 si tout va bien sans modif RTC
// un négatif si problème pour obtenir/exploiter la chaine (-1 : On a Date mais pas les :, -2 : pas de virgule , -3 : la chaine ne termine pas par GMT, -4 : n'obtient pas les 6 parametres date/heure / -5 : année sur serveur incohérente / -6 : 3 lettres pour le mois incohérente)
int checkAjustRTCDeLineDate(String lineDate, bool verbose = false) {
if (verbose) Serial.println(lineDate);
// le parametre String ressemble à "Date: Thu, 29 Jan 2026 16:40:27 GMT" ou "Thu, 29 Jan 2026 16:40:27 GMT"
if (!lineDate.endsWith(" GMT")) return -3; // La chaine reçu n'est pas conforme a l'attendu (doit finir par GMT)
if (lineDate.startsWith("Date")) { // la chaine commence par Date ?
int colonIdx = lineDate.indexOf(':');
if (colonIdx != -1) lineDate = lineDate.substring(colonIdx + 1); else return -1; // supprime Date : ou retourne -2
}
int commaIdx = lineDate.indexOf(',');
if (commaIdx != -1) { // cherche la virgule
lineDate = lineDate.substring(commaIdx + 1); // supprime le jour de la semaine 20 Dec 2025 14:39:38 GMT
} else return -2; // manque le séparateur comma
lineDate.trim(); // Supprime les espaces inutiles
// après nettoyage if reste : "29 Jan 2026 16:40:27 GMT"
time_t tSecCurrentTime; // nombre de seconde écoulé depuis le 1er janvier 1970, d'apres le serveur
char monthStr[4] = {0,0,0,0}; // 3 lettres pour le mois + le 0 de fin
struct tm tmServ = {0}; // charge une structure tm avec la date et l'heure du fuseau GMT avec les infos du serveur
if (sscanf(lineDate.c_str(), "%d %3s %d %d:%d:%d", &tmServ.tm_mday, monthStr, &tmServ.tm_year, &tmServ.tm_hour , &tmServ.tm_min, &tmServ.tm_sec) != 6) return -4; // chaine mal formatée
if (tmServ.tm_year < 2026) return -5; //date avant 2026 depuis le serveur ?
tmServ.tm_year -= 1900;
const char* months = "JanFebMarAprMayJunJulAugSepOctNovDec";
char* m = strstr(months, monthStr);
if (!m) return -6; // pas de correspondance mois dans la date obtenue de l'entete
tmServ.tm_mon = (m - months) / 3;
//Serial.println(&tmServ, "%d/%m/%Y %H:%M:%S Greenwich Mean Time\n"); // date/heure GMT tel que recupéré (heure hiver en angleterre)
// a quelques secondes près c'est une datage temporelle de l'instant
char* oldTZ = getenv("TZ"); // on a un timestamp GMT → utiliser mktime en mode Universal Time Coordinated
char backupTZ[64] = {0};
if (oldTZ) strncpy(backupTZ, oldTZ, sizeof(backupTZ)-1);
setenv("TZ", "UTC", 1);// On force temporairement TZ=UTC (codage GMT au format UTC sur 24h sans décalage heure d'été)
tzset();
tSecCurrentTime = mktime(&tmServ);
if (oldTZ) setenv("TZ", backupTZ, 1); else unsetenv("TZ");
tzset();
struct tm timeRTC;
getLocalTime(&timeRTC);
if (Horloge == HFROMRTE && abs((long)(tSecCurrentTime - mktime(&timeRTC))) > 120) { // si besoin ajuste l'horloge RTC de l'ESP avec ce qu'on a obtenu du serveur
struct timeval tv = { .tv_sec = tSecCurrentTime };
settimeofday(&tv, NULL);
if (verbose) {
Serial.print("Date/Heure en RTC avant actualisation : "); Serial.println(&timeRTC, "%d/%m/%Y %H:%M:%S");
Serial.print("Date/Heure extrait de "); Serial.println(lineDate);
}
getLocalTime(&timeRTC); // Rafraîchissement de la structure avec la nouvelle heure système
if (verbose) {
Serial.print("actualisation Date/Heure RTC de l'ESP : "); Serial.println(&timeRTC, "%d/%m/%Y %H:%M:%S\n");
}
return 1; // tout c'est bien passé, RTC a était mise a jour
}
return 0; // tout c'est bien passé, RTC n'a pas était mise a jour
}
void Call_RTE_data() {
int Hcour = HeureCouranteDeci / 2; //Par pas de 72secondes pour faire 2 appels si un bug
int LastH = LastHeureRTE / 2;
if (Hcour == 529) {STGEt = "0";} //Pour forcer une relecture après 10h30, heure à laquelle RTE doit geler la couleur
// il faut éviter de questionner l'URL tempo light si on a déjà les infos qui nous intéressent...
bool couleur_lendemain = (STGEt == "4" || STGEt == "8" || STGEt == "C"); // true si couleur du lendemain connue
bool couleur_jour = (LTARF == "TEMPO_BLEU" || LTARF == "TEMPO_BLANC" || LTARF == "TEMPO_ROUGE"); // true si couleur du jour connu
// traitement du changement de couleur tempo à 6h00, avec ré-initialisation de la couleur du lendemain
if ((Hcour == 300) && (LTARF != "") && (STGEt != "")) // à 6h00 précises, la couleur tempo du lendemain devient la couleur du jour. Celle du lendemain n'est à priori pas encore connue
// Cela permet de continuer à fonctionner de manière nominale même si le site RTE n'est pas encore renseigné
// && (LTARF!="") pour ne pas passer dans cette boucle au reset si fait à 6h00 du matin...
// && (STGEt<>"") pour éviter de passer plusieurs fois( 3 à 4 ) dans cette boucle à cause du Hcour/2
{
if (STGEt == "4") LTARF = "TEMPO_BLEU";
else if (STGEt == "8") LTARF = "TEMPO_BLANC";
else if (STGEt == "C") LTARF = "TEMPO_ROUGE";
STGEt = "";
couleur_lendemain = false; // on ne connait plus la couleur du lendemain. Cela forcera la lecture sur le site RTE
if (LTARF != "" && TempoRTEon == 1 && ModeReseau == 0) {
//StockMessage("Tempo depuis 6h00: " + LTARF + ",demain ? ");
RTE_Jour = "NON_DEFINI";
RTE_Demain = "NON_DEFINI";
}
}
bool estHeureDeConsulter = false;
if (couleur_lendemain && couleur_jour) return; // on connait déja les couleurs du jour et celle de demain, on interroge pas RTE
if (LastHeureRTE >= 0 && LastH == Hcour) return; // On a déja obtenu des info depuis RTE et l'heure n'a pas changé, on ne traite pas la suite
if (HeureValide && Horloge != HFROMRTE) { // l'horloge n'est pas bonne, on ne peu pas comparer le jour courant
int heuresCheck[] = {306, 310, 530, 560, 600, 900, 1150};
for (int h : heuresCheck) {
if (Hcour == h) {
estHeureDeConsulter = true;
break;
}
}
} else {
estHeureDeConsulter = true; // au premier passage, on force la mise a l'heure depuis le serveur RTE
}
if (LastHeureRTE == -1 || estHeureDeConsulter) { // si l'heure actuelle fait partie des horaires à consulter
if (TempoRTEon == 1 && ModeReseau == 0) {
HTTPClient http;
NetworkClientSecure clientSecuRTE; // ressource prise sur la pile lors de l'appel de la fonction (ne pas oublier le .stop() avant toute sortie pour liberer les buffers réservés qui pourrait être en cours de traitement au moment de sortir)
clientSecuRTE.setInsecure(); // skip https verification
http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
http.setRedirectLimit(5);
const char* headerKeys[] = {"Date"};
const char* url_API_RTE = "https://www.services-rte.com/cms/open_data/v1/tempoLight";
http.collectHeaders(headerKeys, 1);
http.begin(clientSecuRTE, url_API_RTE);
http.addHeader("Accept", "application/json");
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
Serial.printf("Code HTTP != 200 [%d]\n", httpCode);
if (httpCode < 0) Serial.printf("Erreur réseau (Client) : %s\n", http.errorToString(httpCode).c_str()); // erreur bas niveau
http.end();
clientSecuRTE.stop(); // force la déconnexion sur erreur
return; // on sort sans avoir obtenu les infos (pas de changement des données connues)
}
String dateHeader = http.header("Date"); // recupere la ligne de date contenu dans l'entete de réponse du serveur RTE
if (dateHeader.length() > 0 && Horloge == HFROMRTE) { // on a obtenu la ligne Date et on souhaite utiliser l'horloge du serveur RTE
int res = checkAjustRTCDeLineDate(dateHeader, false); // met a l'heure ou ajuste l'horloge de l'ESP (si il y a plus de 2 minutes d'écart avec l'horloge RTC)
if (res < 0) Serial.printf("Problème (%d) pour obtenir date/heure depuis serveur RTE, pas d'ajustement Horloge RTC\n", res);
} // else la date et l'heure doivent être reglé autrement
// prépare les critéres pour l'extraction du JSON
// la date du calendrier "cejour" associé au champ du json est valide de 6h00 a 23h59:59 : a mémoriser pour le J+1 entre 0h00 et 5h59:59
// la couleur "tarif tempo" du moment n'est pas disponible entre 0h00 et 5h59:59, il faut se rappeler de l'information obtenue préalablement
// la date du calendrier "demain" associé au champ du json est valide, si le champ existe, a partir du lendemain 6h00 jusque 23h59:59 (et jusqu'a 5h59:59 le surlendemain mais on sera passé par cejour avant le surlendemain)
char cejour[12];
time_t timestampCJ = time(NULL);
// le 02/01 a 3h00 : cejour = le 02/01 !!! la donnée json associée ne sera valide qu'a partir de 6h00
// le 02/01 a 7h00 : cejour = le 02/01 la donnée json associée est valide pour le jour donné
// le 02/01 a 23h00 : cejour = le 02/01 la donnée json associée est valide pour le jour donné
struct tm* pTimeCJ = localtime(×tampCJ);
strftime(cejour, sizeof(cejour), "%Y-%m-%d", pTimeCJ);
char demain[12];
time_t timestampDE = time(NULL) + 86400; // Decalage début période couleur RTE de 24h pour le lendemain.
// le 02/01 a 3h00 : demain = le 03/01 A cette horaire, la donnée pour la couleur tarifaire du lendemain n'est pas disponible
// le 02/01 a 7h00 : demain = le 03/01 A cette horaire, la donnée pour la couleur tarifaire du lendemain est disponible, valide provisoirement
// le 02/01 a 11h00 : demain = le 03/01 A cette horaire, la donnée pour la couleur tarifaire du lendemain est disponible, valide quasi définitivement
// le 02/01 a 16h00 : demain = le 03/01 A cette horaire, la donnée pour la couleur tarifaire du lendemain est disponible, valide définitivement
struct tm* pTimeDE = localtime(×tampDE);
strftime(demain, sizeof(demain), "%Y-%m-%d", pTimeDE);
String payloadRTE;
payloadRTE.reserve(200); // Vu 112 avec 4 parametres dans le JSON
// la réponse est 'chunked' par le serveur RTE, httpClient remet les choses correctement
payloadRTE = http.getString(); // on se permet de recuperer l'ensemble du body de RTE (petit)
JsonDocument doc;
DeserializationError err = deserializeJson(doc, payloadRTE);
http.end(); // libere les ressources http
clientSecuRTE.stop(); // libere le socket réseau
// TelnetPrint("RTEdata:"); TelnetPrintln(payloadRTE); // pour debug
if (err) {
Serial.print("Erreur deserialization: "); Serial.println(err.c_str());
// clientSecuRTE.stop(); // a déja été appelé, on quitte proprement la fonction
return; // sort sur erreur de decodage du contenu du JSON
}
JsonObject values = doc["values"];
const char* Cdujour = values[cejour] | "UNKNOWN";
const char* Cdemain = values[demain] | "UNKNOWN";
String colDuMoment;
String colAVenir;
String lendemain;
// Cdujour entre 6h00 et 23h59:59 est la couleur de la période en cours
// Cdujour entre 0h00 et 5h59:59 est la couleur du tarif qui sera appliqué a partir de 6h00
// Cdemain n'est plus disponible a partir de 0h00 jusqu'a nouvelle disponibilité provisoire vers 6h12 et confirmation vers 10h30
// Cdemain aprés l'obtention vers 6h12 est la couleur pour le lendemain de 6h à 6h le surlendemain
static String memoColorFor0h_to_6h = "UNKNOWN";
if (pTimeCJ->tm_hour >= 6) { // obligatoirement entre 6h00 et 23h59:59 soit hors période 0h00 5h59:59
colDuMoment = Cdujour;
colAVenir = Cdemain;
memoColorFor0h_to_6h = Cdujour; // sauve la couleur du jour pour l'utiliser le jour suivant entre minuit et 6h
} else {
colDuMoment = memoColorFor0h_to_6h;
colAVenir = Cdujour;
}
//Serial.printf("Couleur ce jour (du %s 6h00 au %s 6h00): %s\n", cejour, demain, Cdujour);
//Serial.printf("Couleur demain (le %s a partir de 6h00): %s\n", demain, Cdemain);
//Serial.printf("Couleur tarifaire active en ce moment : %s\n", colDuMoment.c_str());
//Serial.printf("Couleur tarifaire au prochain changement(à 6h00) : %s\n", colAVenir.c_str());
if (colDuMoment == "BLUE") LTARF = "TEMPO_BLEU";
else if (colDuMoment == "WHITE") LTARF = "TEMPO_BLANC";
else if (colDuMoment == "RED") LTARF = "TEMPO_ROUGE";
else if (colDuMoment == "UNKNOWN") LTARF = "NON_DEFINI";
else LTARF = "?";
// Pour la couleur tarifaire du lendemain, on utilise le codage Hexa du Linky comme STGE 2ieme lettre, uniquement les 2 bits haut, les 2 bit bas pour la couleur du jour reste a 00 (bit 26 et 27 du codage STGE)
if (colAVenir == "UNKNOWN") {STGEt = "0"; lendemain = "NON_DEFINI";} else
if (colAVenir == "BLUE") {STGEt = "4"; lendemain = "TEMPO_BLEU";} else
if (colAVenir == "WHITE") {STGEt = "8"; lendemain = "TEMPO_BLANC";} else
if (colAVenir == "RED") {STGEt = "C"; lendemain = "TEMPO_ROUGE";} else {STGEt = "?"; lendemain = "NON_DEFINI";}
//StockMessage(String(cejour) + " : " + LTARF + " | " + String(demain) + " : " + lendemain);
LastHeureRTE = HeureCouranteDeci; //Heure lecture Tempo RTE
} else {
if (Source != "Linky" && Source != "Ext") {
LTARF = "";
STGEt = "0";
}
}
}
}
void setup() {
Serial.begin(115200);
Serial.println("Hello, ESP32!");
setenv("TZ", "CET-1CEST,M3.5.0,M10.5.0/3", 1); // heure legale Paris
WiFi.begin("Wokwi-GUEST", "", 6); // établi une connexion Wifi
while (WiFi.status() != WL_CONNECTED) {
delay(250);
Serial.print(".");
}
Serial.println(" Connected!");
Serial.println("******************");
Call_RTE_data();
Serial.println("well done !");
}
void loop() {
delay(10); // this speeds up the simulation
}