HA-Gestion Complète d’une Piscine avec ESP32 et ESPHome

Intro

Dans un article précédent, je vous presentais un module de gestion de la filtration et traitement d’une piscine developpé sous APP DAEMON, j’ai depuis, pour simplifier la mise en oeuvre, migré vers ESP Home.

Je vous invite quand même à le parcourir, tout n’est pas à jeter.

Dans ce nouveau projet, j’ai développé une solution complète pour automatiser la gestion de ma piscine à l’aide d’un ESP32 programmé avec ESPHome. Ce système contrôle la filtration, la régulation du pH et du chlore, le niveau d’eau, le volet roulant, et même une protection hors gel, le tout intégré à Home Assistant. Voici les détails de cette réalisation, du matériel au code, pour ceux qui voudraient s’en inspirer !

Objectif du Projet

L’idée était de centraliser et d’automatiser toutes les fonctions essentielles de ma piscine (volume : 50 m³) :

  • Filtration : adapter la durée en fonction de la température de l’eau, avec plusieurs modes (Palier, Classique, Abaque, Horaire).
  • Régulation chimique : maintenir le pH et le chlore à des niveaux cibles via des pompes doseuses.
  • Niveau d’eau : gérer l’appoint automatiquement avec une électrovanne.
  • Volet roulant : ouverture/fermeture via relais.
  • Sécurité : surveiller la pression du filtre, le temps d’usage des galets de chlore, et activer un mode hors gel en hiver.
  • Monitoring : notifications Telegram et affichage local sur un écran LCD.

Le tout repose sur une carte ESP32 et un circuit imprimé maison pour simplifier les raccordements.

Matériel Utilisé

  • Carte ESP32 : J’ai choisi une carte avec 8 relais intégrés, disponible sur AliExpress. Elle offre assez de sorties pour piloter la pompe de filtration, les pompes doseuses, l’électrovanne, et le volet.
  • Circuit Imprimé Personnalisé : Conçu pour connecter facilement les capteurs (température, pH, pression) et les actionneurs via des borniers.
  • Capteurs :
    • Sonde Dallas DS18B20 (température eau).
    • Un module mesure de ph EZO
    • Une sonde pH EZO. J’utilise celle ci mais il existe des modèles compatibles industriels beaucoup plus chers et des modèles chinois beaucoup moins chers, c’est selon l’importance et le budget que vous accorderez à la fiabilité et la pérennité de la mesure.
    • Un module d’isolation galvanique EZO.
    • Sonde ORP (optionnelle, non encore calibrée).
    • Capteur de pression ADS1115 (filtre).
    • un module PZEM-004T 100A permettant de mesurer des courants de 0-100A sous une tension alternative de 80-260V.
    • Détecteurs de niveau (LSH, LSL via SX1509). Voir article sur la mesure de niveau
  • Actionneurs : Relais pour pompe filtration (12 m³/h), pompes doseuses (pH-, chlore), électrovanne eau, et volet roulant.
  • Afficheur LCD 16×2 I2C : Pour un suivi local (pH, pression, température).

Conception du Circuit Imprimé

Pour éviter un câblage anarchique, j’ai conçu un PCB qui regroupe toutes les connexions nécessaires. Voici une vue du PCB final, conçu avec EasyEda et fabriqué par JLCPCB :

Le PCB inclut :

  • Connecteurs pour capteurs : Borniers pour la sonde Dallas (température eau), les sondes EZO (pH et ORP), l’ADS1115 (pression), et les détecteurs de niveau (LSH, LSL).
  • Interfaces de communication : I2C pour l’afficheur et les EZO, UART pour le PZEM-004T.
  • Extension SX1509 : Pour gérer plus d’entrées/sorties (niveaux, LEDs).
  • Alimentation : Régulateurs 12V/5V/3.3V pour alimenter l’ESP32 et les capteurs.
  • Sorties vers les relais : Connecteurs pour piloter la pompe de filtration, les pompes doseuses, l’électrovanne, et le volet roulant.
  • Connecteurs supplémentaires : Pour des extensions futures (ex. : capteur ORP).

Le PCB est étiqueté clairement (ex. : « PZEM-004T », « ADS1115 », « PH-EZO ») pour faciliter les raccordements. Les borniers à vis permettent de brancher/débrancher facilement les capteurs et actionneurs sans soudure. La conception a été finalisée le 25/07/2024, comme indiqué sur le PCB.

Schéma de Câblage

Pour mieux comprendre les connexions, voici le schéma de câblage réalisé sous Easyeda :

Le schéma montre :

  • ESP32 : Cœur du système, connecté via GPIO aux capteurs et actionneurs.
  • Capteurs :
    • Sonde Dallas (GPIO16) pour la température de l’eau.
    • EZO pH/ORP via I2C (SDA GPIO15, SCL GPIO22).
    • ADS1115 (pression filtre) sur I2C.
    • PZEM-004T (puissance) via UART (RX GPIO3, TX GPIO1).
    • Détecteurs de niveau (LSH, LSL) via SX1509.
  • Actionneurs :
    • Relais pour la pompe de filtration (GPIO32), pompes doseuses (GPIO33, GPIO25), électrovanne (GPIO12), et volet roulant (GPIO27, GPIO14).
  • Extensions :
    • SX1509 pour gérer les entrées/sorties supplémentaires (niveaux, LEDs).
    • Afficheur LCD 16×2 sur I2C.
  • Alimentation : Régulateurs 12V/5V/3.3V pour l’ESP32 et les capteurs.

Ce schéma est essentiel pour comprendre comment tout est interconnecté et pour reproduire le projet.

Fonctionnement et Code ESPHome

Le programme ESPHome (ESP178) est structuré autour de plusieurs scripts et capteurs :

Filtration :

Quatre modes de fonctionnement:

Palier : Durée fixée par paliers (ex. : 4h <15°C, 12h >25°C).
Exemple : Si T<10°C T < 10°C T<10°C, durée = 1h ; si 15°C≤T<20°C 15°C \leq T < 20°C 15°C≤T<20°C, durée = 8h.

Classique : Durée = température / 2 (min 5h, max 23h).
Exemple : À 20°C, durée = 10h.

Abaque : Durée calculée par une formule polynomiale cubique :


Horaire : Plage horaire fixe (ex. : hiver).

Régulation pH :

  • Mesure via la sonde EZO pH, mémorisée après un temps de recirculation (tempo ajustable).
  • Calcul du temps d’injection de pH- selon la formule suivante :
    Quantité à injecter (q) = [(pH mesuré – pH consigne) / valeur effet produit] * (volume piscine / volume affecté) * montant produit.
    Avec :
    • pH mesuré (m) et consigne (c) définis dans Home Assistant.
    • Valeur effet produit (ve) = 0.2 (baisse de pH par litre).
    • Volume piscine (vb) = 50 m³, volume affecté (va) = 10 m³.
    • Montant produit (mp) = 0.2 L.
    • Temps d’injection (en secondes) = (q * 3600) / débit pompe, où le débit pompe est de 4.272 L/h (étalonné le 22/08/2024).
  • Exécution à 10h30 et 14h30, arrêt si le pH descend sous la consigne.

Régulation Chlore :

  • Injection basée sur une consigne (ppm) et un débit pompe (ajustable).
  • Activée uniquement si la pompe de filtration tourne.

Niveau d’Eau :

  • Détecteurs LSH/LSL pour niveaux haut, intermédiaire, bas, ou défaut.
  • Mode Auto : remplissage si niveau bas/inter, arrêt si haut ou défaut.

Hors Gel :

  • Activation si température extérieure < seuil 1 (-5°C) ou seuil 2 (-10°C), avec cycles de 15 ou 30 min.

Monitoring :

  • Rapport journalier Telegram à 23h59 (temps filtration, conso, etc.).
  • Alerte si pression filtre > 1.5 bar ou galets chlore épuisés.

Le code complet est inclus ci-dessus. Il utilise SNTP pour l’heure, I2C pour l’afficheur et EZO, et UART pour le PZEM.

Code ESPHOME

# Mode simulation
# Sensor simulé:
  # temperature eau
  # ph EZO
  # Seuil binary sensor "Marche ppe"
  # test si ph_EZO est valide
  # Cron time pour lancer script regule pH

substitutions:
  device_name: "esp178-esp32-piscine"
  friendly_name: esp178
  adress_ip: "192.168.0.178"
  time_timezone: "Europe/Paris"
  # Definition des seuils admissibles
  pu_fonctionnement: "200"
  pression_max: "1.5"


esphome:
  name: ${device_name}
  project:
    name: "rem81.esp178-esp32-piscine"
    version: "0.0.0"
  on_boot:
    priority: 600
    then:
    # Initialisation des templates
    - sensor.template.publish:
        id: _tps_injection_ph_moins
        state: 0.0
    - sensor.template.publish:
        id: _vol_injection_ph_moins
        state: 0.0

esp32:
  board: esp32dev

wifi:
  networks:
   - ssid: !secret wifi
     password: !secret mdpwifi
  reboot_timeout: 5min

#ethernet:
#  type: W5500
#  clk_pin: GPIO17
#  mosi_pin: GPIO19
#  miso_pin: GPIO18
#  cs_pin: GPIO21 
#  interrupt_pin: GPIO4
#  reset_pin: GPIO5
#  clock_speed: 15Mhz
  manual_ip:
    static_ip: ${adress_ip}
    gateway: 192.168.0.254
    subnet: 255.255.255.0
    dns1: 192.168.0.254

# Utilisez la LED de l'appareil comme LED d'état, qui clignotera s'il y a des avertissements (lent) ou des erreurs (rapide)
status_led:
  pin:
    number: GPIO23
    inverted: true

# Enable logging
logger:
  level: INFO
  baud_rate: 0
  
# Enable Home Assistant API
api:

ota:
  platform: esphome

web_server:
  port: 80
  version: 3 # sinon 2

