Bonjour jef59,
Merçi pour le chéma élect..
je l'ai transposé avec les composantes électriques, si tu pouvais valider que j'ai bien compris ton dessin. Je te joint également le code du programme Spaduino 1.86 au cas ou il y aurait des erreurs ou des améliorations à apporter.
Merçi beaucoup de ton aide.
1 relais_2 contacteurs_Branchement électrique.pdf (1,8 Mo)
`
// ═══════════════════════════════════════════════════════════════════════════
// SpaDuino v1.86
// ═══════════════════════════════════════════════════════════════════════════
// Corrections et adaptations v1.86 :
// - Capteur crépusculaire déplacé de A5 vers A0 (conflit I2C corrigé)
// - Broche 4 (résistance) retirée — pas de chauffage électrique
// - Bulleur retiré — manuel
// - Lampe intérieure (A1) retirée — pas installée
// - Lampe extérieure spa (A2) retirée — pas installée
// - Lampe jardin (A3) gardée mais désactivée — future installation
// - Chauffage via Pompe GV uniquement (chaleur moteur)
// - Pompe GV et PV jamais ensemble — GV remplace PV
// - Plages filtration : 10h00→10h30 et 18h30→18h59
// - Circulation minimale : Pompe PV 20 min si aucune pompe depuis 3 heures
// - Chrono 3 heures remis à zéro si GV ou PV tourne
// - Lampe jardin : crépusculaire + télécommande (quand installée)
// - Alimentation générale via horloge mécanique externe 8h00→22h00
// - [FIX] Numéro de version affiché sur LCD utilise désormais la constante VERSION
// - [FIX] delay() bloquant (3s) dans demarrerPompeGV() remplacé par logique millis()
// - [FIX] programmeMassage1() restructurée : demarrerMassage1() + gestion dans gererPompes()
// arreterMassage() inconditionnel supprimé
// ═══════════════════════════════════════════════════════════════════════════
#include <DallasTemperature.h> // Sonde température DS18B20
#include <SoftwareSerial.h> // Module HC-06 BlueTooth
#include <Wire.h> // Communications I2C
#include <RTClib.h> // Horloge temps réel DS3231
#include <hd44780.h> // Bibliothèque LCD hd44780
#include <hd44780ioClass/hd44780_I2Cexp.h> // Contrôle écran LCD via I2C
#include <OneWire.h> // Sonde température OneWire
#include <IRremote.h> // ⚠️ Garder la VERSION 3.9.0
hd44780_I2Cexp ecran;
// ═══════════════════════════════════════════════════════════════════════════
// BROCHES
// ═══════════════════════════════════════════════════════════════════════════
const int BROCHE_SONDE_TEMP = 6;
const int BROCHE_IR = 9;
const int BROCHE_BT_RX = 10;
const int BROCHE_BT_TX = 11;
const int BROCHE_POMPE_PV = 2; // Relais N1 → contacteur pompe PV
const int BROCHE_POMPE_GV = 3; // Relais N2 → contacteur pompe GV
// Broche 4 retirée — pas de résistance électrique
const int BROCHE_COMMUN_POMPE = 5; // Alimentation commune relais
const int BROCHE_CREPUSCULAIRE = A0; // ✅ Déplacé de A5 — conflit I2C corrigé
// A1 retiré — lampe intérieure non installée
// A2 retiré — lampe extérieure spa non installée
const int BROCHE_LAMPE_JARDIN = A3; // Futur — désactivé pour l'instant
// A4 = SDA réservé I2C
// A5 = SCL réservé I2C
// ═══════════════════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════════════════
const int SEUIL_CREPUSCULAIRE = 300;
const int TEMP_MAXI = 37;
const int TEMP_MINI = 28;
const int TEMP_CONSIGNE_DEFAUT = 32;
const int DUREE_FORCEE_POMPE_MIN = 20; // minutes — jets massage
const int DUREE_FORCEE_LAMPE_H = 2; // heures — lampe jardin forcée
const int DELAI_FILTRE_JET_J = 7; // jours
const int DELAI_FILTRE_HP_J = 21; // jours
const int HEURE_FIN_ECLAIRAGE = 23;
// Délais entre activation des vitesses Pv et Gv de la pompe
// [FIX] Conservé comme constante de durée, mais n'est plus utilisé avec delay()
const unsigned long DELAI_PV_VERS_GV_MS = 3000; // 3 secondes
// Plages de filtration : 10h00→10h30 et 18h30→19h00
// Format : { heure_debut, minute_debut, heure_fin, minute_fin }
// Durée sans activité avant circulation minimale : 3 heures en secondes
const unsigned long DUREE_INACTIVITE_S = 3UL * 3600UL;
// Durée de la circulation minimale : 20 minutes en secondes
const unsigned long DUREE_CIRCULATION_MIN_S = 20UL * 60UL;
const String VERSION = "1.86";
// Codes IR télécommande
const unsigned long IR_MASSAGE1 = 0xFD00FF;
const unsigned long IR_TEMP_PLUS = 0xFD8877;
const unsigned long IR_TEMP_MOINS = 0xFD9867;
const unsigned long IR_MENU = 0xFD609F;
const unsigned long IR_JARDIN = 0xFD50AF; // Lampe jardin (future)
const unsigned long IR_POMPE_PV = 0xFD30CF;
const unsigned long IR_POMPE_GV = 0xFD708F;
const unsigned long IR_OK = 0xFDA857;
const unsigned long IR_ECRAN = 0xFD40BF;
const unsigned long IR_BUG = 0xFFFFFFFF;
// ═══════════════════════════════════════════════════════════════════════════
// OBJETS MATÉRIELS
// ═══════════════════════════════════════════════════════════════════════════
OneWire busTemp(BROCHE_SONDE_TEMP);
DallasTemperature sondeTemp(&busTemp);
SoftwareSerial bluetooth(BROCHE_BT_RX, BROCHE_BT_TX);
RTC_DS3231 horloge;
IRrecv recepteurIR(BROCHE_IR);
decode_results signalIR;
const int LCD_COLS = 20;
const int LCD_LIGNES = 4;
// ═══════════════════════════════════════════════════════════════════════════
// NOMS DE JOURS EN MÉMOIRE FLASH (PROGMEM)
// ═══════════════════════════════════════════════════════════════════════════
const char j0[] PROGMEM = "Dimanche";
const char j1[] PROGMEM = "Lundi";
const char j2[] PROGMEM = "Mardi";
const char j3[] PROGMEM = "Mercredi";
const char j4[] PROGMEM = "Jeudi";
const char j5[] PROGMEM = "Vendredi";
const char j6[] PROGMEM = "Samedi";
const char* const nomJours[] PROGMEM = { j0, j1, j2, j3, j4, j5, j6 };
char bufJour[10];
void afficherNomJour(uint8_t indexJour) {
strcpy_P(bufJour, (char*)pgm_read_word(&nomJours[indexJour]));
ecran.print(bufJour);
}
// ═══════════════════════════════════════════════════════════════════════════
// VARIABLES GLOBALES
// ═══════════════════════════════════════════════════════════════════════════
DateTime maintenant;
int tempActuelle = 0;
int consigneTemp = TEMP_CONSIGNE_DEFAUT;
int valeurCrepuscule = 0;
bool chauffageActif = false; // Pompe GV active pour chauffer
bool eclairageActif = false;
bool retroEclairage = true;
// Lampe jardin — future installation
bool lampeForceeJardin = false;
DateTime finMarcheForceeLampe;
// Pompes
bool pompeEnTransition = false;
bool pompeGvActive = false;
bool pompePvActive = false;
bool pompeGvForcee = false; // jets massage via télécommande
bool pompePvForcee = false;
DateTime finMarcheForceePompe;
// [FIX] Transition PV→GV non-bloquante
unsigned long tsTransitionPvGv = 0;
bool enAttenteTransitionGV = false;
//Massage1
bool massageEnCours = false;
// Filtration
bool filtrationActive = false;
// Circulation minimale (3 heures sans activité)
// dermierActivite : moment où une pompe a tourné pour la dernière fois
DateTime derniereActivite;
bool circulationMinActive = false;
DateTime finCirculationMin;
// Délais chauffage sans blocking (millis)
unsigned long tsChauffageAllumage = 0;
unsigned long tsChauffageExtinction = 0;
bool enAttenteAllumage = false;
bool enAttenteExtinction = false;
String messageRecu;
// ═══════════════════════════════════════════════════════════════════════════
// UTILITAIRES
// ═══════════════════════════════════════════════════════════════════════════
void afficherDeuxChiffres(int valeur) {
if (valeur <= 9) ecran.print(F("0"));
ecran.print(valeur);
}
void afficherHeure(DateTime dt) {
afficherDeuxChiffres(dt.hour());
ecran.print(F(":"));
afficherDeuxChiffres(dt.minute());
ecran.print(F(":"));
afficherDeuxChiffres(dt.second());
}
void afficherDateCourte(DateTime dt) {
if (dt.day() <= 9) ecran.print('0');
ecran.print(dt.day());
ecran.print('/');
if (dt.month() <= 9) ecran.print('0');
ecran.print(dt.month());
}
void lireTemperature() {
sondeTemp.requestTemperatures();
tempActuelle = (int)sondeTemp.getTempCByIndex(0);
}
void afficherDateHeure() {
// Ligne physique 2 = setCursor(0, 1)
static int derniereMinute = -1;
if (maintenant.minute() == derniereMinute) return;
derniereMinute = maintenant.minute();
ecran.setCursor(0, 1);
// 1. Récupérer le nom du jour (3 lettres)
strcpy_P(bufJour, (char*)pgm_read_word(&nomJours[maintenant.dayOfTheWeek()]));
ecran.print(bufJour[0]);
ecran.print(bufJour[1]);
ecran.print(bufJour[2]);
ecran.print(F(" "));
afficherDeuxChiffres(maintenant.day());
ecran.print(F("/"));
afficherDeuxChiffres(maintenant.month());
ecran.print(F("/"));
ecran.print(maintenant.year() % 100);
ecran.print(F(" "));
afficherDeuxChiffres(maintenant.hour());
ecran.print(F(":"));
afficherDeuxChiffres(maintenant.minute());
effacerFinLigne(17);
}
void nettoyerIR() {
Serial.print(F("nettoyage IR : "));
Serial.println(signalIR.value, HEX);
recepteurIR.resume();
signalIR.value = 0;
delay(100);
}
void lireBluetooth() {
messageRecu = "";
while (bluetooth.available()) {
delay(3);
messageRecu += (char)bluetooth.read();
}
if (messageRecu.length() > 0)
Serial.println(messageRecu);
}
void envoyerBluetooth() {
bluetooth.print("*T" + String(tempActuelle) + "*");
bluetooth.print("*C" + String(consigneTemp) + "*");
bluetooth.print(chauffageActif ? "*RR255G0B0*" : "*RR255G255B255*");
// Allégé : plus d'alertes filtres ici
if (circulationMinActive) bluetooth.print(F("*MCirculation minimale*"));
else if (filtrationActive) bluetooth.print(F("*MFiltration en cours*"));
else if (chauffageActif) bluetooth.print(F("*MChauffage en cours*"));
else bluetooth.print(F("*MSpaduino*"));
}
// ═══════════════════════════════════════════════════════════════════════════
// AFFICHAGE LCD
// ═══════════════════════════════════════════════════════════════════════════
// Complète une ligne LCD avec des espaces jusqu'à 20 caractères
// Empêche les résidus de l'affichage précédent de rester visibles
void effacerFinLigne(int nbCaracteresEcrits) {
for (int i = nbCaracteresEcrits; i < 20; i++)
ecran.print(' ');
}
// [FIX] Utilise la constante VERSION au lieu d'un texte codé en dur
void effacerEcran() {
ecran.clear();
ecran.setCursor(0, 0);
ecran.print(F("* SpaDuino v"));
ecran.print(VERSION);
ecran.print(F(" *"));
}
void afficherEtatGeneral() {
// Ligne physique 1 — texte fixe, jamais modifie ici
// Gere par effacerEcran() et setup()
}
void afficherTemperatures() {
// Ligne 2 — format exact : "Eau:20C Cons:32C " = 20 car.
// "Eau:"(4) + temp(1-3) + "C"(1) + espaces + "Cons:"(5) + cons(2) + "C"(1)
ecran.setCursor(0, 2);
ecran.print(F("Eau:"));
int nbCar = 4; // "Eau:" = 4 caracteres
if (tempActuelle == -127) {
ecran.print(F("--- "));
nbCar += 4;
} else if (tempActuelle < 10) {
ecran.print(tempActuelle);
ecran.print(F("C "));
nbCar += 5;
} else if (tempActuelle < 100) {
ecran.print(tempActuelle);
ecran.print(F("C "));
nbCar += 5;
} else {
ecran.print(tempActuelle);
ecran.print(F("C "));
nbCar += 6;
}
ecran.print(F("Cons:"));
ecran.print(consigneTemp);
ecran.print(F("C "));
}
void afficherActionneurs() {
// Ligne 3 — format exact : "Ch:O PV:O GV:O " = 20 car.
// Utilise O/N au lieu de OUI/NON pour tenir en 20 colonnes
ecran.setCursor(0, 3);
ecran.print(F("Ch:"));
ecran.print(chauffageActif ? F("O") : F("N"));
ecran.print(F(" PV:"));
ecran.print(pompePvActive ? F("O") : F("N"));
ecran.print(F(" GV:"));
ecran.print(pompeGvActive ? F("O") : F("N"));
ecran.print(F(" ")); // complete jusqu'a 20 colonnes
}
void afficherEcran() {
// Limite le rafraîchissement à 1 fois par seconde (1000 ms)
static unsigned long dernierChronoEcran = 0;
if (millis() - dernierChronoEcran < 1000UL) return;
dernierChronoEcran = millis();
if (maintenant.hour() == 0 && maintenant.minute() == 0 && maintenant.second() == 0)
ecran.clear();
// 1. On écrit d'abord la date et l'heure (Ligne 2)
// Si elle essaie de baver en haut, elle le fera maintenant
afficherDateHeure();
// 2. On FORCE IMMÉDIATEMENT la Ligne 1 à se réécrire par-dessus les résidus !
// [FIX] Ligne 1 réécrite avec VERSION (plus de v1.85 codé en dur)
ecran.setCursor(0, 0);
ecran.print(F("* SpaDuino v"));
ecran.print(VERSION);
ecran.print(F(" *"));
// 3. On affiche le reste normalement sur les lignes 3 et 4
afficherTemperatures();
afficherActionneurs();
}
void afficherDebugSerie() {
Serial.print(F("Heure : "));
afficherDeuxChiffres(maintenant.hour());
Serial.print(F(":"));
afficherDeuxChiffres(maintenant.minute());
Serial.print(F(":"));
afficherDeuxChiffres(maintenant.second());
Serial.println();
Serial.print(F("Température : "));
Serial.print(tempActuelle);
Serial.print(F("C / Consigne : "));
Serial.print(consigneTemp);
Serial.println(F("C"));
Serial.print(F("Pompe PV : "));
Serial.print(pompePvActive ? F("OUI") : F("NON"));
Serial.print(F(" Pompe GV : "));
Serial.print(pompeGvActive ? F("OUI") : F("NON"));
Serial.print(F(" Chauffage : "));
Serial.println(chauffageActif ? F("OUI") : F("NON"));
Serial.print(F("Crépusculaire : "));
Serial.println(valeurCrepuscule);
}
// ═══════════════════════════════════════════════════════════════════════════
// GESTION FILTRATION
// Plages : 10h00→10h30 et 18h30→18h59
// ═══════════════════════════════════════════════════════════════════════════
void gererFiltration() {
const int h = maintenant.hour();
const int m = maintenant.minute();
// Vérifie si on est dans une plage de filtration
// Plage 1 : 10h00 → 10h30
bool dansPlage1 = (h ==10) && (m >= 0) && (m < 30); // 10:00-10:29
// Plage 2 : 18h30 → 19h00
bool dansPlage2 = (h == 18) && (m >= 30) && (m < 60); // 18:30-18:59
// Donc on ouvre à 18:30-18:59 et 19:00 n'est pas inclus (fin à 19:00 pile)
filtrationActive = dansPlage1 || dansPlage2;
}
// ═══════════════════════════════════════════════════════════════════════════
// GESTION ÉCLAIRAGE
// Lampe jardin : crépusculaire automatique + télécommande
// Lampe intérieure et extérieure spa : non installées
// ═══════════════════════════════════════════════════════════════════════════
void gererEclairage() {
valeurCrepuscule = analogRead(BROCHE_CREPUSCULAIRE);
bool nuit = (valeurCrepuscule <= SEUIL_CREPUSCULAIRE
&& maintenant.hour() >= 17
&& maintenant.hour() < HEURE_FIN_ECLAIRAGE);
// ── Lampe jardin (A3) ────────────────────────────────────────────────────
// DÉSACTIVÉE pour l'instant — décommenter quand installée :
// bool allumerJardin = nuit || lampeForceeJardin;
// digitalWrite(BROCHE_LAMPE_JARDIN, allumerJardin ? LOW : HIGH);
// eclairageActif = allumerJardin;
// Arrêt automatique lampe jardin forcée
if (maintenant.unixtime() >= finMarcheForceeLampe.unixtime() && lampeForceeJardin) {
// digitalWrite(BROCHE_LAMPE_JARDIN, HIGH); // décommenter quand installée
lampeForceeJardin = false;
}
eclairageActif = false; // mettre à jour quand lampe jardin installée
}
// ═══════════════════════════════════════════════════════════════════════════
// GESTION POMPES
// Règle : Pompe GV et PV jamais ensemble — GV remplace PV
// ═══════════════════════════════════════════════════════════════════════════
// Démarre la Pompe PV — vérifie que GV ne tourne pas
void demarrerPompePV() {
if (pompeGvActive || pompeGvForcee || enAttenteAllumage || pompeEnTransition) return; // GV prioritaire — PV ne démarre pas
pompeEnTransition = true;
pompePvActive = true;
digitalWrite(BROCHE_POMPE_PV, HIGH); // OFF (si active LOW)
digitalWrite(BROCHE_POMPE_PV, LOW); // ON
digitalWrite(BROCHE_COMMUN_POMPE, HIGH);
derniereActivite = maintenant; // remet le chrono d'inactivité à zéro
pompeEnTransition = false;
}
// Arrête la Pompe PV
void arreterPompePV() {
pompeEnTransition = true;
pompePvActive = false;
digitalWrite(BROCHE_POMPE_PV, LOW); // OFF
// Ne coupe pas COMMUN_POMPE si GV pourrait démarrer ensuite
if (!pompeGvActive && enAttenteAllumage)
digitalWrite(BROCHE_COMMUN_POMPE, LOW);
pompeEnTransition = false;
}
// [FIX] Activation effective de GV — séparée de demarrerPompeGV()
// afin de permettre un appel différé (après le délai non-bloquant)
void _activerPompeGV() {
pompeEnTransition = true;
pompeGvActive = true;
pompePvActive = false;
chauffageActif = false; // sera réévalué dans gererChauffage
digitalWrite(BROCHE_POMPE_GV, HIGH); // OFF puis...
digitalWrite(BROCHE_POMPE_GV, LOW); // ON
digitalWrite(BROCHE_COMMUN_POMPE, HIGH);
derniereActivite = maintenant;
pompeEnTransition = false;
enAttenteTransitionGV = false;
Serial.println(F("Pompe GV : active"));
}
// Démarre la Pompe GV — arrête PV si elle tournait
// [FIX] demarrerPompeGV() — plus de delay() bloquant.
// Si PV tourne, on l'arrête et on arme un délai millis().
// _activerPompeGV() sera appelée par gererTransitionGV() dans loop().
void demarrerPompeGV() {
if (pompeEnTransition || enAttenteTransitionGV) return;
if (pompePvActive){
arreterPompePV(); // GV remplace PV
enAttenteTransitionGV = true;
tsTransitionPvGv = millis();
return; // on reviendra dans gererTransitionGV()
}
_activerPompeGV(); // démarrage immédiat si PV ne tournait pas
}
// [FIX] Gestion non-bloquante du délai PV→GV — à appeler dans loop()
void gererTransitionGV() {
if (!enAttenteTransitionGV) return;
if (millis() - tsTransitionPvGv >= DELAI_PV_VERS_GV_MS) {
_activerPompeGV();
}
}
// Arrête la Pompe GV
void arreterPompeGV() {
pompeEnTransition = true;
pompeGvActive = false;
pompeGvForcee = false;
chauffageActif = false;
enAttenteTransitionGV = false;
digitalWrite(BROCHE_POMPE_GV, HIGH); // OFF
digitalWrite(BROCHE_COMMUN_POMPE, LOW);
pompeEnTransition = false;
}
void gererPompes() {
// ! NE RIEN FAIRE PENDANT UNE TRANSITION
if (pompeEnTransition) return;
// [FIX] Arrêt automatique marche forcée (jets massage / pompe forcée)
if (maintenant.unixtime() >= finMarcheForceePompe.unixtime()
&& (pompePvForcee || pompeGvForcee)) {
pompePvForcee = false;
pompeGvForcee = false;
massageEnCours = false;
if (pompePvActive) arreterPompePV();
if (pompeGvActive) arreterPompeGV();
effacerEcran();
Serial.println(F("Massage/PompeForcée : temps écoulé - arrêt"));
}
// Filtration — Pompe PV si pas de GV active
if (filtrationActive && !pompeGvActive && !pompePvActive && !pompeEnTransition)
demarrerPompePV();
if (!filtrationActive && pompePvActive && !pompePvForcee)
arreterPompePV();
}
// ═══════════════════════════════════════════════════════════════════════════
// GESTION CHAUFFAGE
// Chauffage = Pompe GV (chaleur moteur)
// GV démarre si temp < consigne-1, s'arrête si temp > consigne+1
// ═══════════════════════════════════════════════════════════════════════════
void gererChauffage() {
// Demande de chauffage — démarrer Pompe GV après 3 secondes
// Sonde invalide (-127) — on ne chauffe pas
if (tempActuelle == -127) return;
if (tempActuelle <= consigneTemp - 1 && !pompeGvActive && !enAttenteAllumage
&& !pompeGvForcee) {
enAttenteAllumage = true;
tsChauffageAllumage = millis();
}
if (enAttenteAllumage && (millis() - tsChauffageAllumage >= 3000UL)) {
enAttenteAllumage = false;
demarrerPompeGV();
chauffageActif = true;
Serial.println(F("Chauffage : Pompe GV démarrée"));
}
// Fin de chauffage — arrêter Pompe GV après 3 secondes
if (tempActuelle >= consigneTemp + 1 && chauffageActif && !enAttenteExtinction
&& !pompeGvForcee) {
enAttenteExtinction = true;
tsChauffageExtinction = millis();
}
if (enAttenteExtinction && (millis() - tsChauffageExtinction >= 3000UL)) {
enAttenteExtinction = false;
chauffageActif = false;
if (!pompeGvForcee) {
arreterPompeGV();
Serial.println(F("Chauffage : Pompe GV arrêetée"));
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// CIRCULATION MINIMALE
// Si aucune pompe n'a tourné depuis 3 heures → Pompe PV pendant 20 minutes
// Le chrono repart à zéro dès qu'une pompe tourne
// ═══════════════════════════════════════════════════════════════════════════
void gererCirculationMinimale() {
// Si une pompe tourne, on n'a pas besoin de circulation minimale
if (pompePvActive || pompeGvActive) {
derniereActivite = maintenant; // remet le chrono à zéro
circulationMinActive = false;
return;
}
// Vérifier si 3 heures se sont écoulées sans activité
unsigned long inactivite = maintenant.unixtime() - derniereActivite.unixtime();
if (!circulationMinActive && inactivite >= DUREE_INACTIVITE_S) {
// Démarrer circulation minimale
circulationMinActive = true;
finCirculationMin = maintenant + TimeSpan(0, 0, 20, 0);
demarrerPompePV();
Serial.println(F("Circulation minimale : Pompe PV démarrée"));
}
// Arrêter circulation minimale après 20 minutes
if (circulationMinActive && maintenant.unixtime() >= finCirculationMin.unixtime()) {
circulationMinActive = false;
arreterPompePV();
derniereActivite = maintenant; // remet le chrono à zéro
Serial.println(F("Circulation minimale : Pompe PV arrêtée"));
}
}
// ═══════════════════════════════════════════════════════════════════════════
// MENU FILTRE
// ═══════════════════════════════════════════════════════════════════════════
void afficherConfirmationRAZ() {
effacerEcran();
ecran.setCursor(0, 0);
ecran.print(F("Confirmer RAZ? "));
ecran.setCursor(0, 1);
ecran.print(F("OK=Oui 0=Annuler "));
}
bool attendreConfirmation() {
bool reponse = false;
bool enAttente = true;
while (enAttente) {
lireBluetooth();
if (recepteurIR.decode(&signalIR) || messageRecu.length() > 0) {
if (signalIR.value == IR_OK || messageRecu == "OK") {
reponse = true;
enAttente = false;
} else if (signalIR.value == IR_BUG) {
nettoyerIR();
} else {
reponse = false;
enAttente = false;
}
}
delay(100);
}
delay(100);
recepteurIR.resume();
effacerEcran();
return reponse;
}
// ═══════════════════════════════════════════════════════════════════════════
// PROGRAMME MASSAGE (Jets — Pompe GV forcée 20 minutes)
// ═══════════════════════════════════════════════════════════════════════════
// [FIX] Restructuré : demarrerMassage1() appelée une seule fois au déclenchement.
// Le suivi (temps écoulé, arrêt) est géré dans gererPompes().
// La variable massageEnCours permet une annulation manuelle dans gererCommandes().
//
/*void arreterMassage() {
if (!chauffageActif) arreterPompeGV();
pompeGvForcee = false;
effacerEcran();
Serial.println(F("Massage terminé"));
}
void programmeMassage1() {
const int CONSIGNE_TEMP_MASSAGE = 35;
messageRecu = "";
maintenant = horloge.now();
DateTime heureFinMassage = maintenant + TimeSpan(0, 0, DUREE_FORCEE_POMPE_MIN, 0);
effacerEcran();
ecran.setCursor(0, 0);
ecran.print(F("**Cycle Massage 1***"));
// Fin par le temps
maintenant = horloge.now(); // mise à jour
if (maintenant.unixtime() >= heureFinMassage.unixtime()){
massageEnCours = false;
}
// Annulation télécommande
if (recepteurIR.decode(&signalIR) || messageRecu.length() > 0) {
if (signalIR.value == IR_MASSAGE1 || messageRecu == "massage1") {
delay(100);
recepteurIR.resume();
massageEnCours = false;
} else if (signalIR.value == IR_BUG) {
nettoyerIR();
}
}
messageRecu = "";
delay(10);
arreterMassage();
}
*/
void demarrerMassage1() {
maintenant = horloge.now();
finMarcheForceePompe = maintenant + TimeSpan(0, 0, DUREE_FORCEE_POMPE_MIN, 0);
pompeGvForcee = true;
massageEnCours = true;
demarrerPompeGV();
effacerEcran();
ecran.setCursor(0, 0);
ecran.print(F("**Cycle Massage 1***"));
Serial.println(F("Massage démarré — Pompe GV forcée ON"));
}
// ═══════════════════════════════════════════════════════════════════════════
// GESTION DES COMMANDES IR / BLUETOOTH
// ═══════════════════════════════════════════════════════════════════════════
void gererCommandes() {
lireBluetooth();
if (!recepteurIR.decode(&signalIR) && messageRecu.length() == 0) return;
Serial.print(F("Code IR : 0x"));
Serial.println(signalIR.value, HEX);
Serial.print(F("Message BT : "));
Serial.println(messageRecu);
// ── Massage / Jets ───────────────────────────────────────────────────────
// [FIX] Logique toggle : démarrage si inactif, annulation si en cours
if (signalIR.value == IR_MASSAGE1 || messageRecu == "massage1") {
if (!massageEnCours) {
demarrerMassage1();
} else {
massageEnCours = false;
pompeGvForcee = false;
if (!chauffageActif) arreterPompeGV();
effacerEcran();
Serial.println(F("Massage annulé par télécommande"));
}
recepteurIR.resume();
messageRecu = "";
return;
}
// ── Pompe petite vitesse forcée ──────────────────────────────────────────
if (signalIR.value == IR_POMPE_PV || messageRecu == "pompePV") {
if (!pompePvForcee && !pompeGvActive) {
finMarcheForceePompe = maintenant + TimeSpan(0, 0, DUREE_FORCEE_POMPE_MIN, 0);
pompePvForcee = true;
demarrerPompePV();
Serial.println(F("Pompe PV forcée ON"));
} else {
pompePvForcee = false;
if (!filtrationActive) arreterPompePV();
Serial.println(F("Pompe PV forcée OFF"));
}
}
// ── Pompe grande vitesse forcée (Jets) ───────────────────────────────────
if (signalIR.value == IR_POMPE_GV || messageRecu == "pompeGV") {
if (!pompeGvForcee) {
finMarcheForceePompe = maintenant + TimeSpan(0, 0, DUREE_FORCEE_POMPE_MIN, 0);
pompeGvForcee = true;
demarrerPompeGV();
Serial.println(F("Pompe GV forcée ON"));
} else {
pompeGvForcee = false;
if (!chauffageActif) arreterPompeGV();
Serial.println(F("Pompe GV forcée OFF"));
}
}
// ── Consigne température ─────────────────────────────────────────────────
if (signalIR.value == IR_TEMP_PLUS || messageRecu == "temp+") {
if (consigneTemp < TEMP_MAXI) {
consigneTemp++;
Serial.print(F("Consigne temp : "));
Serial.println(consigneTemp);
}
}
if (signalIR.value == IR_TEMP_MOINS || messageRecu == "temp-") {
if (consigneTemp > TEMP_MINI) {
consigneTemp--;
Serial.print(F("Consigne temp : "));
Serial.println(consigneTemp);
}
}
// ── Menu filtre - Désactivé - Filtres supprimé ─────────
if (signalIR.value == IR_MENU || messageRecu == "menu") {
recepteurIR.resume();
}
// ── Lampe jardin (future installation) ────────────────────────
// DÉSACTIVÉ — décommenter quand lampe jardin installée sur A3 :
/*
if (signalIR.value == IR_JARDIN || messageRecu == "lampeJardin") {
lampeForceeJardin = !lampeForceeJardin;
if (lampeForceeJardin) {
finMarcheForceeLampe = maintenant + TimeSpan(0, DUREE_FORCEE_LAMPE_H, 0, 0);
digitalWrite(BROCHE_LAMPE_JARDIN, LOW);
} else {
digitalWrite(BROCHE_LAMPE_JARDIN, HIGH);
}
}
*/
// ── Rétroéclairage LCD ───────────────────────────────────────────────────
if (signalIR.value == IR_ECRAN || messageRecu == "ecran") {
retroEclairage = !retroEclairage;
ecran.setBacklight(retroEclairage ? HIGH : LOW);
}
recepteurIR.resume();
nettoyerIR();
messageRecu = "";
}
// ═══════════════════════════════════════════════════════════════════════════
// SETUP
// ═══════════════════════════════════════════════════════════════════════════
void setup() {
Serial.begin(115200);
int erreurLcd = ecran.begin(LCD_COLS, LCD_LIGNES);
if (erreurLcd) {
Serial.print(F("Erreur init LCD : "));
Serial.println(erreurLcd);
hd44780::fatalError(erreurLcd);
}
ecran.setBacklight(HIGH);
effacerEcran();
ecran.setCursor(0, 0);
ecran.print(F(" SpaDuino v"));
ecran.print(VERSION);
ecran.setCursor(0, 1);
ecran.print(F(" Initialisation..."));
sondeTemp.begin();
pinMode(2, OUTPUT);
pinMode(3, OUTPUT);
digitalWrite(2, HIGH);
digitalWrite(3, HIGH);
pinMode(BROCHE_COMMUN_POMPE, OUTPUT);
digitalWrite(BROCHE_COMMUN_POMPE, LOW);
pinMode(BROCHE_POMPE_PV, OUTPUT);
digitalWrite(BROCHE_POMPE_PV, LOW);
delay (2000);
//pinMode(BROCHE_POMPE_PV, OUTPUT); digitalWrite(BROCHE_POMPE_PV, HIGH);//temporaire
//delay (2000);//temporaire
pinMode(BROCHE_POMPE_GV, OUTPUT);
digitalWrite(BROCHE_POMPE_GV, LOW);
delay(2000);
// Lampe jardin — future installation
// pinMode(BROCHE_LAMPE_JARDIN, OUTPUT); digitalWrite(BROCHE_LAMPE_JARDIN, HIGH);
digitalWrite(BROCHE_POMPE_PV, LOW);
digitalWrite(BROCHE_POMPE_GV, LOW);
digitalWrite(BROCHE_COMMUN_POMPE, LOW);
pompeGvActive = false;
pompePvActive = false;
chauffageActif = false;
pompeEnTransition = false;
enAttenteTransitionGV = false;
massageEnCours = false;
recepteurIR.enableIRIn();
bluetooth.begin(9600);
delay(1000);
// Initialisation du DS3231 avec vérification
// Si le DS3231 ne répond pas, on affiche un message clair au lieu de bloquer
if (!horloge.begin()) {
Serial.println(F("ERREUR : DS3231 non trouvé !"));
Serial.println(F("Verifiez SDA et SCL du DS3231."));
ecran.setCursor(0, 2);
ecran.print(F("ERREUR : Horloge! "));
ecran.setCursor(0, 3);
ecran.print(F("Verif. SDA/SCL RTC "));
// On ne bloque pas — on continue avec une heure par défaut
// pour que le reste du programme fonctionne quand même
} else {
Serial.println(F("DS3231 OK"));
// Décommenter UNE SEULE FOIS pour remettre l'horloge à l'heure :
// horloge.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
maintenant = horloge.now();
derniereActivite = maintenant; // démarre le chrono d'inactivité
ecran.setCursor(0, 2);
ecran.print(F(" Heure : "));
afficherDeuxChiffres(maintenant.hour());
ecran.print(F(":"));
afficherDeuxChiffres(maintenant.minute());
delay(2000);
effacerEcran(); // efface + ecrit texte fixe ligne 1
Serial.print(F("SpaDuino Version "));
Serial.println(VERSION);
Serial.println(F("Initialisation OK"));
}
// ═══════════════════════════════════════════════════════════════════════════
// LOOP
// ═══════════════════════════════════════════════════════════════════════════
void loop() {
lireTemperature();
envoyerBluetooth();
maintenant = horloge.now();
afficherEcran();
afficherDebugSerie();
gererCommandes();
gererFiltration();
gererEclairage();
// gèrer le délai non-bloquant, délais Pv-Gv :
gererTransitionGV(); // [FIX] gestion non-bloquante délai PV→GV
gererPompes();
gererChauffage();
gererCirculationMinimale();
}
`