HA-Mesure de température de l’eau (V2026) : stratification + estimation du volume d’eau chaude

Intro

Dans un article précédent HA-Gestion Eau Chaude Sanitaire – Domo Rem81 je mesurais la température du ballon ECS avec une seule sonde au contact de la cuve donnant ainsi une tendance exploitable (et c’est déjà très bien).
Mais dans un ballon électrique, la réalité est souvent une stratification : très chaud en haut, tiède au milieu, froid en bas. Avec une seule mesure, on peut croire que “le ballon est bon” alors que le volume réellement utilisable (ex : douche) ne l’est pas autant.

Objectif de cette V2026 :

  • mesurer 5 points de température du haut vers le bas,
  • en déduire un volume d’eau exprimé en litres,
  • garder une valeur cohérente même si une sonde décroche (WiFi, bus 1-Wire, etc.).
  • Gérer la chauffe de l’ECS non pas en fonction d’une température mais en fonction d’un volume d’eau chaude disponible.

Réalisation:

Implantation des sondes (recommandation pratique)

  • Très haut / Haut : proche de la sortie ECS et du dôme supérieur (zone la plus chaude).
  • Milieu : mi-hauteur du ballon.
  • Bas / Très bas : zone proche de la résistance (souvent la plus “froide” en début de chauffe PV partielle).
  • Les DS18B20 peuvent être :
    • soit glissées dans l’isolant au contact de la cuve (comme expliqué dans la V1),
    • soit positionnées dans des fourreaux si le ballon en dispose.

Bus 1-Wire : câblage classique (3 fils), sondes en parallèle avec résistance de rappel de 4.7 k ohm entre le « signal » et le « +VCC ».


Principe de calcul du “volume d’eau chaude utile”

On découpe virtuellement le ballon (200 L) en 4 segments verticaux définis par 5 sondes.

Pour chaque segment :

  • si les 2 températures (haut/bas du segment) sont au-dessus du seuil → segment “100% chaud”
  • si les 2 sont sous le seuil → segment “0% chaud”
  • si on est en transition → on estime la fraction chaude du segment

Dans le code, j’ai ajouté deux sécurités importantes :

  1. Correction monotone : on force T[0] >= T[1] >= T[2] >= T[3] >= T[4] (on évite les inversions aberrantes dues à une sonde qui “dérive” ou un point de mesure mal plaqué).
  2. Valeur de repli : si une sonde est indisponible (NaN) → on conserve le dernier volume valide (global restauré au reboot).

Code ESPHome (ESP32 + 6 DS18B20 + volume calculé)

Remarque : j’ai déclaré 6 sondes de T° (5 pour le ballon + 1 “sortie régulateur”). Le volume calculé utilise les 5 sondes ballon, la « température sortie régulateur » me sert uniquement de contrôle et d’alarme en cas de défaillance du régulateur thermostatique situé en sortie de l’ECS.

Vous trouverez la dernière version du code ici: home-assistant/esphome/esp139-ecs.yaml at master · remycrochon/home-assistant

substitutions:
  device_name: esp139-ecs
  adress_ip: "192.168.0.139"

esphome:
  name: ${device_name}

esp32:
  board: esp32dev
  framework:
    type: arduino

wifi:
  networks:
    - ssid: !secret wifi
      password: !secret mdpwifi
  reboot_timeout: 5min
  min_auth_mode: WPA2  
  manual_ip:
    static_ip: ${adress_ip}
    gateway: 192.168.0.254
    subnet: 255.255.255.0
    dns1: !secret dns1
    dns2: !secret dns2

# 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

logger:
  level: DEBUG

api:
  
ota:
  platform: esphome

web_server:
  port: 80

one_wire:
  - platform: gpio  
    pin: GPIO32

globals:
  - id: last_volume_chaud
    type: float
    restore_value: yes
    initial_value: '0'