time:
  - platform: sntp
    id: sntp_time
    timezone: Europe/Paris
    servers:
     - 0.pool.ntp.org
     - 1.pool.ntp.org
     - 2.pool.ntp.org

    on_time:
      # Tous les jours on execute le script de regul pH
      - seconds: 0
        minutes: 30
        hours: 10
        then:
          - script.execute: _regul_ph
      - seconds: 0
        minutes: 30
        hours: 14
        then:
          - script.execute: _regul_ph
      # reset le compteur de temps à minuit
      - seconds: 0
        minutes: 0
        hours: 0
        then:
          - sensor.duty_time.reset: _temps_fonctionnement_ppe_piscine_jour
          - sensor.duty_time.reset: _temps_fonctionnement_ppe_ph
          - sensor.duty_time.reset: _temps_fonctionnement_ppe_chlore     
          - sensor.duty_time.reset: _temps_fonctionnement_ev_eau
      # Notification du rapport journalier sur Telegram
      - seconds: 00
        minutes: 59
        hours: 23
        then:
          - lambda: |-
              static String mess;
              static int h,m,r,s=0;
              mess= "ESP178 Rapport Journalier";
              mess= mess+"\n";
              // Affichage du temps de fonctionnement Ppe Filtre
              s = (id(_temps_fonctionnement_ppe_piscine_jour).state);
              // Calcul des heures - minutes
              h = s / 3600;
              r = s - h * 3600;
              m = r / 60;
              r = r - m * 60;
              mess= mess+"Tps Filtration: "+String(h)+" h:"+String(m)+" mn:"+String(r)+" s""\n";
              mess= mess+ "Conso Ppe:"+ String(id(conso_elec_jour).state)+" kW"+"\n";
              // Affichage du temps de fonctionnement Ppe pH
              s = (id(_temps_fonctionnement_ppe_ph).state);
              // Calcul des heures - minutes
              h = s / 3600;
              r = s - h * 3600;
              m = r / 60;
              r = r - m * 60;          
              mess= mess+"Tps Ppe pH: "+String(h)+" h:"+String(m)+" mn:"+String(r)+" s""\n";
              // Affichage du temps de fonctionnement Ppe Chlore
              s = (id(_temps_fonctionnement_ppe_chlore).state);
              // Calcul des heures - minutes
              h = s / 3600;
              r = s - h * 3600;
              m = r / 60;
              r = r - m * 60;          
              mess= mess+"Tps Ppe Chlore: "+String(h)+" h:"+String(m)+" mn:"+String(r)+" s""\n";
              id(_message_telegram)->execute(mess.c_str());      

# Connection Bus i2c (Afficheur, EZO,...)
i2c:
  sda: 15
  scl: 22
  scan: false
  id: bus_a

# Configuration UART
uart:
  rx_pin: GPIO3 #rx
  tx_pin: GPIO1 #tx
  baud_rate: 9600

# modbus necessaire au PZEM  
modbus:

# Connection sonde(s) de température DS18b20
one_wire:
  - platform: gpio
    pin: GPIO16

# Extension E/S
sx1509:
  - id: sx1509_hub1
    address: 0x3E

ads1115:
  - address: 0x48

# déclaration des variables "globals"
globals:
    # température de fonctionnement en début de pompage avant prise en compte de la mesure de température
    # en secondes
    - id: g_memoire_temp_eau
      type: float
      restore_value: yes
      initial_value: '25'

    - id: g_memoire_ph
      type: float
      restore_value: yes
      initial_value: '7.2'

    - id: flag_tempo_ppe_filtre
      type: bool
      restore_value: no
      initial_value: 'false'

    # flag marche Hors gel activé
    - id: g_flag_hg
      type: bool
      restore_value: yes

    # temps d'injection ph
    - id: g_tps_injection_ph_moins
      type: float
      restore_value: no

    # temps d'injection chlore
    - id: g_tps_injection_chlore
      type: float
      restore_value: no

    # mémorise la durée de filtation dans les différents modes de fonctionnement
    - id: g_tps_filtration
      type: float
      restore_value: no

    # Limite haute du temps de filtration "en heure"
    - id: g_temps_max_filtration
      type: float
      initial_value: '23'
    # Limite basse du temps de filtration (en heure)
    - id: g_temps_min_filtration
      type: float
      initial_value: '5'
    # Paliers temperature/Temps filtration avec le mode "Palier"
    # Seuils Température en °C
    # la durée est calculée manuellement par rapport au volume de la piscine: Vol=9*4*1.4=50.4 m3
    # et le débit de la pompe: Qppe Théorique=12m3/h -> retenu:12m3/h
    # donc 1 cycle= 50.4/12=4.2h
    # il faut filtrer au minimum:
    # T°eau<10° = 0 cycle
    # 10°<T°eau<15° = 1 cycle = 4.2h => 4h
    # 15°<T°eau<20° = 2 cycles = 8.4h => 8h
    # 20°<T°eau<25° = 3 cycles = 12.6h => 12h
    # 25°<T°eau = 3 ou 4 cycles = 12.6h => 12h
    # 
    # si beaucoup de baigneurs de jour alors augmenter le coeff
    # Mode "Palier":
      # Si T°eau< Seuil Temp1 alors Durée = Tps paliers 1
      # Sinon
      # Si T°eau>= Seuil Temp1 et T°eau< Seuil Temp2 alors Durée = Tps paliers 2
      # Sinon
      # Si T°eau>= Seuil Temp2 et T°eau< Seuil Temp3 alors Durée = Tps paliers 3
      # Sinon
      # Si T°eau>= Seuil Temp3 et T°eau< Seuil Temp4 alors Durée = Tps paliers 4
      # Sinon
      # Durée = Tps paliers 5
    - id: g_temp_palier1
      type: float
      initial_value: '10'
    - id: g_temp_palier2
      type: float
      initial_value: '15'
    - id: g_temp_palier3
      type: float
      initial_value: '20'
    - id: g_temp_palier4
      type: float
      initial_value: '25' 
  # nb d'heures de filtration (en h)
    - id: g_tps_palier1
      type: float
      initial_value: '1'      
    - id: g_tps_palier2
      type: float
      initial_value: '4'
    - id: g_tps_palier3
      type: float
      initial_value: '8'
    - id: g_tps_palier4
      type: float
      initial_value: '12'
    - id: g_tps_palier5
      type: float
      initial_value: '14'

    # Constantes utilisées dans le mode "Abaque"  
    - id: g_abaque_a
      type: float
      initial_value: '0.00335'
    - id: g_abaque_b
      type: float
      initial_value: '-0.14953'
    - id: g_abaque_c
      type: float
      initial_value: '2.43489'
    - id: g_abaque_d
      type: float
      initial_value: '-10.72859'

    # Variables intermediaires utilisées dans le calcul "heure debut et fin"
    - id: g_hh
      type: int
    - id: g_mm
      type: int
    - id: g_ss
      type: int

# déclaration des modes de fonctionnement dans des "input select"
select:
  - platform: template
    name: "${friendly_name}_Mode_Fonctionnement_filtration"
    optimistic: true
    restore_value: true
    options:
      - Palier
      - Classique
      - Abaque
      - Horaire
      - Ma_f
      - At_f
    id: _Mode_Fonctionnement_filtration
    on_value: 
      then:
        - logger.log:
            format: "Mode Fonct Filtration --> %s"
            args: [ 'id(_Mode_Fonctionnement_filtration).state.c_str()' ]
            level: INFO
        - script.execute: _fonctionnement_filtration

  - platform: template
    name: "${friendly_name}_Mode_Fonctionnement_regul_ph"
    optimistic: true
    restore_value: true
    options:
      - Auto
      - Ma_f
      - At_f
    id: _Mode_Fonctionnement_regul_ph
    on_value: 
      then:
        - logger.log:
            format: "Mode Fonct Regul pH --> %s"
            args: [ 'id(_Mode_Fonctionnement_regul_ph).state.c_str()' ]
            level: INFO
        - if:
            condition:
              or:
                - lambda: 'return id(_Mode_Fonctionnement_regul_ph).state == "Ma_f";'
                - lambda: 'return id(_Mode_Fonctionnement_regul_ph).state == "At_f";'
            then:                    
            - script.stop: _regul_ph   
            - delay: 1s            
            - script.execute: _regul_ph       

  - platform: template
    name: "${friendly_name}_Mode_Fonct_regul_chlore"
    optimistic: true
    restore_value: true
    options:
      - Auto
      - Ma_f
      - At_f
    id: _Mode_Fonctionnement_regul_chlore
    on_value: 
      then:
        - logger.log:
            format: "Mode Fonct Regul pH --> %s"
            args: [ 'id(_Mode_Fonctionnement_regul_chlore).state.c_str()' ]
            level: INFO        
        - script.stop: _regul_chlore
        - delay: 1s            
        - script.execute: _regul_chlore

  - platform: template
    name: "${friendly_name}_Mode_Fonct_appoint_eau"
    optimistic: true
    restore_value: true
    options:
      - Auto
      - Ma_f
      - At_f
    id: _Mode_Fonctionnement_regul_eau
    on_value: 
      then:
        - logger.log:
            format: "Mode Fonct Regul Eau --> %s"
            args: [ 'id(_Mode_Fonctionnement_regul_eau).state.c_str()' ]
            level: INFO        
        - script.stop: _regul_eau
        - delay: 1s            
        - script.execute: _regul_eau  

  # défini l'activation du mode Hors Gel
  - platform: template
    name: "${friendly_name}_Mode_Hors_Gel"
    optimistic: true
    restore_value: true
    options:
      - Desactivé
      - Activé
    id: _Mode_Fonctionnement_hg

button:
  - platform: template
    name: "${friendly_name}_BP_Cycle_Regul_pH"
    on_press:
      - script.execute: _regul_ph

  - platform: template
    name: "${friendly_name}_BP_Cycle_Regul_chlore"
    on_press:
      - script.execute: _regul_chlore

  - platform: template
    name: "${friendly_name}_BP_RAZ_Tps_Galet_Chlore"
    on_press:
      - sensor.duty_time.reset: _temps_galet_chlore

  # Ce bouton stoppe la filtration pour la journée (cas de mauvais temps par ex)
  # Durée "Arret Jour" en heure on multiplie par 3600 pour avoir des secondes 
  # puis par 1000 pour des millisecondes: unité du delay lambda
  - platform: template
    name: "${friendly_name}_BP_arret_jour"
    id: _arret_jour
    on_press: 
      then:
      - switch.template.publish:
          id: ent_at_force
          state: ON
      - logger.log:
          format: "Début arret jour pour : %.0f h"
          args: [ 'id(duree_at_jour).state' ]
          level: INFO         
      - delay: !lambda "return id(duree_at_jour).state*3600*1000;"
      - switch.template.publish:
          id: ent_at_force
          state: OFF
      - logger.log:
          format: "Fin arret jour de : %.0f h"
          args: [ 'id(duree_at_jour).state' ]
          level: info 

