Programmateur horaire avec carte de développement ESP32 à 4 relais et interface Web
Description :
Ce projet consiste en la conception et la réalisation d’un système de gestion domotique intelligent basé sur le microcontrôleur ESP32. Il permet de piloter quatre appareils électriques de manière indépendante via des relais, en offrant une flexibilité totale entre un contrôle manuel et une programmation automatisée basée sur le temps réel.
L’atout majeur de ce système est son interface Web embarquée, accessible depuis n’importe quel navigateur (smartphone, tablette ou ordinateur), permettant de configurer les plages horaires et de forcer l’état des relais sans avoir à reprogrammer la carte.
Prérequis :
- 1 x Carte de développement à 4 relais ESP32
- Fils de connexion
Autre :
- Tutoriel disponible pour téléverser sur la carte de développement à double relais ESP32 ICI
Version IDE :
- Arduino IDE 2.3.5
Bibliothèque :
- LittleFS.h (Tuto dispo)
- ESPAsyncWebServer.h (version: 3.7.10 par ESP32Async)
- AsyncTCP.h (version 3.4.7 par ESP32Async)
- ArduinoJson.h (version 7.4.2 par Benoit Blanchon)
Vidéo de démonstration :
Schéma de câblage :



Code :
Code Arduino :
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include <time.h>
// --- CONFIGURATION ---
const char *ssid = "......";
const char *password = "......";
const int PR1RELAY_PIN = 26;
const int PR2RELAY_PIN = 25;
const int PR3RELAY_PIN = 33;
const int PR4RELAY_PIN = 32;
const char *TZ_INFO = "CET-1CEST,M3.5.0,M10.5.0/3";
AsyncWebServer server(80);
// Variables de contrôle
String PR1heureDebut = "08:00";
String PR1heureFin = "18:00";
bool PR1modeAuto = true;
bool PR1relayState = false;
String PR2heureDebut = "09:00";
String PR2heureFin = "19:00";
bool PR2modeAuto = true;
bool PR2relayState = false;
String PR3heureDebut = "10:00";
String PR3heureFin = "20:00";
bool PR3modeAuto = true;
bool PR3relayState = false;
String PR4heureDebut = "11:00";
String PR4heureFin = "21:00";
bool PR4modeAuto = true;
bool PR4relayState = false;
String now;
// --- SYSTÈME DE SAUVEGARDE LITTLEFS ---
void saveSettings() {
File file = LittleFS.open("/config.json", "w");
if (!file) {
Serial.println("Erreur lors de l'ouverture du fichier pour écriture");
return;
}
StaticJsonDocument<256> doc;
doc["PR1debut"] = PR1heureDebut;
doc["PR1fin"] = PR1heureFin;
doc["PR1auto"] = PR1modeAuto;
doc["PR1etatMan"] = PR1relayState;
doc["PR2debut"] = PR2heureDebut;
doc["PR2fin"] = PR2heureFin;
doc["PR2auto"] = PR2modeAuto;
doc["PR2etatMan"] = PR2relayState;
doc["PR3debut"] = PR3heureDebut;
doc["PR3fin"] = PR3heureFin;
doc["PR3auto"] = PR3modeAuto;
doc["PR3etatMan"] = PR3relayState;
doc["PR4debut"] = PR4heureDebut;
doc["PR4fin"] = PR4heureFin;
doc["PR4auto"] = PR4modeAuto;
doc["PR4etatMan"] = PR4relayState;
if (serializeJson(doc, file) == 0) {
Serial.println("Erreur d'écriture JSON");
}
file.close();
Serial.println("Paramètres sauvegardés dans LittleFS");
}
void loadSettings() {
if (LittleFS.exists("/config.json")) {
File file = LittleFS.open("/config.json", "r");
StaticJsonDocument<256> doc;
DeserializationError error = deserializeJson(doc, file);
if (!error) {
PR1heureDebut = doc["PR1debut"] | "08:00";
PR1heureFin = doc["PR1fin"] | "18:00";
PR1modeAuto = doc["PR1auto"] | true;
PR1relayState = doc["PR1etatMan"] | false;
PR2heureDebut = doc["PR2debut"] | "09:00";
PR2heureFin = doc["PR2fin"] | "19:00";
PR2modeAuto = doc["PR2auto"] | true;
PR2relayState = doc["PR2etatMan"] | false;
PR3heureDebut = doc["PR3debut"] | "10:00";
PR3heureFin = doc["PR3fin"] | "20:00";
PR3modeAuto = doc["PR3auto"] | true;
PR3relayState = doc["PR3etatMan"] | false;
PR4heureDebut = doc["PR4debut"] | "11:00";
PR4heureFin = doc["PR4fin"] | "21:00";
PR4modeAuto = doc["PR4auto"] | true;
PR4relayState = doc["PR4etatMan"] | false;
Serial.println("Paramètres chargés depuis LittleFS");
}
file.close();
}
}
void setup() {
Serial.begin(115200);
pinMode(PR1RELAY_PIN, OUTPUT);
pinMode(PR2RELAY_PIN, OUTPUT);
pinMode(PR3RELAY_PIN, OUTPUT);
pinMode(PR4RELAY_PIN, OUTPUT);
// Initialisation LittleFS
if (!LittleFS.begin(true)) {
Serial.println("Erreur critique LittleFS");
return;
}
loadSettings(); // Charger les valeurs au démarrage
digitalWrite(PR1RELAY_PIN, PR1relayState ? HIGH : LOW);
digitalWrite(PR2RELAY_PIN, PR2relayState ? HIGH : LOW);
digitalWrite(PR3RELAY_PIN, PR3relayState ? HIGH : LOW);
digitalWrite(PR4RELAY_PIN, PR4relayState ? HIGH : LOW);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected.");
Serial.println("IP address: ");
// Imprimer l'adresse IP locale ESP
Serial.println(WiFi.localIP());
configTzTime(TZ_INFO, "pool.ntp.org", "time.google.com");
// --- ROUTES WEB ---
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(LittleFS, "/index.html", "text/html");
});
server.on("/get-data", HTTP_GET, [](AsyncWebServerRequest *request) {
StaticJsonDocument<400> doc;
doc["PR1debut"] = PR1heureDebut;
doc["PR1fin"] = PR1heureFin;
doc["PR1auto"] = PR1modeAuto;
doc["PR1boolEtat"] = PR1relayState;
doc["PR2debut"] = PR2heureDebut;
doc["PR2fin"] = PR2heureFin;
doc["PR2auto"] = PR2modeAuto;
doc["PR2boolEtat"] = PR2relayState;
doc["PR3debut"] = PR3heureDebut;
doc["PR3fin"] = PR3heureFin;
doc["PR3auto"] = PR3modeAuto;
doc["PR3boolEtat"] = PR3relayState;
doc["PR4debut"] = PR4heureDebut;
doc["PR4fin"] = PR4heureFin;
doc["PR4auto"] = PR4modeAuto;
doc["PR4boolEtat"] = PR4relayState;
struct tm timeinfo;
char buff[10];
if (getLocalTime(&timeinfo)) strftime(buff, sizeof(buff), "%H:%M:%S", &timeinfo);
else sprintf(buff, "--:--:--");
doc["actuelle"] = String(buff);
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
});
server.on("/PR1toggle-mode", HTTP_GET, [](AsyncWebServerRequest *request) {
PR1modeAuto = !PR1modeAuto;
saveSettings(); // Sauvegarde le changement de mode
request->send(200, "text/plain", "OK");
});
server.on("/PR2toggle-mode", HTTP_GET, [](AsyncWebServerRequest *request) {
PR2modeAuto = !PR2modeAuto;
saveSettings(); // Sauvegarde le changement de mode
request->send(200, "text/plain", "OK");
});
server.on("/PR3toggle-mode", HTTP_GET, [](AsyncWebServerRequest *request) {
PR3modeAuto = !PR3modeAuto;
saveSettings(); // Sauvegarde le changement de mode
request->send(200, "text/plain", "OK");
});
server.on("/PR4toggle-mode", HTTP_GET, [](AsyncWebServerRequest *request) {
PR4modeAuto = !PR4modeAuto;
saveSettings(); // Sauvegarde le changement de mode
request->send(200, "text/plain", "OK");
});
server.on("/PR1force-state", HTTP_GET, [](AsyncWebServerRequest *request) {
PR1modeAuto = false;
PR1relayState = !PR1relayState;
saveSettings(); // Sauvegarde l'état forcé
request->send(200, "text/plain", "PR1OK");
});
server.on("/PR2force-state", HTTP_GET, [](AsyncWebServerRequest *request) {
PR2modeAuto = false;
PR2relayState = !PR2relayState;
Serial.println("test");
saveSettings(); // Sauvegarde l'état forcé
request->send(200, "text/plain", "PR2OK");
});
server.on("/PR3force-state", HTTP_GET, [](AsyncWebServerRequest *request) {
PR3modeAuto = false;
PR3relayState = !PR3relayState;
saveSettings(); // Sauvegarde l'état forcé
request->send(200, "text/plain", "PR3OK");
});
server.on("/PR4force-state", HTTP_GET, [](AsyncWebServerRequest *request) {
PR4modeAuto = false;
PR4relayState = !PR4relayState;
saveSettings(); // Sauvegarde l'état forcé
request->send(200, "text/plain", "PR4OK");
});
server.on("/PR1save", HTTP_POST, [](AsyncWebServerRequest *request) {
if (request->hasParam("PR1debut", true) && request->hasParam("PR1fin", true)) {
PR1heureDebut = request->getParam("PR1debut", true)->value();
PR1heureFin = request->getParam("PR1fin", true)->value();
saveSettings(); // Sauvegarde les nouveaux horaires
request->send(200, "text/plain", "OK");
}
});
server.on("/PR2save", HTTP_POST, [](AsyncWebServerRequest *request) {
if (request->hasParam("PR2debut", true) && request->hasParam("PR2fin", true)) {
PR2heureDebut = request->getParam("PR2debut", true)->value();
PR2heureFin = request->getParam("PR2fin", true)->value();
saveSettings(); // Sauvegarde les nouveaux horaires
request->send(200, "text/plain", "OK");
}
});
server.on("/PR3save", HTTP_POST, [](AsyncWebServerRequest *request) {
if (request->hasParam("PR3debut", true) && request->hasParam("PR3fin", true)) {
PR3heureDebut = request->getParam("PR3debut", true)->value();
PR3heureFin = request->getParam("PR3fin", true)->value();
saveSettings(); // Sauvegarde les nouveaux horaires
request->send(200, "text/plain", "OK");
}
});
server.on("/PR4save", HTTP_POST, [](AsyncWebServerRequest *request) {
if (request->hasParam("PR4debut", true) && request->hasParam("PR4fin", true)) {
PR4heureDebut = request->getParam("PR4debut", true)->value();
PR4heureFin = request->getParam("PR4fin", true)->value();
saveSettings(); // Sauvegarde les nouveaux horaires
request->send(200, "text/plain", "OK");
}
});
server.begin();
}
void loop() {
static unsigned long lastCheck = 0;
if (millis() - lastCheck >= 1000) {
lastCheck = millis();
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
char nowStr[6];
strftime(nowStr, sizeof(nowStr), "%H:%M", &timeinfo);
now = String(nowStr);
}
if (PR1modeAuto) {
bool PR1newState;
if (PR1heureDebut < PR1heureFin) PR1newState = (now >= PR1heureDebut && now < PR1heureFin);
else PR1newState = (now >= PR1heureDebut || now < PR1heureFin);
if (PR1newState != PR1relayState) {
PR1relayState = PR1newState;
digitalWrite(PR1RELAY_PIN, PR1relayState ? HIGH : LOW);
}
} else {
digitalWrite(PR1RELAY_PIN, PR1relayState ? HIGH : LOW);
}
if (PR2modeAuto) {
bool PR2newState;
if (PR2heureDebut < PR2heureFin) PR2newState = (now >= PR2heureDebut && now < PR2heureFin);
else PR2newState = (now >= PR2heureDebut || now < PR2heureFin);
if (PR2newState != PR2relayState) {
PR2relayState = PR2newState;
digitalWrite(PR2RELAY_PIN, PR2relayState ? HIGH : LOW);
}
} else {
digitalWrite(PR2RELAY_PIN, PR2relayState ? HIGH : LOW);
}
if (PR3modeAuto) {
bool PR3newState;
if (PR3heureDebut < PR3heureFin) PR3newState = (now >= PR3heureDebut && now < PR3heureFin);
else PR3newState = (now >= PR3heureDebut || now < PR3heureFin);
if (PR3newState != PR3relayState) {
PR3relayState = PR3newState;
digitalWrite(PR3RELAY_PIN, PR3relayState ? HIGH : LOW);
}
} else {
digitalWrite(PR3RELAY_PIN, PR3relayState ? HIGH : LOW);
}
if (PR4modeAuto) {
bool PR4newState;
if (PR4heureDebut < PR4heureFin) PR4newState = (now >= PR4heureDebut && now < PR4heureFin);
else PR4newState = (now >= PR4heureDebut || now < PR4heureFin);
if (PR4newState != PR4relayState) {
PR4relayState = PR4newState;
digitalWrite(PR4RELAY_PIN, PR4relayState ? HIGH : LOW);
}
} else {
digitalWrite(PR4RELAY_PIN, PR4relayState ? HIGH : LOW);
}
}
}
Code HTML :
( index.html )
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ESP32 Control</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #ffffff;
padding-top: 20px;
}
.card {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.status-on {
color: #28a745;
font-weight: bold;
}
.status-off {
color: #dc3545;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<div class="row mb-4">
<div class="col-12 text-center">
<div class="card bg-primary text-white p-3">
<h4 class="mb-0">Heure ESP32 : <span id="time">--:--:--</span></h4>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-md-6 col-lg-3">
<div class="card h-100">
<div class="card-header bg-dark text-white">
<h5 class="card-title mb-0">Programmateur 1</h5>
</div>
<div class="card-body">
<p class="mb-1">Statut : <span id="PR1relay-status" class="badge bg-secondary">---</span></p>
<p class="mb-3">Mode : <strong id="PR1mode-text">---</strong></p>
<button class="btn btn-outline-primary btn-sm w-100 mb-3" onclick="fetch('/PR1toggle-mode')">
Changer Mode (Auto/Man)
</button>
<div id="PR1timer-ui" class="border-top pt-2">
<label class="form-label small">Début :</label>
<input type="time" id="PR1debut" class="form-control form-control-sm mb-2">
<label class="form-label small">Fin :</label>
<input type="time" id="PR1fin" class="form-control form-control-sm mb-2">
<button class="btn btn-success btn-sm w-100 mt-2" onclick="PR1save()">Sauvegarder</button>
<p id="PR1msg" class="small text-muted mt-1"></p>
</div>
<div id="PR1btn-ui" class="border-top pt-2">
<button id="PR1btn-force" class="btn btn-danger w-100" onclick="fetch('/PR1force-state')">
FORCER ON/OFF
</button>
</div>
</div>
<div class="card-footer">
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card h-100">
<div class="card-header bg-dark text-white">
<h5 class="card-title mb-0">Programmateur 2</h5>
</div>
<div class="card-body">
<p class="mb-1">Statut : <span id="PR2relay-status" class="badge bg-secondary">---</span>
</p>
<p class="mb-3">Mode : <strong id="PR2mode-text">---</strong></p>
<button class="btn btn-outline-primary btn-sm w-100 mb-3" onclick="fetch('/PR2toggle-mode')">
Changer Mode (Auto/Man)
</button>
<div id="PR2timer-ui" class="border-top pt-2">
<label class="form-label small">Début :</label>
<input type="time" id="PR2debut" class="form-control form-control-sm mb-2">
<label class="form-label small">Fin :</label>
<input type="time" id="PR2fin" class="form-control form-control-sm mb-2">
<button class="btn btn-success btn-sm w-100 mt-2" onclick="PR2save()">Sauvegarder</button>
<p id="PR2msg" class="small text-muted mt-1"></p>
</div>
<div id="PR2btn-ui" class="border-top pt-2">
<button id="PR2btn-force" class="btn btn-danger w-100" onclick="fetch('/PR2force-state')">
FORCER ON/OFF
</button>
</div>
</div>
<div class="card-footer">
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card h-100">
<div class="card-header bg-dark text-white">
<h5 class="card-title mb-0">Programmateur 3</h5>
</div>
<div class="card-body">
<p class="mb-1">Statut : <span id="PR3relay-status" class="badge bg-secondary">---</span>
</p>
<p class="mb-3">Mode : <strong id="PR3mode-text">---</strong></p>
<button class="btn btn-outline-primary btn-sm w-100 mb-3" onclick="fetch('/PR3toggle-mode')">
Changer Mode (Auto/Man)
</button>
<div id="PR3timer-ui" class="border-top pt-2">
<label class="form-label small">Début :</label>
<input type="time" id="PR3debut" class="form-control form-control-sm mb-2">
<label class="form-label small">Fin :</label>
<input type="time" id="PR3fin" class="form-control form-control-sm mb-2">
<button class="btn btn-success btn-sm w-100 mt-2" onclick="PR3save()">Sauvegarder</button>
<p id="PR3msg" class="small text-muted mt-1"></p>
</div>
<div id="PR3btn-ui" class="border-top pt-2">
<button id="PR3btn-force" class="btn btn-danger w-100" onclick="fetch('/PR3force-state')">
FORCER ON/OFF
</button>
</div>
</div>
<div class="card-footer">
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card h-100">
<div class="card-header bg-dark text-white">
<h5 class="card-title mb-0">Programmateur 4</h5>
</div>
<div class="card-body">
<p class="mb-1">Statut : <span id="PR4relay-status" class="badge bg-secondary">---</span>
</p>
<p class="mb-3">Mode : <strong id="PR4mode-text">---</strong></p>
<button class="btn btn-outline-primary btn-sm w-100 mb-3" onclick="fetch('/PR4toggle-mode')">
Changer Mode (Auto/Man)
</button>
<div id="PR4timer-ui" class="border-top pt-2">
<label class="form-label small">Début :</label>
<input type="time" id="PR4debut" class="form-control form-control-sm mb-2">
<label class="form-label small">Fin :</label>
<input type="time" id="PR4fin" class="form-control form-control-sm mb-2">
<button class="btn btn-success btn-sm w-100 mt-2" onclick="PR4save()">Sauvegarder</button>
<p id="PR4msg" class="small text-muted mt-1"></p>
</div>
<div id="PR4btn-ui" class="border-top pt-2">
<button id="PR4btn-force" class="btn btn-danger w-100" onclick="fetch('/PR4force-state')">
FORCER ON/OFF
</button>
</div>
</div>
<div class="card-footer">
</div>
</div>
</div>
</div>
</div>
<script>
async function update() {
try {
const res = await fetch('/get-data');
const data = await res.json();
// Mise à jour de l'affichage
document.getElementById('time').innerText = data.actuelle;
document.getElementById('PR1mode-text').innerText = data.PR1auto ? "AUTOMATIQUE" : "MANUEL";
document.getElementById('PR2mode-text').innerText = data.PR2auto ? "AUTOMATIQUE" : "MANUEL";
document.getElementById('PR3mode-text').innerText = data.PR3auto ? "AUTOMATIQUE" : "MANUEL";
document.getElementById('PR4mode-text').innerText = data.PR4auto ? "AUTOMATIQUE" : "MANUEL";
// Gestion de l'affichage dynamique
const timerSectionPR1 = document.getElementById('PR1timer-ui');
const forceBtnPR1 = document.getElementById('PR1btn-ui');
const timerSectionPR2 = document.getElementById('PR2timer-ui');
const forceBtnPR2 = document.getElementById('PR2btn-ui');
const timerSectionPR3 = document.getElementById('PR3timer-ui');
const forceBtnPR3 = document.getElementById('PR3btn-ui');
const timerSectionPR4 = document.getElementById('PR4timer-ui');
const forceBtnPR4 = document.getElementById('PR4btn-ui');
if (data.PR1auto) {
timerSectionPR1.style.display = "block";
forceBtnPR1.style.display = "none";
} else {
timerSectionPR1.style.display = "none";
forceBtnPR1.style.display = "block";
}
if (data.PR2auto) {
timerSectionPR2.style.display = "block";
forceBtnPR2.style.display = "none";
} else {
timerSectionPR2.style.display = "none";
forceBtnPR2.style.display = "block";
}
if (data.PR3auto) {
timerSectionPR3.style.display = "block";
forceBtnPR3.style.display = "none";
} else {
timerSectionPR3.style.display = "none";
forceBtnPR3.style.display = "block";
}
if (data.PR4auto) {
timerSectionPR4.style.display = "block";
forceBtnPR4.style.display = "none";
} else {
timerSectionPR4.style.display = "none";
forceBtnPR4.style.display = "block";
}
const PR1status = document.getElementById('PR1relay-status');
// Mise à jour du texte
PR1status.innerText = data.PR1boolEtat ? "ALLUMÉ" : "ÉTEINT";
// Mise à jour de la couleur (Bootstrap Classes)
if (data.PR1boolEtat) {
PR1status.className = "badge bg-success"; // Vert
} else {
PR1status.className = "badge bg-danger"; // Rouge
}
const PR2status = document.getElementById('PR2relay-status');
// Mise à jour du texte
PR2status.innerText = data.PR2boolEtat ? "ALLUMÉ" : "ÉTEINT";
// Mise à jour de la couleur (Bootstrap Classes)
if (data.PR2boolEtat) {
PR2status.className = "badge bg-success"; // Vert
} else {
PR2status.className = "badge bg-danger"; // Rouge
}
const PR3status = document.getElementById('PR3relay-status');
// Mise à jour du texte
PR3status.innerText = data.PR3boolEtat ? "ALLUMÉ" : "ÉTEINT";
// Mise à jour de la couleur (Bootstrap Classes)
if (data.PR3boolEtat) {
PR3status.className = "badge bg-success"; // Vert
} else {
PR3status.className = "badge bg-danger"; // Rouge
}
const PR4status = document.getElementById('PR4relay-status');
// Mise à jour du texte
PR4status.innerText = data.PR4boolEtat ? "ALLUMÉ" : "ÉTEINT";
// Mise à jour de la couleur (Bootstrap Classes)
if (data.PR4boolEtat) {
PR4status.className = "badge bg-success"; // Vert
} else {
PR4status.className = "badge bg-danger"; // Rouge
}
// Chargement initial des inputs
if (!window.loaded) {
document.getElementById('PR1debut').value = data.PR1debut;
document.getElementById('PR1fin').value = data.PR1fin;
document.getElementById('PR2debut').value = data.PR2debut;
document.getElementById('PR2fin').value = data.PR2fin;
document.getElementById('PR3debut').value = data.PR3debut;
document.getElementById('PR3fin').value = data.PR3fin;
document.getElementById('PR4debut').value = data.PR4debut;
document.getElementById('PR4fin').value = data.PR4fin;
window.loaded = true;
}
} catch (e) { console.error("Erreur de synchronisation"); }
}
async function PR1save() {
const PR1body = new FormData();
PR1body.append('PR1debut', document.getElementById('PR1debut').value);
PR1body.append('PR1fin', document.getElementById('PR1fin').value);
const PR1res = await fetch('/PR1save', { method: 'POST', body: PR1body });
if (PR1res.ok) {
document.getElementById('PR1msg').innerText = "Sauvegardé avec succès !";
setTimeout(() => document.getElementById('PR1msg').innerText = "", 3000);
}
}
async function PR2save() {
const PR2body = new FormData();
PR2body.append('PR2debut', document.getElementById('PR2debut').value);
PR2body.append('PR2fin', document.getElementById('PR2fin').value);
const PR2res = await fetch('/PR2save', { method: 'POST', body: PR2body });
if (PR2res.ok) {
document.getElementById('PR2msg').innerText = "Sauvegardé avec succès !";
setTimeout(() => document.getElementById('PR2msg').innerText = "", 3000);
}
}
async function PR3save() {
const PR3body = new FormData();
PR3body.append('PR3debut', document.getElementById('PR3debut').value);
PR3body.append('PR3fin', document.getElementById('PR3fin').value);
const PR3res = await fetch('/PR3save', { method: 'POST', body: PR3body });
if (PR3res.ok) {
document.getElementById('PR3msg').innerText = "Sauvegardé avec succès !";
setTimeout(() => document.getElementById('PR3msg').innerText = "", 3000);
}
}
async function PR4save() {
const PR4body = new FormData();
PR4body.append('PR4debut', document.getElementById('PR4debut').value);
PR4body.append('PR4fin', document.getElementById('PR4fin').value);
const PR4res = await fetch('/PR4save', { method: 'POST', body: PR4body });
if (PR4res.ok) {
document.getElementById('PR4msg').innerText = "Sauvegardé avec succès !";
setTimeout(() => document.getElementById('PR4msg').innerText = "", 3000);
}
}
// Rafraîchissement toutes les secondes
setInterval(update, 1000);
update();
</script>
</body>
</html>