sensor:
  - platform: dallas_temp
    address: 0x3649465609646128
    name: "Temp Trés Haut"
    id: temp_th
    update_interval: 10s
    device_class: temperature
    accuracy_decimals: 2

  - platform: dallas_temp
    address: 0xc0ae154809646128
    name: "Temp Haut"
    id: temp_h
    update_interval: 10s
    device_class: temperature
    accuracy_decimals: 2

  - platform: dallas_temp
    address: 0xab48885709646128
    name: "temp_ecs"
    id: temp_m
    update_interval: 10s
    device_class: temperature
    accuracy_decimals: 2
    filters: 
      - offset: 2

  - platform: dallas_temp
    address: 0x82012111efe81d28
    name: "Temp Bas"
    id: temp_b
    update_interval: 10s
    device_class: temperature
    accuracy_decimals: 2
    
  - platform: dallas_temp
    address: 0x61b6945709646128
    name: "Temp Trés Bas"
    id: temp_tb
    update_interval: 10s
    device_class: temperature
    accuracy_decimals: 2

  - platform: dallas_temp
    address: 0x94000000855c4e28
    name: "Temp Sortie Régulateur"
    id: temp_s_regule
    update_interval: 10s
    device_class: temperature
    accuracy_decimals: 2

  - platform: template
    name: "Ballon volume eau chaude"
    id: ballon_volume_chaud
    unit_of_measurement: "L"
    icon: "mdi:water-thermometer"
    accuracy_decimals: 0
    update_interval: 10s
    lambda: |-
      // Paramètres physiques
      const float V_TOTAL = 200.0f;   // volume total du ballon en litres
      const float T_SEUIL = 40.0f;    // température mini eau "utile"
      const float K = 4.0f;           // raideur de la transition hyperbolique (2 à 6)

      // Récupération des 5 sondes (du haut vers le bas)
      float T[5] = {
        id(temp_th).state,  // Très haut
        id(temp_h).state,   // Haut
        id(temp_m).state,   // Milieu
        id(temp_b).state,   // Bas (résistance)
        id(temp_tb).state   // Très bas
      };

      bool all_ok = true;
      for (int i = 0; i < 5; i++) {
        if (isnan(T[i])) {
          all_ok = false;
        }
      }

      // Si une sonde est indispo → on garde le dernier volume valide
      if (!all_ok) {
        ESP_LOGW("ecs", "Lecture DS18B20 manquante → on garde last_volume_chaud = %.0f L", id(last_volume_chaud));
        return id(last_volume_chaud);
      }

      // 🔧 Correction monotone :
      // on impose que T[0] >= T[1] >= T[2] >= T[3] >= T[4]
      for (int i = 1; i < 5; i++) {
        if (T[i] > T[i-1]) {
          T[i] = T[i-1];
        }
      }

      const int   SEGMENTS = 4;                  // 5 sondes → 4 segments verticaux
      const float V_SEG    = V_TOTAL / SEGMENTS; // volume par segment
      float volume = 0.0f;

      for (int i = 0; i < SEGMENTS; i++) {
        float Th = T[i];       // température en haut du segment
        float Tb = T[i+1];     // température en bas du segment
        float f  = 0.0f;       // fraction d'eau chaude dans le segment (0..1)

        if (Th >= T_SEUIL && Tb >= T_SEUIL) {
          // Segment entièrement au-dessus du seuil
          f = 1.0f;
        } else if (Th < T_SEUIL && Tb < T_SEUIL) {
          // Segment entièrement en dessous du seuil
          f = 0.0f;
        } else {
          // Zone de transition : on interpole + correction hyperbolique
          float denom = Th - Tb;

          if (fabs(denom) < 0.01f) {
            // Gradient quasi nul → on considère homogène
            f = (Th >= T_SEUIL) ? 1.0f : 0.0f;
          } else {
            // interpolation linéaire de la position de T_SEUIL entre Th et Tb
            float f_lin = (Th - T_SEUIL) / denom;
            if (f_lin < 0.0f) f_lin = 0.0f;
            if (f_lin > 1.0f) f_lin = 1.0f;

            // Application d'une sigmoïde hyperbolique autour de 0.5
            float x   = f_lin - 0.5f;      // recentrage en 0
            float num = tanh(K * x);
            float den = tanh(K * 0.5f);    // normalisation pour garder [0,1]
            float f_hyp = 0.5f * (num / den + 1.0f);

            f = f_hyp;
          }
        }

        // Sécurité bornes
        if (f < 0.0f) f = 0.0f;
        if (f > 1.0f) f = 1.0f;

        volume += f * V_SEG;
      }

      // On mémorise le dernier volume valide
      id(last_volume_chaud) = volume;
      return volume;