binary_sensor:
  #Etat de la connection
  - platform: status
    name: "${friendly_name}_Status"

  # calcul des niveaux piscine
  # Si LSL ou LSH recouvert alors True sinon False
  - platform: template
    name: "${friendly_name}_niv_haut"
    id: niv_haut
      
  - platform: template
    name: "${friendly_name}_niv_inter"
    id: niv_inter

  - platform: template
    name: "${friendly_name}_niv_bas"
    id: niv_bas

  - platform: template
    name: "${friendly_name}_niv_defaut"
    id: niv_defaut

  # Etat galets Chlore
  # Si Tps > à temps max alors = True
  - platform: template
    name: "${friendly_name}_Etat_Galets_Chlore"
    id: _etat_galets_chlore
    on_press:
      - lambda: |-                
          static String mess;
          mess= "ESP178 Tps Utilisaton Galets Chlore Atteint";
          id(_message_telegram)->execute(mess.c_str());     
    on_release: 
      - lambda: |-                
          static String mess;
          mess= "ESP178 Tps Utilisaton Galets Chlore OK";
          id(_message_telegram)->execute(mess.c_str());    


  # Etat pression filtre
  # Si pression superieure à p_Max alors = True
  - platform: analog_threshold
    name: "${friendly_name}_Etat_pression_Filtre"
    sensor_id: pression_filtre    
    id: _etat_pression_filtre
    threshold: ${pression_max} # Défini dans Substitution en bar
    on_press:
      - lambda: |-                
          static String mess;
          mess= "ESP178 Seuil Pression Filtre Atteint";
          id(_message_telegram)->execute(mess.c_str());     
    on_release: 
      - lambda: |-                
          static String mess;
          mess= "ESP178 Pression filtre OK";
          id(_message_telegram)->execute(mess.c_str());    


  # Pompe en fonctionnement
  # Remplacer le seuil (threshold) par du négatif pour simuler
  - platform: analog_threshold
    name: "${friendly_name}_ppe_en_fonctionnement"
    id: ppe_filt_en_fonctionnement
    sensor_id: puissance
    threshold: ${pu_fonctionnement} # Défini dans Substitution en Watt
    on_press:
      - lambda: |-                
          static String mess;
          mess= "ESP178 Debut Filtration";
          id(_message_telegram)->execute(mess.c_str());           

    on_release: 
      - lambda: |-                
          static String mess;
          mess= "ESP178 Fin Filtration";
          id(_message_telegram)->execute(mess.c_str());    

  # Entrée logique permettant de lire le BP I00 de la carte
  - platform: gpio
    pin:
      number: GPIO00
      inverted: True
      mode:
        input: true
        pullup: true
    name: "${friendly_name}_bp1"

  # GPIO sur module extension SX1509

  - platform: gpio
    name: "${friendly_name}_volet_ferme"
    id: volet_ferme
    pin:
      sx1509: sx1509_hub1
      # Use pin number 0 on the SX1509
      number: 0
      mode:
        input: true
        pullup: false
      inverted: false
    filters:
      - delayed_on_off: 500ms

  # Niveau haut à 1 si decouvert
  - platform: gpio
    name: "${friendly_name}_tp_plein_lsh"
    id: lsh
    pin:
      sx1509: sx1509_hub1
      number: 1
      mode:
        input: true
        pullup: false
      inverted: true
    filters:
      - delayed_on_off: 5s
    on_press:
      then:
        - script.stop: _regul_eau
        - switch.turn_off: cde_ev_eau

  # Niveau bas à 1 si decouvert
  - platform: gpio
    name: "${friendly_name}_tp_plein_lsl"
    id: lsl
    pin:
      sx1509: sx1509_hub1
      number: 2
      mode:
        input: true
        pullup: false
      inverted: true
    filters:
      - delayed_on_off: 5s

  - platform: gpio
    name: "${friendly_name}_E4"
    pin:
      sx1509: sx1509_hub1
      number: 3
      mode:
        input: true
        pullup: false
      inverted: false   
  - platform: gpio
    name: "${friendly_name}_E5"
    pin:
      sx1509: sx1509_hub1
      number: 4
      mode:
        input: true
        pullup: false
      inverted: false   
  - platform: gpio
    name: "${friendly_name}_E6"
    pin:
      sx1509: sx1509_hub1
      number: 5
      mode:
        input: true
        pullup: false
      inverted: false   

# Définiton des "Time"
datetime:
  - platform: template
    id: heure_pivot
    type: time
    name: "${friendly_name}_heure_pivot"
    optimistic: yes
    initial_value: "13:30:00"
    restore_value: true

  - platform: template
    id: h_debut
    type: time
    name: "${friendly_name}_h_debut"
    optimistic: yes
    initial_value: "00:00:00"
    restore_value: false
    
  - platform: template
    id: h_fin
    type: time
    name: "${friendly_name}_h_fin"
    optimistic: yes
    initial_value: "00:00:00"
    restore_value: false

  - platform: template
    id: duree_filtration
    type: time
    name: "${friendly_name}_duree_filtration"
    optimistic: yes
    initial_value: "00:00:00"
    restore_value: false

  - platform: template
    id: debut_mode_horaire
    type: time
    name: "${friendly_name}_debut_mode_horaire"
    optimistic: yes
    restore_value: true

  - platform: template
    id: duree_mode_horaire
    type: time
    name: "${friendly_name}_duree_mode_horaire"
    optimistic: yes
    restore_value: true

  - platform: template
    id: duree_injection_ph
    type: time
    name: "${friendly_name}_duree_injection_pH"
    optimistic: yes
    restore_value: true

# Input Number
number:
  # Simulation Temp eau et Mesure pH
  - platform: template
    name: "${friendly_name}_simule_Temp"
    id: simul_temp_eau
    optimistic: true
    restore_value: true
    mode: box
    min_value: -10
    max_value: 50
    device_class: temperature
    step: 0.01 
    on_value: 
      then:
        - lambda: |-
            id(g_memoire_temp_eau)=id(temp_eau).state;     

  # Simulation Niveau pH
  - platform: template
    name: "${friendly_name}_simule_pH"
    id: simul_ph_ezo
    optimistic: true
    restore_value: true
    mode: box
    min_value: 0
    max_value: 10
    unit_of_measurement: "pH"
    step: 0.01

  # Temps de recirculation avant prise en compte mesure de température
  - platform: template
    name: "${friendly_name}_tempo_recirculation"
    id: tempo_mesure_temp
    optimistic: true
    restore_value: true
    mode: box
    min_value: 0
    max_value: 900
    unit_of_measurement: "s"
    step: 1
    icon: mdi:clock

  # Coefficient de filtration
  - platform: template
    name: "${friendly_name}_coeff_Filtration"
    id: coeff_filtration
    optimistic: true
    restore_value: true
    mode: box
    min_value: 50
    max_value: 150
    unit_of_measurement: "%"
    step: 1
    icon: mdi:percent

  # Cible Niveau pH
  - platform: template
    name: "${friendly_name}_pH_Cible"
    id: _ph_cible
    optimistic: true
    restore_value: true
    mode: box
    min_value: 6
    max_value: 7.6
    unit_of_measurement: "pH"
    step: 0.01

  # Debit Pompe pH moins
  # Etalonnage du 22/08/2024: 4.272 l/h
  - platform: template
    name: "${friendly_name}_debit_ppe_ph_moins"
    id: _debit_ppe_moins
    optimistic: true
    restore_value: true
    mode: box
    min_value: 0.5
    max_value: 7.2
    unit_of_measurement: "l/h"
    step: 0.001

  # Coefficient regulation
  - platform: template
    name: "${friendly_name}_coef_regul_ph_moins"
    id: _coef_regul_ph_moins
    optimistic: true
    restore_value: true
    mode: box
    min_value: 0
    max_value: 150
    unit_of_measurement: "%"
    step: 0.01
    icon: mdi:percent

  # Cible Niveau Chlore
  - platform: template
    name: "${friendly_name}_chlore_Cible"
    id: _chlore_cible
    optimistic: true
    restore_value: true
    mode: box
    min_value: 0
    max_value: 10
    unit_of_measurement: "ppm"
    step: 0.01

  # Debit Pompe chlore
  # Etalonnage du 
  - platform: template
    name: "${friendly_name}_debit_ppe_chlore"
    id: _debit_ppe_chlore
    optimistic: true
    restore_value: true
    mode: box
    min_value: 0.5
    max_value: 7.2
    unit_of_measurement: "l/h"
    step: 0.001

  # Tps Max Galet Chlore
  - platform: template
    name: "${friendly_name}_tps_Max_galet_Chlore"
    id: _tps_max_galet_chlore
    optimistic: true
    restore_value: true
    mode: box
    min_value: 0
    max_value: 100
    unit_of_measurement: "h"
    step: 1

  # Seuil 1 Temp Hors gel
  - platform: template
    name: "${friendly_name}_Seuil1_Temp_HG"
    id: s1_temp_hg
    optimistic: true
    restore_value: true
    mode: box
    min_value: -5
    max_value: 0
    device_class: temperature
    step: 0.1

  # Seuil 2 Temp Hors gel
  - platform: template
    name: "${friendly_name}_Seuil2_Temp_HG"
    id: s2_temp_hg
    optimistic: true
    restore_value: true
    mode: box
    min_value: -10
    max_value: 0
    device_class: temperature
    step: 0.1
  
  # Durée de l'arret sur la journée en lien avec "arret_jour"
  - platform: template
    name: "${friendly_name}_Durée-Arret_jour"
    id: duree_at_jour
    optimistic: true
    restore_value: true
    mode: box
    min_value: 0
    max_value: 24
    unit_of_measurement: "h"
    step: 0.01

