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 :
- 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é). - 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 misoffset: 2sur la sonde milieu ; conserve-le si tu as étalonné “sur robinet”, comme expliqué dans la V1.