binary_sensor:
  - platform: status
    name: "Status"

switch:   
  - platform: gpio
    name: "Relais"
    pin: GPIO16
    id: relais

Exploitation dans les automatismes

Idée simple:

Dans mon article, les automatismes V1 se basaient sur un seuil de température (ex : < 40°C → chauffe nuit en HC, > 45°C → stop).
Avec le volume, tu peux piloter plus finement, par exemple :

  • Nuit (HC) : si Ballon volume eau chaude < 80 L → chauffe réseau
  • Arrêt : si Ballon volume eau chaude > 140 L → stop

Exemple (logique identique à ton “cahier des charges”, mais critère “L” au lieu de “°C”) :

trigger:
  - platform: numeric_state
    entity_id: sensor.ballon_volume_eau_chaude
    below: 80
    id: v_bas
  - platform: numeric_state
    entity_id: sensor.ballon_volume_eau_chaude
    above: 140
    id: v_haut
  # + tes triggers HC/HP + soleil + mode Auto, etc.

# Puis choose: comme dans ton automatisme, en remplaçant les conditions de température.

Idée un peu plus complexe:

J’exploite le volume d’ECS dans ma nouvelle version de routeur ESPHOME 2026, voir cet article: HA-Routeur Solaire Photovoltaïque avec ESPHome — V2026 (HP/HC + calibration) – Domo Rem81


Tableau de bord:

Exemple de carte:

type: picture-elements
elements:
  - entity: sensor.esp139_ecs_temp_sortie_regulateur
    prefix: ""
    style:
      background: null
      color: black
      font-size: 150%
      left: 60%
      top: 3%
      transform: none
    type: state-label
  - entity: sensor.esp139_ecs_temp_tres_haut
    prefix: <--
    style:
      background: null
      color: black
      font-size: 150%
      left: 65%
      top: 25%
      transform: none
    type: state-label
  - entity: sensor.esp139_ecs_temp_haut
    prefix: <--
    style:
      background: null
      color: black
      font-size: 150%
      left: 65%
      top: 35%
      transform: none
    type: state-label
  - entity: sensor.esp139_ecs_temp_ecs
    prefix: <--
    style:
      background: null
      color: black
      font-size: 150%
      left: 65%
      top: 45%
      transform: none
    type: state-label
  - entity: sensor.esp139_ecs_temp_bas
    prefix: <--
    style:
      background: null
      color: black
      font-size: 150%
      left: 65%
      top: 55%
      transform: none
    type: state-label
  - entity: sensor.esp139_ecs_temp_tres_bas
    prefix: <--
    style:
      background: null
      color: black
      font-size: 150%
      left: 65%
      top: 65%
      transform: none
    type: state-label
  - entity: sensor.esp139_ecs_ballon_volume_eau_chaude
    prefix: ""
    style:
      background: null
      color: black
      font-size: 190%
      left: 30%
      top: 50%
      transform: none
    type: state-label
  - entity: sensor.esp176_esp32_routeur_1r_p_ecs_jsymk
    prefix: ""
    style:
      background: null
      color: black
      font-size: 150%
      left: 76%
      top: 75%
      transform: none
    type: state-label
image: /local/images/ecs.png
grid_options:
  columns: 12
  rows: 10

Le fichier image est téléchargeable ici: domo.rem81/ecs.png at main · remycrochon/domo.rem81

Notes de réglage:

  • V_TOTAL : adapte à ton ballon (200 L ici).
  • T_SEUIL : 40°C est un bon standard “eau utile”, mais tu peux mettre 42–45°C si tu veux être plus conservateur.
  • K (tanh) : plus K est grand, plus la transition est “brutale”. 3–5 est généralement raisonnable.
  • Offset sur temp_ecs : tu as mis offset: 2 sur la sonde milieu ; conserve-le si tu as étalonné “sur robinet”, comme expliqué dans la V1.

Laisser un commentaire

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