sensor:
  - platform: homeassistant
    name: "${friendly_name}_temperature_exterieure"
    entity_id: "sensor.vp2_temp_out"
    id: temp_ext
  #    entity_id: "input_number.simule_temp_exterieur" sert à la simulation

  - platform: dallas_temp
    #address: 0x060321117ae89b28
    name: "${friendly_name}_temperature_eau"
    id: temp_eau
    device_class: temperature
    state_class: "measurement"     
    filters:
      - filter_out: 0.0
        # Etalonné le 26 Aout 2024  avec une PT100       
      #- calibrate_linear:
      #  - 0 -> 0
      #  - 26.6 -> 27.7
    
  # Mesure du pH
  # Procédure étalonnage:
    # Mettre 1 s dans "update interval"
    # ainsi on moyenne sur 15 s avec un affichage toutes les 5s 
    # 1-Faire une mesure avec solution étalon de 4.0
    # 2-Attendre 2 à 3 minutes que ca se stabilise
    # 3-relever la valeur du pH
    # 4-Toujours bien rincer la sonde à l'eau déminéralisée entre deux solutions
    # Refaire étapes 1 à 3 avec une solution étalon de 6.86
    # Puis avec une solution étalon de 9.18
    # Saisir les valeurs relevés dans "Calibrate_linear"
    # Remettre 60 s dans "update interval"
    # Compiler
  # Fin procédure étalonnage
  - platform: ezo
    id: ph_ezo
    name: "${friendly_name}_ph_ezo"
    address: 99
    unit_of_measurement: "pH"
    accuracy_decimals: 2
    update_interval: 60s
    device_class: ph
    state_class: "measurement"     
  #  moyenne sur 15 mn-affichage toutes les 5mn
    filters:
      - sliding_window_moving_average:
          window_size: 15
          send_every: 5
          send_first_at: 1
  # Etalonné le 15 juin 2024          
      - calibrate_linear:
        - 4.665 -> 4.0
        - 7.482 -> 6.86
        - 9.505 -> 9.18          
  # Etalonné le 28 juin 2023          
  #    - calibrate_linear:
  #      - 4.547 -> 4.0
  #      - 7.282 -> 6.86
  #      - 9.447 -> 9.18
  # Etalonné le 6 juillet 2022          
  #    - calibrate_linear:
  #      - 4.44 -> 4.0
  #      - 7.17 -> 6.86
  #      - 9.41 -> 9.18

  # Statistiques pH
  - platform: combination
    type: kalman
    name: "${friendly_name}_ph_stat_standard_deviation"
    process_std_dev: 0.001
    sources:
      - source: ph_ezo
        error: 1.0
    device_class: ph
    state_class: "measurement"         

  - platform: combination
    type: median
    name: "${friendly_name}_ph_stat_median"
    device_class: ph
    state_class: "measurement"        
    sources:
      - source: ph_ezo

  # Mesure de l'ORP
  # Procédure étalonnage:
    # Mettre 1 s dans "update interval"
    # ainsi on moyenne sur 15 s avec un affichage toutes les 5s 

  - platform: ezo
    id: orp_ezo
    name: "${friendly_name}_orp_ezo"
    address: 98
    unit_of_measurement: "mV"
    accuracy_decimals: 2
    update_interval: 60s
  #  moyenne sur 15 mn-affichage toutes les 5mn
  #  filters:
  #    - sliding_window_moving_average:
  #        window_size: 15
  #        send_every: 5
  #        send_first_at: 1
  # Etalonné le 15 juin 2024          
  #    - calibrate_linear:
  #      - 4.665 -> 4.0
  #      - 7.482 -> 6.86
  #      - 9.505 -> 9.18    

  # Mesure de la pression filtre Entrée ANA 3
  - platform: ads1115
    multiplexer: 'A3_GND'
    gain: 6.144
    name: "${friendly_name}_Pression_filtre"          
    update_interval: 10s
    unit_of_measurement: "bar"
    device_class: pressure
    state_class: "measurement"        
    id: pression_filtre
    filters:
      - calibrate_linear:
        - 0.576 -> 0.0
        - 0.825 -> 0.825
  # moyenne sur 30 mn + affichage toutes les 2 mn
      - sliding_window_moving_average:
          window_size: 30
          send_every: 2
          send_first_at: 1

  # mémorise le temps d'injection calculé
  - platform: template
    name: "${friendly_name}_tps_injection_ph_moins"
    id: _tps_injection_ph_moins
    unit_of_measurement: "s"
    state_class: "measurement" 

  # Affiche le volume de pH moins à injecter
  - platform: template
    name: "${friendly_name}_vol_injection_ph_moins"
    id: _vol_injection_ph_moins
    unit_of_measurement: "l"
    state_class: "measurement" 

  # mémorise le temps d'injection calculé
  - platform: template
    name: "${friendly_name}_tps_injection_chlore"
    id: _tps_injection_chlore
    unit_of_measurement: "s"
    state_class: "measurement" 
   
  # mesure Puissance avec un PZEM-004T-100A
  - platform: pzemac
    update_interval: 30s
    current:
      name: "pzem_pisc_courant"
      unit_of_measurement: "A"
    voltage:
      name: "pzem_pisc_tension"
      unit_of_measurement: "V"
    energy:
      name: "pzem_pisc_energy"
      unit_of_measurement: "kWh"
      filters:
        - multiply: 0.001
    power:
      name: "pzem_pisc_puissance"
      unit_of_measurement: "W"
      id: puissance

  # Energie consommée par jour
  - platform: total_daily_energy
    name: '${friendly_name}_energie_jour'
    power_id: puissance
    unit_of_measurement: 'kWh'
    state_class: total_increasing
    device_class: energy
    accuracy_decimals: 3
    id: conso_elec_jour
    filters:
      # Multiplication factor from W to kW is 0.001
      - multiply: 0.001

  # Calcul du temps de fonctionnement
  # Pompe piscine
  - platform: duty_time
    id: _temps_fonctionnement_ppe_piscine_jour
    name: '${friendly_name}_temps_ma_ppe_piscine_jour'
    sensor: ppe_filt_en_fonctionnement
    restore: true
    filters: 
      - round: 2

  - platform: duty_time
    id: _temps_galet_chlore
    name: '${friendly_name}_temps_galet_chlore'
    sensor: ppe_filt_en_fonctionnement
    restore: true
    filters: 
      - round: 2
    
  # Ppe pH
  - platform: duty_time
    id: _temps_fonctionnement_ppe_ph
    name: '${friendly_name}_temps_ma_ppe_ph'
    lambda: "return id(cde_ppe_ph_moins).state == true;"
    restore: true
    filters: 
      - round: 2

  # Ppe Chlore
  - platform: duty_time
    id: _temps_fonctionnement_ppe_chlore
    name: '${friendly_name}_temps_ma_ppe_chlore'
    lambda: "return id(cde_ppe_chlore).state == true;"
    restore: true
    filters: 
      - round: 2

  # EV Eau
  - platform: duty_time
    id: _temps_fonctionnement_ev_eau
    name: '${friendly_name}_temps_ma_ev_eau'
    lambda: "return id(cde_ev_eau).state == true;"
    restore: true
    filters: 
      - round: 2


# Déclaration des "Covers" 
# Volet piscine dans mon cas
cover:
  - platform: template
    name: "${friendly_name}_volet_piscine"
    lambda: |-
      if (id(volet_ferme).state) {
        return COVER_CLOSED;
      } else {
        return COVER_OPEN;
      }
    open_action:
      - script.execute: script_ouv_volet
    close_action:
      - script.execute: script_ferm_volet
    stop_action:
      - script.execute: script_stop_volet
    optimistic: true


# déclaration des "text_sensors"
text_sensor:
  # Affichage des heures de filtration dans Home Assistant
  - platform: template
    id: aff_heure_filtration
    name: "${friendly_name} affich_heure_filtration"
    icon: mdi:timer

  # Message en lien avec L'automatisme de notification telegram
  - platform: template
    name: "${friendly_name}_message_notif_telegram"
    lambda: |-
      return {"ESP178 RAZ Telegram"};
    update_interval: never
    id: _msg_notif_telegram

# Déclaration des switches: cde des relais
switch:
  - platform: gpio
    name: "${friendly_name} cde_pompe_filtration"
    pin: GPIO32
    id: cde_ppe_filtration
    on_turn_on:
      then:
        - switch.turn_on: led14        
        - delay: !lambda "return id(tempo_mesure_temp).state*1000;" # Durée de fonctionnement de la pompe avant prise en compte de la température eau
        - logger.log:
            format: "Set tempo cde ppe"
            level: DEBUG
        - lambda: |-
            id(flag_tempo_ppe_filtre) = true;

    on_turn_off: 
      then:
        - logger.log:
            format: "Reset tempo cde ppe"
            level: DEBUG
        - lambda: |-
            id(flag_tempo_ppe_filtre) = false;
        - script.stop: _regul_ph
        - switch.turn_off: cde_ppe_ph_moins
        - switch.turn_off: led14        

  - platform: gpio
    name: "${friendly_name} cde_ppe_ph_moins"
    pin: GPIO33
    id: cde_ppe_ph_moins

  - platform: gpio
    name: "${friendly_name} cde_ppe_chlore"
    pin: GPIO25
    id: cde_ppe_chlore

  - platform: gpio
    name: "${friendly_name} cde_eclairage"
    pin: GPIO26
    id: cde_eclairage

  - platform: gpio
    name: "${friendly_name} cde_volet_ouverture"
    pin: GPIO27
    id: cde_volet_ouverture
    interlock: [cde_volet_fermeture]

  - platform: gpio
    name: "${friendly_name} cde_volet_fermeture"
    pin: GPIO14
    id: cde_volet_fermeture
    interlock: [cde_volet_ouverture]

  - platform: gpio
    name: "${friendly_name} cde_ev_eau"
    pin: GPIO12
    id: cde_ev_eau

  - platform: gpio
    name: "${friendly_name} relais8"
    pin: GPIO13
    id: relais8

  - platform: gpio
    name: "${friendly_name} led14"
    id: led14
    pin:
      sx1509: sx1509_hub1
      number: 14
      mode:
        output: true
      inverted: false

  - platform: gpio
    name: "${friendly_name} led15"
    id: led15
    pin:
      sx1509: sx1509_hub1
      number: 15
      mode:
        output: true
      inverted: false

  - platform: restart
    name: "${friendly_name} Restart"

  # Switch Forçage Arret pompe filtration en mode Auto
  #
  - platform: template
    name: "${friendly_name}_ent_arret_forcé"
    id: ent_at_force
    optimistic: True
    lambda: |-
      if (id(ent_at_force).state) {
        return true;
      } else {
        return false;
      }
    on_turn_on: 
      then:
        - script.execute: _fonctionnement_filtration        
        - switch.turn_off: cde_ppe_filtration
        - logger.log: 
            format: "entrée_arret_forcé_ppe_filtration"
            level: INFO
        # RAZ de la durée de filtration
        - datetime.time.set:
            id: duree_filtration
            time: !lambda |-
              return {second: 0, minute: 0, hour: 0};                
        - lambda: |-
            static String mess;
            mess= "Ent At Forcé";
            id(aff_heure_filtration).publish_state(mess.c_str());
        # Message Telegram
        - lambda: |-                
            static String mess;
            mess= "ESP178 Debut Arret Forcé Filtration";
            id(_message_telegram)->execute(mess.c_str());                 

    on_turn_off: 
      then:
        # Message Telegram
        - lambda: |-                
            static String mess;
            mess= "ESP178 Fin arret Forcé Filtration";
            id(_message_telegram)->execute(mess.c_str());              
        - script.execute: _fonctionnement_filtration

