Contrôler 2 contacteurs avec un relais

Bonjour,
chéma électrique + relais intermédiaire.pdf (1,4 Mo)
Branchement relais.pdf (232,4 Ko)

Je veux contrôler 2 contacteurs (bobine 120volts) avec un relais (bobine 120 volts) afin que la petite vitesse et la grande vitesse du moteur de la pompe de mon spa ne démarre pas en même temps. J'ai demandé à IA mais ses indications ne sont pas claires pour moi. Je vous joint le chéma électrique actuel et les modifications que je doit faire au filage pour que cela fonctionne avec l'ajout du relais intermédiaire.
Merçi pour votre aide.

Bonjour

Quel type d’aide souhaitez vous - le schéma électrique et / ou du code ?

Le chéma électrique.
Merçi.

Je suis sur mon iPhone donc je ne peux pas simplement envoyer des dessins mais en regardant votre schéma, l'Arduino peut effectivement commander les deux relais séparément en éteignant l'un et en allumant l'autre par programme, et c'est déjà ce que fait le module relais avec NO1 et NO2, donc sur le plan logiciel seul, un délai dans le code entre la coupure d'une vitesse et l'activation de l'autre suffit techniquement à éviter qu'elles fonctionnent ensemble.

Le problème est que cette protection dépend entièrement du programme. Si le code plante, si l'Arduino redémarre au mauvais moment, si une commande infrarouge est mal reçue, ou si un bogue apparaît dans une future modification du programme, rien n'empêche physiquement les deux contacteurs d'être alimentés en même temps, ce qui pourrait causer un court-circuit entre les deux vitesses du moteur ou endommager le moteur et les contacteurs.

Pour cette raison, je recommanderais d'ajouter deux relais intermédiaires bobine 120 volts, un pour chaque vitesse, montés en miroir avec leurs contacts normalement fermés croisés, de sorte que le relais Petite Vitesse ait sa bobine en parallèle avec le contacteur Petite Vitesse et son contact NF en série avec l'alimentation de la bobine du contacteur Grande Vitesse, et inversement pour le relais Grande Vitesse.

Cette solution crée un verrouillage matériel indépendant du programme, qui garantit qu'un contacteur ne peut physiquement pas être alimenté tant que l'autre est actif, peu importe ce que fait le code, et vient s'ajouter à la protection déjà prévue dans le programme Arduino plutôt que de la remplacer.

Parfait, j'ai 2 relais (bobine 120Volt); comment je fais cela?

Bonsoir,

Me suis je trompé?

Je vous certaines incohérences dans ce qui semble être le raisonnement de l'IA


Ensuite, avant de regarder en détails la proposition de l'IA, je crois que 2 relais pour la commande Arduino + 1 relais Intermédiaire + 1 relais PV + 1 relais GV = "ça devient lourd".
Ça pourrait peut être se simplifier.

Et pour ma culture perso, ce moteur m'intrigue, je ne doute pas qu'il marche, je le demande quel est son type?
Est-ce possible de voir une photo de sa plaque signalétique?

Je vous envoies un schéma de ce que j'ai compris partant du principe que celui que vous avez envoyé, relais intermédiaire non branché, marche.

(Dans l'immédiat je n'ai pas trop le temps, désolé,)

OK, le OUI c'est mon erreur.
Ci-joint information sur le moteur.


Désolé mais le chéma électrique n'était pas à jour. Le voiçi.
Chéma élextrique avec relais_13-07-2026.pdf (1,7 Mo)

Bonsoir,

J'ai tardé à répondre.

Il y a une multitude de solutions je pense.
J'ai opté pour une solution qui modifie le moins le câblage existant

J'insére le "relais intermédiaire" pour empêcher que PV et GV ne soient en fonctionnement en même temps même si le 2 sont pilotés par Arduino.

Ici GV est prioritaire si jamais PV et GV sont demandés en même temps, mais ce n' est pas un probléme de rendre PV prioritaire plutôt que GV.

J'ai fais le schéma à la "va vite" mais n'hésitez pas à demander des précisions.

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

Hello,

J'ai vérifié si tu avais trznsposé correctement ma modif dans ton schéma, ça me semble OK.