# Gestion de l'afficheur 
display:
  - platform: lcd_pcf8574
    dimensions: 16x2
    address: 0x27
    update_interval: 5s
    lambda: |-
      it.printf(0,0,"Ph=%.2f",id(ph_ezo).state);
      it.printf(8,0,"P=%.3f",id(pression_filtre).state);
      it.printf(8,1,"T=%.1f",id(g_memoire_temp_eau));
#it.printf(15,1,"T=%.1s",id(mode_f).state);

# Déclenchement des scripts à intervalles réguliers
interval:
  #- interval: 10s
  #  then:
  #    - script.execute: _regul_ph
  #- interval: 10s
  #  then:
  #    - script.execute: _regul_chlore
  - interval: 5s
    then:      
      - script.execute: _fonctionnement_filtration
      - script.execute: _calcul_niveau_eau
      - script.execute: _regul_eau
  - interval: 5s
    then: 
      - script.execute: _memorisation_temperature_eau
      - script.execute: _memorisation_ph
      - script.execute: _securisation_regul_ph

  - interval: 900s # Test HG toutes les 15 mn (900s)
    then: 
      - script.execute: _fonction_hors_gel

  - interval: 1h # Test temps usage Galets Chlore
    then: 
      - script.execute: _fonction_galet_chlore


  #- interval: 20s # Test
  #  then: 
  #    - script.execute: _test

# Déclaration des "Scripts"
script:
  # Script utilisé pour tester", peut etre supprimer si inutilse
  - id: _test
    then:
      - lambda: |-
          static String mess;
          static int h,m,r,s=0;
          mess= "ESP178 Rapport Journalier";
          mess= mess+"\n";
          // Affichage du temps de fonctionnement Ppe Filtre
          s = (id(_temps_fonctionnement_ppe_piscine_jour).state);
          // Calcul des heures - minutes
          h = s / 3600;
          r = s - h * 3600;
          m = r / 60;
          r = r - m * 60;
          mess= mess+"Tps Filtration: "+String(h)+" h:"+String(m)+" mn:"+String(r)+" s""\n";
          mess= mess+ "Conso Ppe:"+ String(id(conso_elec_jour).state)+" kW"+"\n";
          // Affichage du temps de fonctionnement Ppe pH
          s = (id(_temps_fonctionnement_ppe_ph).state);
          // Calcul des heures - minutes
          h = s / 3600;
          r = s - h * 3600;
          m = r / 60;
          r = r - m * 60;          
          mess= mess+"Tps Ppe pH: "+String(h)+" h:"+String(m)+" mn:"+String(r)+" s""\n";
          // Affichage du temps de fonctionnement Ppe Chlore
          s = (id(_temps_fonctionnement_ppe_chlore).state);
          // Calcul des heures - minutes
          h = s / 3600;
          r = s - h * 3600;
          m = r / 60;
          r = r - m * 60;          
          mess= mess+"Tps Ppe Chlore: "+String(h)+" h:"+String(m)+" mn:"+String(r)+" s""\n";
          id(_message_telegram)->execute(mess.c_str()); 

  # Si la pompe tourne depuis au moins "tempo_recirculation" on raffraichit la memoire de la temperature eau qui est
  # prise en compte dans les scripts.
  # sinon on travaille avec la témpérature mémorisée avant l'arret précédent
  - id: _memorisation_temperature_eau
    then:
      - if:
          condition:
            lambda: 'return id(flag_tempo_ppe_filtre) == true;'
          then:
            - lambda: |-
                id(g_memoire_temp_eau)=id(temp_eau).state;            
          else:
            - lambda: |-
                id(g_memoire_temp_eau)=id(g_memoire_temp_eau);
      - logger.log:
          format: "Flag Temp: %i / Temp Dallas: %.2f / Mem Temp: %.2f"
          args: [ 'id(flag_tempo_ppe_filtre)','id(temp_eau).state','id(g_memoire_temp_eau)' ]
          level: DEBUG

  # Si la pompe tourne depuis au moins "tempo_recirculation" on raffraichit la memoire du Ph qui est
  # prise en compte dans les scripts.
  # sinon on travaille avec le pH mémorisé avant l'arret précédent
  - id: _memorisation_ph
    then:
      - if:
          condition:
            lambda: 'return id(flag_tempo_ppe_filtre) == true;'
          then:
            - lambda: |-
                id(g_memoire_ph)=id(ph_ezo).state;            
          else:
            - lambda: |-
                id(g_memoire_ph)=id(g_memoire_ph);
      - if:
          condition:
            lambda: 'return isnan(id(ph_ezo).state);' # verifie si ph_ezo est valide
          then:
            - logger.log:
                format: "ph_EZO invalide: %.2f"
                args: [ 'id(ph_ezo).state']
                level: INFO            
            - lambda: |-            
                id(g_memoire_ph)=id(g_memoire_ph);
          else: 
            - logger.log:
                format: "ph_EZO Valide: %.2f"
                args: [ 'id(ph_ezo).state']
                level: INFO   

      - logger.log:
          format: "Flag Temp: %i / Ph EZO: %.2f / Mem Ph: %.2f"
          args: [ 'id(flag_tempo_ppe_filtre)','id(ph_ezo).state','id(g_memoire_ph)' ]
          level: INFO

  # Calcul de la durée de la filtration en fonction du mode de fonctionnement selectionné
  - id: _fonctionnement_filtration
    then:
      - logger.log:
          format: "Switch at force 2: %i "
          args: [ 'id(ent_at_force).state' ]
          level: DEBUG

      # Entrée Arret Forcé par Binary_sensor "Arret force"
      - if:
          condition:
            - lambda: 'return id(ent_at_force).state == true;'
          then:
            - switch.turn_off: cde_ppe_filtration

      # Entrée Marche Forcée HG
      - if:
          condition:
            - lambda: 'return id(g_flag_hg) == true;'
          then:
            - switch.turn_on: cde_ppe_filtration
            - logger.log: 
                format: "Marche HG Ppe filtration"
                level: INFO
            - logger.log:
                format: "Flag HG: %i / Temp Ext: %.2f / S1: %.2f / S2: %.2f"
                args: [ 'id(g_flag_hg)','id(temp_ext).state','id(s1_temp_hg).state','id(s2_temp_hg).state' ]
                level: DEBUG                
            - lambda: |-
                static String mess;
                mess= "Ma HG";
                id(aff_heure_filtration).publish_state(mess.c_str());

            # RAZ de la durée de filtration
            - datetime.time.set:
                id: duree_filtration
                time: !lambda |-
                  return {second: 0, minute: 0, hour: 0};

      # Mode Arret forcé Input Select
      - if:
          condition:
              - lambda: 'return id(_Mode_Fonctionnement_filtration).state == "At_f";'
              - lambda: 'return id(ent_at_force).state == false;'
              - lambda: 'return id(g_flag_hg) == false;'
          then:
            - switch.turn_off: cde_ppe_filtration
            - logger.log: 
                format: "arret_forcé_ppe_filtration"
                level: INFO
            - lambda: |-
                static String mess;
                mess="At_force";
                id(aff_heure_filtration).publish_state(mess.c_str());

      # Mode Marche forcée Input Select
      - if:
          condition:
            - lambda: 'return id(_Mode_Fonctionnement_filtration).state == "Ma_f";'
            - lambda: 'return id(ent_at_force).state == false;'
            - lambda: 'return id(g_flag_hg) == false;'
          then:
            - switch.turn_on: cde_ppe_filtration
            - logger.log: 
                format: "Marche_forcée_ppe_filtration"
                level: INFO
            - lambda: |-
                static String mess;
                mess="Ma_force";
                id(aff_heure_filtration).publish_state(mess.c_str());                

      # Mode "Palier":
      # Si T°eau< Seuil Temp1 alors Durée = Tps paliers 1
      # Sinon
      # Si T°eau>= Seuil Temp1 et T°eau< Seuil Temp2 alors Durée = Tps paliers 2
      # Sinon
      # Si T°eau>= Seuil Temp2 et T°eau< Seuil Temp3 alors Durée = Tps paliers 3
      # Sinon
      # Si T°eau>= Seuil Temp3 et T°eau< Seuil Temp4 alors Durée = Tps paliers 4
      # Sinon
      # Durée = Tps paliers 5
      - if:
          condition:
            - lambda: 'return id(_Mode_Fonctionnement_filtration).state == "Palier";'
            - lambda: 'return id(ent_at_force).state == false;'
            - lambda: 'return id(g_flag_hg) == false;'
          then:
            - lambda: |-
                if (id(g_memoire_temp_eau)<id(g_temp_palier1)){
                  id(g_tps_filtration)=id(g_tps_palier1);
                } else {
                  if (id(g_memoire_temp_eau)>=id(g_temp_palier1) && (id(g_memoire_temp_eau)<id(g_temp_palier2))){
                    id(g_tps_filtration)=id(g_tps_palier2);
                  } else {
                    if (id(g_memoire_temp_eau)>=id(g_temp_palier2) && (id(g_memoire_temp_eau)<id(g_temp_palier3))){
                    id(g_tps_filtration)=id(g_tps_palier3);
                    } else {
                      if (id(g_memoire_temp_eau)>=id(g_temp_palier3) && (id(g_memoire_temp_eau)<id(g_temp_palier4))){
                      id(g_tps_filtration)=id(g_tps_palier4);
                      } else {
                        id(g_tps_filtration)=id(g_tps_palier5);
                      }
                    }
                  }
                }
                
                id(g_tps_filtration)=id(g_tps_filtration)*id(coeff_filtration).state/100;

            - logger.log:
                format: "Mode: Palier / Valeur Mem Temp: %.2f / Tps Filtrat: %2f"
                args: [ 'id(g_memoire_temp_eau)','id(g_tps_filtration)' ]
                level: DEBUG
            - script.execute: _calcul_hdebut_hfin
      
      # Mode "Classique":
      # La durée de filtration en h est égale à la température de l'eau divisée par 2
      - if:
          condition:
            - lambda: 'return id(_Mode_Fonctionnement_filtration).state == "Classique";'
            - lambda: 'return id(ent_at_force).state == false;'     
            - lambda: 'return id(g_flag_hg) == false;'       
          then:
            - lambda: |-
                id(g_tps_filtration)=id(g_memoire_temp_eau)/2;
                id(g_tps_filtration)=min(id(g_temps_max_filtration),id(g_tps_filtration));
                id(g_tps_filtration)=max(id(g_temps_min_filtration),id(g_tps_filtration));
                id(g_tps_filtration)=id(g_tps_filtration)*id(coeff_filtration).state/100;
            - logger.log:
                format: "Mode Classique / Valeur Mem Temp: %.2f / Tps Filtrat: %2f"
                args: [ 'id(g_memoire_temp_eau)','id(g_tps_filtration)' ]
                level: DEBUG
            - script.execute: _calcul_hdebut_hfin
      
      # Mode "Abacus"
      #
      - if:
          condition:
            - lambda: 'return id(_Mode_Fonctionnement_filtration).state == "Abaque";'
            - lambda: 'return id(ent_at_force).state == false;'    
            - lambda: 'return id(g_flag_hg) == false;'        
          then:
            - lambda: |-
                  id(g_tps_filtration)=id(g_abaque_a)*pow(id(g_memoire_temp_eau),3)+id(g_abaque_b)*pow(id(g_memoire_temp_eau),2)+id(g_abaque_c)*id(g_memoire_temp_eau)+id(g_abaque_d);
                  id(g_tps_filtration)=id(g_tps_filtration)*id(coeff_filtration).state/100;
                  id(g_tps_filtration)=min(id(g_temps_max_filtration),id(g_tps_filtration));
                  id(g_tps_filtration)=max(id(g_temps_min_filtration),id(g_tps_filtration));
            - logger.log:
                format: "Mode Abaque / Valeur Mem Temp: %.2f / Tps Filtrat: %2f"
                args: [ 'id(g_memoire_temp_eau)','id(g_tps_filtration)' ]
                level: DEBUG
            - script.execute: _calcul_hdebut_hfin

      # Mode "Horaire"
      # Débuté à l'heure programmée pour une durée programmée
      # je m'en sers surtout l'hiver
      - if:
          condition:
            - lambda: 'return id(_Mode_Fonctionnement_filtration).state == "Horaire";'
            - lambda: 'return id(ent_at_force).state == false;'
            - lambda: 'return id(g_flag_hg) == false;'
          then:
            - logger.log:
                format: "Mode Abaque / Valeur Mem Temp: %.2f"
                args: [ 'id(g_memoire_temp_eau)' ]
                level: DEBUG

            - datetime.time.set:
                id: h_debut
                time: !lambda |-
                  return {second: 0, minute: id(debut_mode_horaire).minute, hour: id(debut_mode_horaire).hour};

            - datetime.time.set:
                id: h_fin
                time: !lambda |-
                  return {second: 0, minute: static_cast<uint8_t>(id(h_debut).minute+id(duree_mode_horaire).minute), hour: static_cast<uint8_t>(id(h_debut).hour+id(duree_mode_horaire).hour)};
                  // return {second: 0, minute: id(h_debut).minute+id(duree_mode_horaire).minute, hour: id(h_debut).hour+id(duree_mode_horaire).hour};
            - lambda: |-
                static String mess;
                mess= String(id(h_debut).hour)+":"+String(id(h_debut).minute)+"/"+String(id(h_fin).hour)+":"+String(id(h_fin).minute);
                id(aff_heure_filtration).publish_state(mess.c_str());
                
            - script.execute: _calcul_ma_at_ppe_filtration