Je ne suis pas assez calé pour vérifier ton code donc je n'essaie même pas.

Pour ma curiosité personnelle, je n'ai pas tout compris à ton installation électrique, elle est de quel pays? (110V, ce n'est pas français).

A quoi sert le signal HALT (ou MALT) qui à l'air d'arriver sur un gros condensateur, la photo de ton moteur n'est pas trés lisible!

Est ce que c'est une installation qui a déjà tournée avec ces composants?

Je dis cela car au démarrage, le courant d'appel d'un moteur est souvent trés supérieur au courant nominal, il faut que les contacteurs moteur le supportent.

J’ai lu en diagonale, il semble qu’il y ait du code qui ne sert pas et dans cette fonction vous passez à LOW pour arrêter la pompe

Mais partout ailleurs dans le code, HIGH = OFF, LOW = ON (cf demarrerPompePV(), activerPompeGV(), arreterPompeGV() qui font tous HIGH pour éteindre).

Est-ce normal ? Si ce n’est pas le cas, suivant le montage que je n’ai pas regardé, vous risqueriez de faire tourner les 2 ensemble ce qui serait un souci.

Bonjour jef59,

Pays = Québec, Canada.
110 ou 120 volts, Circuit monophasé içi.
Malt = mise à la terre (fil cuivre nu) "ground en anglais", je crois que chez vous c'est "PE"

Le spa fonctionnait depuis plus de 20 ans, mais le petit panneau de contrôle (boitier 102mm x 102mm) ne fonctionne plus et le manufacturier a cessé de le produire et il n'y a pas d'alternative, voilà pourquoi je me suis tourné vers une solution Arduino.

Concernant le moteur; l'ampérage qu'il requière est 12.2 amp. à grande vitesse et 3.8amp. à petite vitesse tel qu'indiqué sur le chéma électrique. GV = contacteur 40 A pour AC7-b= 18A, PV = contacteur 32A pour AC-7b= 12A.

Bonjour J-M-L,

J'ai travaillé avec IA pour les fonctions Marche-Arrêt des 2 vitesses de la pompe. Comme je ne m'y connais pas en code, alors les yeux d'un spécialiste comme toi serait apprécié.

Salutations,

bonjour

pourquoi pas une simple logique de commande No - Nc ?

simple sécurité, Arduino doit tout de même gérer le timing de commande des relais.

Pour ma part, j'ai spécifié:

Avec cette solution parmis tant d'autre, vous ne pouvez pas avoir PV et GV en même temps quel que soit le code.

Dans une méthode plus "industrielle" + complète, au lieu d'avoir un relais interméduaire, il y aurait eut des contacteurs avec des contacts auxiliaires NO + NC ou des modules relais compatibles Arduino avec (si ça existe!!!) soit 2 contacts Repos/Travail soit 1 NO + 1 NC (et pas de relais intermédiaire).
Et éventuellement une protection rearmable du moteur.

Mais ce ne serait plus les mêmes composants.

les relais de pilotage PV et GV sont gourmands, il ne faut pas économiser sur leurs relais de commande...

module 2 relais en 5V Arduino avec pilotage, 20mA par relais, ça devrait aller avec Vcc arduino= 7,5V:

2 relais

Bonjour Pascal0171,

C'est ce type de relais 5v que j'utilise voir chéma électrique.

Est-ce que ton croquis fait référence à ce type de relais?

Merçi.

oui, le croquis en #15 utilise ce module, schématisé en bleu.

évidemment les sorties arduino ne commandent pas directement les relais, il y a les optos de pilotage.

Bonjour Pascal0171,

J'essai de comprendre ton croquis.
Donc sur mon chéma électrique: Coté Arduino; Cde PV = INI1-->broche2, Cde HV = INI2-->broche3, Com = ?, 5V = Vcc, Ground=Ground puis coté Contacteur NO1-->A1 PV, NO2-->A1 GV, COM1-->borne 3 de PV et COM2-->borne3 de GV. C'est ce que j'ai actuellement.

Que signifie le K PV et K HV sur ton croquis, Résistance?

Et tu ne semble pas avoir de relais intermédiaire!