# Calcul l'heure de debut et fin de filtration en fonction la durée de filtration et de l'heure pivot
# La variable: g_tps_filtration contient la durée en heure
  - id: _calcul_hdebut_hfin
    mode: single
    then:
      #RAZ des secondes de l'heure pivot
      - datetime.time.set:
          id: heure_pivot
          time: !lambda |-
            return {second: 0,minute: id(heure_pivot).minute, hour: id(heure_pivot).hour};

      - logger.log:
          format: "HPivot %2d:%.2d:%2d"
          args: [ 'id(heure_pivot).hour', 'id(heure_pivot).minute', 'id(heure_pivot).second' ]
          level: DEBUG

      # Heure de debut = Heure pivot converti en minutes - temps filtration converti en minutes
      - lambda: |-
          static double dt=0;
          static double hp=0;
          
          hp = id(heure_pivot).hour*60+id(heure_pivot).minute;
          dt = id(heure_pivot).hour*60+id(heure_pivot).minute-((id(g_tps_filtration)/2)*60);

          id(g_hh)=int(dt/60);
          id(g_mm)=dt-id(g_hh)*60;

      - logger.log:
          format: "H debut Filtration %2d: %.2d"
          args: [ 'id(g_hh)', 'id(g_mm)' ]
          level: DEBUG

      - datetime.time.set:
          id: h_debut
          time: !lambda |-
            return {second: 0, minute: static_cast<uint8_t>(id(g_mm)), hour: static_cast<uint8_t>(id(g_hh))};

      # Heure de fin = Heure pivot converti en minutes + temps filtration converti en minutes
      - lambda: |-
          static double dt=0;
          static double hp=0;
          
          hp = id(heure_pivot).hour*60+id(heure_pivot).minute;
          dt = id(heure_pivot).hour*60+id(heure_pivot).minute+((id(g_tps_filtration)/2)*60);

          id(g_hh)=int(dt/60);
          id(g_mm)=dt-id(g_hh)*60;

      - logger.log:
          format: "H debut Filtration %2d:%.2d"
          args: [ 'id(g_hh)', 'id(g_mm)' ]
          level: DEBUG

      - datetime.time.set:
          id: h_fin
          time: !lambda |-
            return {second: 0, minute: static_cast<uint8_t>(id(g_mm)), hour: static_cast<uint8_t>(id(g_hh))};

      # Convertion et affichage de la durée de filtration en hh:mm
      - lambda: |-
          static double dt=0;
          dt = id(g_tps_filtration)*60;
          id(g_hh)=int(dt/60);
          id(g_mm)=dt-id(g_hh)*60;
      - datetime.time.set:
          id: duree_filtration
          time: !lambda |-
            return {second: 0, minute: static_cast<uint8_t>(id(g_mm)), hour: static_cast<uint8_t>(id(g_hh))};
      
      - lambda: |-
          static String mess;
          mess= String(id(h_debut).hour)+":"+String(id(h_debut).minute)+"/"+String(id(heure_pivot).hour)+":"+String(id(heure_pivot).minute)+"/"+String(id(h_fin).hour)+":"+String(id(h_fin).minute);
          id(aff_heure_filtration).publish_state(mess.c_str());          

      - script.execute: _calcul_ma_at_ppe_filtration

# Calcul la sortie de commande la pompe de filtration en fonction de l'heure actuelle, de l'heure de début et de l'heure de fin
  - id: _calcul_ma_at_ppe_filtration
    mode: single
    then:
      - lambda: |-
          auto time = id(sntp_time).now();
      - logger.log:
          format: "H now: %.2d:%2d:%d"
          args: [ 'id(sntp_time).now().hour', 'id(sntp_time).now().minute', 'id(heure_pivot).second' ]
          level: DEBUG
      - logger.log:
          format: "HT: %.2d - HD:%2d - HF:%d"
          args: [ 'id(sntp_time).now().hour*60+id(sntp_time).now().minute', 'id(h_debut).hour*60+id(h_debut).minute', 'id(h_fin).hour*60+id(h_fin).minute' ]
          level: DEBUG
      - if:
          condition:
            time.has_time:
          else:
            - logger.log:
                format: "L'heure n'est ni initialisée, ni validée!"
                level: INFO
      - if:
          condition:
            - lambda: 'return (id(sntp_time).now().is_valid());'
            - lambda: 'return (id(sntp_time).now().hour*60+id(sntp_time).now().minute >= id(h_debut).hour*60+id(h_debut).minute && id(sntp_time).now().hour*60+id(sntp_time).now().minute < id(h_fin).hour*60+id(h_fin).minute);'
          then:
            - switch.turn_on: cde_ppe_filtration
          else:
            - switch.turn_off: cde_ppe_filtration
    

  # Regulation du pH
  # Calcul du temps d'injection en fonction:
  # caracteristiques du pH-:
    # mp= montant du produit =>quantité necessaire pour agir (0.2l)
    # ve= valeur de l'effet du produit (0.1 à 0.2)
    # va= volume d'eau affecté par le produit ( 10m3)
  # vb= volume du bassin
  # c= consigne du pH
  # m= mesure du pH
  # de= debit de la pompe (en l/h)
  # q=quantité à injecté = (m-c)/ve*(vb/va)*mp
  # temps injection (s)= (q*3600)/de
 
  - id: _regul_ph
    mode: single
    then:
      - if:
          condition:
            - lambda: 'return id(_Mode_Fonctionnement_regul_ph).state == "Auto";'
          then:
            - logger.log:
                format: "Log mem ph: %f / pH Cible: %f"
                args: [ 'id(g_memoire_ph)','id(_ph_cible).state' ]
                level: debug      
            - if:
                condition:
                  and:
                    - lambda: 'return id(g_memoire_ph) > id(_ph_cible).state;'
                    - lambda: 'return id(g_memoire_ph) > 0;'
                    - switch.is_on: cde_ppe_filtration          
                then:
                  - lambda: |-
                      static float m=0;
                      static float c=0;                
                      static float mp=0.2;
                      static float ve=0.2;
                      static float va=10;
                      static float vb=50;
                      static float de=0;
                      static float q=0;
                      static float coef=0;
                      m=id(g_memoire_ph);
                      c=id(_ph_cible).state;
                      de=id(_debit_ppe_moins).state;
                      q=(m-c)/ve*(vb/va)*mp;
                      coef= id(_coef_regul_ph_moins).state/100;
                      id(g_tps_injection_ph_moins)=q*3600/de*coef;
                      id(_vol_injection_ph_moins).publish_state(q);
                else:
                  - lambda: |-
                      id(g_tps_injection_ph_moins)=0;

            - lambda: |-
               id(_tps_injection_ph_moins).publish_state(id(g_tps_injection_ph_moins));

            # Convertion et affichage de la durée d'injection pH en hh:mm
            - lambda: |-
                static double dt=0;
                static int r=0;
                id(g_ss) = id(g_tps_injection_ph_moins);
                // Calcul des heures - minutes
                id(g_hh) = id(g_ss) / 3600;
                r = id(g_ss) - id(g_hh) * 3600;
                id(g_mm) = r / 60;
                id(g_ss) = r - id(g_mm) * 60;
            - datetime.time.set:
                id: duree_injection_ph
                time: !lambda |-
                  return {second: static_cast<uint8_t>(id(g_ss)), minute: static_cast<uint8_t>(id(g_mm)), hour: static_cast<uint8_t>(id(g_hh))};

            - logger.log:
                format: "Log tps injection: %f"
                args: [ 'id(g_tps_injection_ph_moins)' ]
                level: DEBUG

            - if:
                condition:
                  and:
                    - lambda: 'return id(g_tps_injection_ph_moins) > 0;'
                then:
                  - lambda: |-
                      static String mess;
                      mess= "ESP178 Debut injection pH";
                      mess= mess+"\n";
                      mess= mess+"Cible pH: "+String(id(_ph_cible).state)+"\n";
                      mess= mess+"Mesure pH: "+String(id(g_memoire_ph))+"\n";
                      mess= mess+ "Volume:"+ String(id(_vol_injection_ph_moins).state)+"\n";
                      mess= mess+ "Durée:"+String(id(duree_injection_ph).hour)+":"+String(id(duree_injection_ph).minute)+":"+String(id(duree_injection_ph).second);
                      id(_message_telegram)->execute(mess.c_str()); 
                  - switch.turn_on: cde_ppe_ph_moins
                  - logger.log: 
                      format: "Marche ppe Ph moins"
                      level: INFO        
                  - delay: !lambda "return id(g_tps_injection_ph_moins)*1000;"
                  - switch.turn_off: cde_ppe_ph_moins
                  - lambda: |-
                      static String mess;
                      mess= "ESP178 Fin Injection pH";
                      mess= mess+"\n";
                      mess= mess+"Mesure pH: "+String(id(g_memoire_ph))+"\n";
                      mess= mess + "Volume inj:"+ String(id(_vol_injection_ph_moins).state)+"\n";
                      mess= mess + "Durée:"+String(id(duree_injection_ph).hour)+":"+String(id(duree_injection_ph).minute)+":"+String(id(duree_injection_ph).second);
                      id(_message_telegram)->execute(mess.c_str()); 
                  
                  - logger.log: 
                      format: "Arret ppe Ph moins"
                      level: INFO
                else:
                  - switch.turn_off: cde_ppe_ph_moins
                  - logger.log: 
                      format: "Arret ppe Ph moins"
                      level: INFO

      - if:
          condition:
            and:
              - lambda: 'return id(_Mode_Fonctionnement_regul_ph).state == "Ma_f";'
              - switch.is_on: cde_ppe_filtration
          then:
            - switch.turn_on: cde_ppe_ph_moins
            - logger.log: 
                format: "Marche ppe Ph moins"
                level: INFO
      - if:
          condition:
            or:
              - lambda: 'return id(_Mode_Fonctionnement_regul_ph).state == "At_f";'
              - switch.is_off: cde_ppe_filtration
          then:
            - switch.turn_off: cde_ppe_ph_moins
            - logger.log: 
                format: "Arret ppe Ph moins"
                level: INFO
  # Stoppe le Script de regule pH quand la mesure devient supérieure à la consigne
  # Cela évite de trop injecter de pH Moins
  - id: _securisation_regul_ph
    mode: single
    then:
      - if:
          condition:
            - lambda: 'return id(_Mode_Fonctionnement_regul_ph).state == "Auto";'
            - lambda: 'return id(g_memoire_ph) < id(_ph_cible).state;'
            - script.is_running: _regul_ph

          then:
            - logger.log:
                format: "Log mem ph: %f / pH Cible: %f"
                args: [ 'id(g_memoire_ph)','id(_ph_cible).state' ]
                level: debug      
            - script.stop: _regul_ph                
            - switch.turn_off: cde_ppe_ph_moins
            - lambda: |-
                static String mess; 
                static float Q;
                Q = id(_temps_fonctionnement_ppe_ph).state*id(_debit_ppe_moins).state/3600;
                mess= "ESP178 Arret Script Injection pH";
                mess= mess+"\n";
                mess= mess+"Cible pH: "+String(id(_ph_cible).state)+"\n";
                mess= mess+"Mesure pH: "+String(id(g_memoire_ph))+"\n";

                static int h,m,r,s=0;
                // Affichage du temps de fonctionnement Ppe pH
                s = (id(_temps_fonctionnement_ppe_ph).state);
                // Calcul des heures - minutes
                h = s / 3600;
                r = s - h * 3600;
                m = r / 60;
                r = r - m * 60;          
                mess= mess+"Tps Ppe pH: "+String(h)+" h:"+String(m)+" mn:"+String(r)+" s""\n";
                mess= mess + "Volume inj:"+ String(Q)+" L"+"\n";
                id(_message_telegram)->execute(mess.c_str());
                
                 
# Regulation du chlore
# Calcul du temps d'injection en fonction:
# caracteristiques du Chlore 9.6°:
  # mp= montant du produit =>quantité necessaire pour agir (0.1l)
  # ve= valeur de l'effet du produit (1)
  # va= volume d'eau affecté par le produit ( 10m3)
# vb= volume du bassin
# nb= nb de ppm Clhore à ajouter
# de= debit de la pompe (en l/h)
# q=quantité à injecté en litre = (m-c)/ve*(vb/va)*mp
# temps injection (s)= (q*3600)/de

  - id: _regul_chlore
    mode: single
    then:
      - if:
          condition:
            and:
              - switch.is_on: cde_ppe_filtration          
          then:
            - lambda: |-
                static float nb=0;                
                static float mp=0.1;
                static float ve=1;
                static float va=10;
                static float vb=50;
                static float de=0;
                static float q=0;
                static float coef=0;
                nb=id(_chlore_cible).state;
                de=id(_debit_ppe_chlore).state;
                q=(nb)/ve*(vb/va)*mp;
                id(g_tps_injection_chlore)=q*3600/de;
          else:
            - lambda: |-
                id(g_tps_injection_chlore)=0;

      - lambda: |-
          id(_tps_injection_chlore).publish_state(id(g_tps_injection_chlore));

      - logger.log:
          format: "Log tps injection: %f"
          args: [ 'id(g_tps_injection_chlore)' ]
          level: DEBUG
      - if:
          condition:
            - lambda: 'return id(_Mode_Fonctionnement_regul_chlore).state == "Auto";'

          then:
            - if:
                condition:
                  and:
                    - lambda: 'return id(g_tps_injection_chlore) > 0;'
                    - switch.is_on: cde_ppe_filtration
                then:
                  - switch.turn_on: cde_ppe_chlore
                  - logger.log: 
                      format: "Marche ppe Ph Chlore"
                      level: INFO        
                  - delay: !lambda "return id(g_tps_injection_chlore)*1000;"
                  - switch.turn_off: cde_ppe_chlore
                  - logger.log: 
                      format: "Arret ppe chlore"
                      level: INFO
                else:
                  - switch.turn_off: cde_ppe_chlore
                  - logger.log: 
                      format: "Arret ppe chlore"
                      level: INFO

      - if:
          condition:
            and:
              - lambda: 'return id(_Mode_Fonctionnement_regul_chlore).state == "Ma_f";'
              - switch.is_on: cde_ppe_filtration
          then:
            - switch.turn_on: cde_ppe_chlore
            - logger.log: 
                format: "Marche ppe Chlore"
                level: INFO
      - if:
          condition:
            or:
              - lambda: 'return id(_Mode_Fonctionnement_regul_chlore).state == "At_f";'
              - switch.is_off: cde_ppe_filtration
          then:
            - switch.turn_off: cde_ppe_chlore
            - logger.log: 
                format: "Arret ppe Chlore"
                level: INFO

  # Regulation du niveau eau piscine
  - id: _regul_eau
    mode: single  
    then:
      - if:
          condition:
            and:
              - lambda: 'return id(_Mode_Fonctionnement_regul_eau).state == "Auto";'
              - binary_sensor.is_off: niv_defaut
              - binary_sensor.is_off: volet_ferme
              - or:
                - binary_sensor.is_on: niv_bas
                - binary_sensor.is_on: niv_inter
          then:
            - logger.log: 
                format: "Ouverture vanne eau Mode Auto"
                level: INFO
            - switch.turn_on: cde_ev_eau
            - delay: 15min
            - logger.log: 
                format: "Fermeture vanne eau sur Timeout 15 min"
                level: WARN
            - switch.turn_off: cde_ev_eau
      - if:
          condition:
            and:
              - lambda: 'return id(_Mode_Fonctionnement_regul_eau).state == "Auto";'
              - or:
                - binary_sensor.is_on: niv_haut
                - binary_sensor.is_on: niv_defaut
          then:
            - logger.log: 
                format: "Fermeture vanne eau Mode Auto"
                level: INFO
            - switch.turn_off: cde_ev_eau

      - if:
          condition:
            and:
              - lambda: 'return id(_Mode_Fonctionnement_regul_eau).state == "Ma_f";'
          then: 
            - logger.log: 
                format: "Ouverture vanne eau Mode Ma Forçé"
                level: INFO
            - switch.turn_on: cde_ev_eau

      - if:
          condition:
            or:
              - lambda: 'return id(_Mode_Fonctionnement_regul_eau).state == "At_f";'
          then:
            - logger.log: 
                format: "Fermeture vanne eau Mode At Forçé"
                level: INFO
            - switch.turn_off: cde_ev_eau

  # Scripts Commande Volet
  - id: script_ouv_volet
    then:
      - switch.turn_off: cde_volet_fermeture
      - delay: 2s
      - switch.turn_on: cde_volet_ouverture
      - delay: 5s
      - switch.turn_off: cde_volet_ouverture
          
  - id: script_ferm_volet
    then:
      - switch.turn_off: cde_volet_ouverture
      - delay: 2s
      - switch.turn_on: cde_volet_fermeture
      - switch.turn_on: cde_eclairage
      - delay: 90s
      - switch.turn_off: cde_volet_fermeture
      - switch.turn_off: cde_eclairage
                    
  - id: script_stop_volet
    then:
      - switch.turn_off: cde_volet_ouverture
      - switch.turn_off: cde_volet_fermeture
      - delay: 2s
      - switch.turn_on: cde_volet_fermeture
      - delay: 2s
      - switch.turn_off: cde_volet_fermeture
      - switch.turn_off: cde_eclairage

# Calcul des niveaux d'eau en fonction des sondes de niveaux
# si niveau haut et niveau bas => niveau haut
  - id: _calcul_niveau_eau
    then:
      - if: 
          condition:
            and:
              - binary_sensor.is_on: lsh
              - binary_sensor.is_on: lsl
          
          then:
            - binary_sensor.template.publish:
                id: niv_haut
                state: ON
          else:
            - binary_sensor.template.publish:
                id: niv_haut
                state: OFF
      # si pas niveau haut et niveau bas => defaut intermédiaire
      - if: 
          condition:
            and:
              - binary_sensor.is_off: lsh
              - binary_sensor.is_on: lsl
          
          then:
            - binary_sensor.template.publish:
                id: niv_inter
                state: ON
          else:
            - binary_sensor.template.publish:
                id: niv_inter
                state: OFF     
      # si pas niveau haut et pas niveau bas => niveau bas
      - if: 
          condition:
            and:
              - binary_sensor.is_off: lsh
              - binary_sensor.is_off: lsl
          
          then:
            - binary_sensor.template.publish:
                id: niv_bas
                state: ON
          else:
            - binary_sensor.template.publish:
                id: niv_bas
                state: OFF                
      # si niveau haut et pas niveau bas => defaut niveau
      - if: 
          condition:
            and:
              - binary_sensor.is_on: lsh
              - binary_sensor.is_off: lsl
          then:
            - binary_sensor.template.publish:
                id: niv_defaut
                state: ON
          else:
            - binary_sensor.template.publish:
                id: niv_defaut
                state: OFF

# Fonctionnement Hors Gel
# Si temp extérieure inferieur à seuil1 et supérieur à seuil2 alors Ma pompe Filtration pendant 15 mn
# Si temp extérieure inferieur à seuil2 alors Ma pompe Filtration pendant 30 mn

  - id: _fonction_hors_gel
    then:
      # Reset flag HG si temp >S1
      - if:
          condition:
            and:
              - lambda: 'return id(_Mode_Fonctionnement_hg).state == "Activé";'
              - lambda: 'return id(temp_ext).state > id(s1_temp_hg).state;'
          then:
            - lambda: |-
                id(g_flag_hg) = false;
            - logger.log: 
                format: "Reset Flag HG"
                level: INFO
       
      # Activation si S1 >temp >S2
      - if:
          condition:
            and:
              - lambda: 'return id(_Mode_Fonctionnement_hg).state == "Activé";'
              - lambda: 'return id(temp_ext).state < id(s1_temp_hg).state;'
              - lambda: 'return id(temp_ext).state > id(s2_temp_hg).state;'
              - lambda: 'return id(g_flag_hg) == false;'
          then:
            - lambda: |-
                id(g_flag_hg) = true;
            - logger.log: 
                format: "Set Flag HG Seuil1"
                level: INFO
            - lambda: |-                
                static String mess;
                mess= "ESP178 Debut Marche HG Seuil1";
                mess= mess+"Temp Ext: "+String(id(temp_ext).state)+"\n";
                mess= mess+"Seuil1: "+String(id(s1_temp_hg).state)+"\n";
                id(_message_telegram)->execute(mess.c_str());                      
            - delay: 900s  #900s
            - lambda: |-
                id(g_flag_hg) = false;
            - logger.log: 
                format: "Reset Flag HG Seuil1"
                level: INFO     
            - lambda: |-                
                static String mess;
                mess= "ESP178 Fin Marche HG Seuil1";
                mess= mess+"Temp Ext: "+String(id(temp_ext).state)+"\n";
                mess= mess+"Seuil1: "+String(id(s1_temp_hg).state)+"\n";
                id(_message_telegram)->execute(mess.c_str());                               
      # Activation si temp > S2
      - if:
          condition:
            and:
              - lambda: 'return id(_Mode_Fonctionnement_hg).state == "Activé";'
              - lambda: 'return id(temp_ext).state < id(s1_temp_hg).state;'
              - lambda: 'return id(temp_ext).state < id(s2_temp_hg).state;'
              - lambda: 'return id(g_flag_hg) == false;'
          then:
            - lambda: |-
                id(g_flag_hg) = true;
            - logger.log: 
                format: "Set Flag HG Seuil2"
                level: INFO
            - lambda: |-                
                static String mess;
                mess= "ESP178 Debut Marche HG Seuil2";
                mess= mess+"Temp Ext: "+String(id(temp_ext).state)+"\n";
                mess= mess+"Seuil2: "+String(id(s2_temp_hg).state)+"\n";
                id(_message_telegram)->execute(mess.c_str());                   
            - delay: 1800s  #1800s
            - lambda: |-
                id(g_flag_hg) = false;
            - logger.log: 
                format: "Reset Flag HG Seuil2"
                level: INFO      
            - lambda: |-                
                static String mess;
                mess= "ESP178 Fin Marche HG Seuil2";
                mess= mess+"Temp Ext: "+String(id(temp_ext).state)+"\n";
                mess= mess+"Seuil2: "+String(id(s2_temp_hg).state)+"\n";
                id(_message_telegram)->execute(mess.c_str());   

  # Surveille temps utilisation galets Chlore en heure
  - id: _fonction_galet_chlore
    then:
      - if:
          condition:
            and:
              - lambda: 'return (id(_temps_galet_chlore).state/3600) >= id(_tps_max_galet_chlore).state;'
          then:
            - lambda: 
                id(_etat_galets_chlore).publish_state(true);
            - logger.log: 
                format: "Galet Chlore Dépassé"
                level: INFO  
          else:
            - lambda: 
                id(_etat_galets_chlore).publish_state(false);

  # je n'ai pas trouvé de solution pour envoyer un message vers Telegram
  # je passe donc par HA
  # Envoi d'un message à l'automatisme de notification Telegram dans HA
  # message à construire au format String avant appel de ce script
  - id: _message_telegram
    parameters:
      mess1: string
    then:
      - lambda: |-                
          static String mess;
          mess= (id(sntp_time).now().strftime("%Y-%m-%d %H:%M:%S").c_str());
          mess= mess +"\n";
          mess= mess+ mess1.c_str();
          id(_msg_notif_telegram).publish_state(mess.c_str()); 

Intégration dans Home Assistant

L’ESP178 s’intègre via l’API ESPHome. Dans HA, j’ai créé un dashboard avec :

  • Graphiques : pH, température, pression, puissance.
  • Réglages : consignes pH/chlore, modes filtration, seuils hors gel.
  • Notifications Telegram via un text_sensor dédié.

Notification avec Telegram

N’ayant pas trouver de programme de notification « Télégram » fonctionel dans ESP Home, j’utilise ma notification telegram fonctionnelle dans HA. Un automatisme surveille un changement de valeur dans le « sensor.esp178_message_notif_telegram » et notifie en cas de chanement de valeur.

alias: Notification message Telegram de ESP178
description: " Notifie sur Telegram le message mis à jour par l'ESP178 Piscine"
mode: single
triggers:
  - entity_id:
      - sensor.esp178_message_notif_telegram
    for:
      hours: 0
      minutes: 0
      seconds: 0
    trigger: state
conditions:
  - condition: not
    conditions:
      - condition: or
        conditions:
          - condition: state
            entity_id: sensor.esp178_message_notif_telegram
            state: unavailable
          - condition: state
            entity_id: sensor.esp178_message_notif_telegram
            state: unknown
actions:
  - data:
      message: "{{states('sensor.esp178_message_notif_telegram')}}"
      title: Message ESP178!!!
    action: notify.telegram

Exemple de carte HA

type: custom:pool-monitor-card
display:
  compact: false
  show_names: true
  show_labels: true
  show_last_updated: false
  show_icons: true
  show_units: true
  gradient: true
  language: fr
colors:
  normal_color: "#00b894"
  low_color: "#fdcb6e"
  warn_color: "#e17055"
  cool_color: "#00BFFF"
  marker_color: "#000000"
  hi_low_color: "#00000099"
sensors:
  temperature:
    - entity: sensor.esp178_temperature_eau
      setpoint: 30
      step: 3
  ph:
    - entity: sensor.esp178_ph_ezo
      setpoint: 7.2
      step: 0.2
  free_chlorine:
    - entity: sensor.my_sampling_point_pl_chlorine_free
      setpoint: 2
      chlorine_step: 1
  total_chlorine:
    - entity: sensor.my_sampling_point_pl_chlorine_total
      setpoint: 3
      step: 1
  cya:
    - entity: sensor.my_sampling_point_pl_cyanuric_acid
      setpoint: 37.5
      step: 12.5
  alkalinity:
    - entity: sensor.my_sampling_point_pl_alkalinity
      setpoint: 90
      step: 30
  pressure:
    - entity: sensor.esp178_pression_filtre
      unit: bar
      setpoint: 1
      step: 1
  product_weight:
    - entity: sensor.esp129_poids_ph_moins

Vous pouvez utiliser « auto-entites », vous visualisez ainsi toutes les entités contenant « ESP178 » dans mon cas (c’est le nom de mon ESP).

type: custom:auto-entities
card:
  type: entities
  show_header_toggle: false
  title: ESP 178
filter:
  include:
    - entity_id: "*esp178*"
      options: {}
  exclude: []
sort:
  method: entity_id

Résultats

Après plusieurs mois d’utilisation (été 2024) :

  • Filtration : Le mode Palier s’adapte bien à la température, avec environ 12h/j à 25°C.
  • pH : Stabilisé autour de 7.2 avec 0.2-0.5L de pH- injecté par jour.
  • Niveau : Remplissage automatique fiable, sauf si volet fermé.
  • Conso : Environ 1-2 kWh/j pour la pompe filtration.

Améliorations Possibles

  • Ajouter une sonde ORP calibrée pour une régulation chlore plus précise.
  • Intégrer une prévision météo pour ajuster la filtration.
  • Tester le mode Ethernet (commenté dans le code) pour plus de stabilité.

Conclusion

Ce système offre une gestion autonome et personnalisable de ma piscine, avec un coût matériel raisonnable (< 100€ hors PCB). Le PCB maison et le schéma de câblage ont grandement simplifié l’installation et la maintenance. Le code est perfectible, mais il répond déjà à mes besoins. Si vous voulez les fichiers PCB, le schéma EasyEda, ou des précisions, laissez un commentaire !

Liste des publications en lien avec cet article:

  1. Filtration avec ESPHome et ESP32
  2. Filtration avec « AppDaemon » ou avec « Pool Pump Manager« 
  3. Mesure de puissance électrique
  4. Mise à niveau automatique
  5. Mesure du pH
  6. Régulation du Ph
  7. Mise Hors Gel
  8. Mesure de pression

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *