Sommaire
La mise en œuvre de ce projet nécessite un certain nombre de compétences techniques. Elle est réalisée sous la seule responsabilité de la personne qui l’entreprend. HACF et l'auteur de l'article déclinent toute responsabilité en cas de dommage, incident ou mauvaise utilisation.
L'objet de ce DIY est de détailler une méthode de prise de contrôle sur un poêle à granulés, à l'ancienne, qui ne dispose pas de connexion ni de supervision déportée.
La solution est transposable à tout équipement de chauffe disposant d'une sonde d'ambiance filaire de type résistance.
Introduction
État des lieux
Le chauffage de la maison est assuré par un poêle à granulés de 8 kW, de marque DeDietrich, modèle Quadralis (clone de Oertli OPPA8 qui est un haas+Sohn rebrandé), datant de 2012, et ne disposant d'aucun élément de connectivité (un module propriétaire a existé, mais en GSM, très peu répandu et introuvable).
Le poêle dispose de sa propre régulation interne de puissance, type PID, asservie à un thermostat interne alimenté par une sonde de température filaire de type détecteur de température à résistance/Pt100.
Le poêle est piloté sur une consigne de température, et une hystérésis, et fonctionne en mode marche / arrêt entre les deux bornes d’hystérésis.
Au niveau utilisateur, on dispose d'un paramètre sur le PID pour ajuster le ralentissement de la charge thermique lorsque la température approche de la consigne pour éviter de surchauffer.
Ce type d'équipement pose divers problèmes de confort, de flexibilité et de facilité d'utilisation :
- Un calendrier de programmation sommaire et peu flexible / adaptable
- Une alternance de marches / arrêts qui génèrent un inconfort thermique et limitent la durée de vie de la résistance d'allumage
- L'incapacité de prendre la main a distance pour relancer le chauffage en cas d'absence prolongée
Fonctionnement initial
Pour décrire le fonctionnement initial, puis la logique de domotisation, on introduit déjà un certain nombre de paramètres :
- WTI : Température mesurée par la sonde filaire (Wired Temperature Input)
- STT : Température de consigne du poêle (Stove Temperature Target)
- SHY : Hystérésis du poêle (Stove HYsteresis)
- RTT : Température de consigne réelle (Real Temperature Target)
La logique de fonctionnement du poêle est donc la suivante :
- Si WTI < (STT + SHY) alors allumage (dans mons cas SHY = -1°C)
- Tant que (STT + SHY) < WTI < STT alors on maintient la chauffe, moyennant un ralentissement quand (WTI - STI) < x°C, x etant paramétrable dans le poêle
- quand WTI >= STT alors extinction du poêle
Traduit en français :
- Lorsque la mesure de température descend en dessous de la consigne + son hystérésis alors le poêle lance sa séquence d'allumage
- Tant que la mesure de température n'atteint pas la consigne, la chauffe se poursuit tout en ralentissant en approchant de l'objectif
- Lorsque la mesure de température dépasse la consigne, le poêle s'éteint.
Exemple concret :
- Consigne STT = 21°C
- Hystérésis SHY = -1°C
- Marge de régulation à la baisse x = -0,2°C
- Quand WTI < (STT+SHY) = (21°C + (-1°C)) = 20°C le poêle demarre sa chauffe
- arrivé a WTI > (STT+x) = (21°C + (-0,2°C)) = 20,8°C le poêle regule sa puissance au minimum
- Quand WTI > STT = 21°C le poêle s'arrête
C'est le mode de fonctionnement classique de n'importe quel thermostat de base, y compris les climate qu'on définit dans Home Assistant.
Sur cette base le poêle ne dispose que de 3 modes :
- Arrêt : Pas de chauffe du tout
- Marche forcée : Gestion par le thermostat en continu, alternance infinie de marches / arrêts
- Auto : Marche forcée pendant des plages définies et arrêt en dehors
Stratégie de domotisation
Ne disposant pas de schémas électroniques clairement documentés et sans compétences sur le sujet, j'ai adopté une approche détournée. La stratégie repose sur un leurre.
La sonde filaire est de type "résistance". Quand la température ambiante évolue, la résistance du capteur varié et le poêle mesure la température ambiante en lisant la valeur de cette résistance et en l'interprétant au moyen d'une table d'étalonnage.
On comprend alors qu'en injectant des valeurs de résistance dans la régulation du poêle, on arrivera à lui faire faire ce qu'on veut :
- Démarrage forcé de la chauffe : Injection d'une valeur faible de résistance correspondant à une température d'ambiance basse (typiquement 5 °C)
- Arrêt forcé de la chauffe : Injection d'une valeur haute de résistance correspondant à une température d'ambiance basse (typiquement 45 °C)
- Maintien de la chauffe : Injection en continu de valeurs de résistance ajustées en fonction d'une mesure de température déportée
- Modification des consignes de chauffe : Injection d'un offset de correction dans les valeurs envoyées en continu pour créer un écart entre la consigne voulue, et la consigne définie en dur dans le poêle.
On va donc devoir introduire deux nouveaux paramètres :
- ETS : Température mesurée par le capteur déporté (External Temperature Sensor)
- ETC : Correction de mesure de température, (External Temperature Correction)
- CRI : Résistance corrigée (Corrected Resistance Input)
- CTI : Température corrigée (Corrected Temperature input)
La correction de mesure servira à faire croire au poêle qu'il s'approche de sa consigne de chauffe interne (STT) alors qu'en fait, il s'approchera de la consigne réelle (RTT)
Un exemple concret :
- Le poêle est calé en mode auto, avec une consigne STT = 21 °C et une Hystérésis SHY = -1 °C et je veux qu'il chauffe à RTT = 20 °C
- Pour déclencher la chauffe le poêle attend une température mesurée WTI < (STT + SHY) = (21 °C + (-1 °C)) = 20 °C
- Ce qu'on veut, c'est qu'il régule à RTT = 20 °C et non pas à STT = 21 °C, on injecte donc dans la sonde (WTI) des valeurs de résistance corrigées, correspondant à la température corrigée (CTI) calculées à partir des mesures déportées et de la correction de consigne CRI = f(CTI) = f(ETS + ETC) (on ajoute un offset positif pour diminuer artificiellement la consigne, un offset négatif pour l'augmenter)
Avec ETC = (STT - RTT) = (21 °C - 20 °C) = 1 °C
et CTI = ETS + ETC
Le poêle va alors recevoir des informations erronées via son entrée filaire et va continuer fonctionner comme d'habitude, mais en se calant finalement sur les consignes qu'on lui demande.
Admettons qu'on mesure ETS = 19,5 °C et qu'on veuille réguler à STT 20 °C on envoie au poêle la CRI correspondant à ETC = 19,5 + 1 °C = 20,5 °C. Le poêle continue à chauffer tout en voyant qu'il s'approche de sa consigne. On a bien décalé toute la régulation du poêle de 1 °C par rapport à sa consigne interne
Mise en œuvre
Matériel
Pour injecter des valeurs corrigées au poêle, j'utilise un module dédié, un potentiomètre numérique pilotable, qui crée des valeurs de résistance aux bornes en fonction des consignes qu'on lui envoie.
Un module tout prêt existe, OHMIGO / Ohm On wifi, qui fonctionne sous Tasmota, pilotable en wifi :

Câblage
La sonde filaire est reliée directement à la carte mère du poêle, sur un bornier dédié (bornes 39 et 40 dans mon cas).

- fil Gris : Masse / GND
- fil rouge : Résistance (Ohms)
Le câblage est donc le suivant :
- Fil rouge de la sonde sur le NC du contact sec
- Fil rouge du bornier poêle (39) sur le COM du contact sec
- Liaison entre le OHM du ohmigo et le NO du contact SEC
- Liaison du fil rouge de la sonde au NC du contact sec
- Mise à la masse (40) des fils blancs et du Ohmigo

On finalise le câblage en reliant :
- le 230V à l'alim du contact sec
- le 5V USB à l'alim du ohmigo

En l'état, on dispose donc d'un système qui permet de bypasser la sonde filaire pour lui injecter des valeurs fictives de résistance via le module Ohmigo, et la possibilité de revenir en pilotage filaire via le basculement du contact sec.
Intégration à Home Assistant
Le module Ohmigo se connecte en wifi et dialogue via MQTT

Il dispose de deux modes de fonctionnement :
- Mode Température : Injection par MQTT de valeurs de température et utilisation des bibliothèques d'étalonnage internes du module pour simuler la sonde
- Mode Résistance : Injection par MQTT de valeurs de résistance directement
Pour rester le plus universel possible, et aussi parce que le modèle de ma sonde n'était pas présent dans les bibliothèques d'étalonnage, je travaille en mode Résistance.

Une fois connecté au wifi et à MQTT, on dispose d'une interface dans Home Assistant :

On peut vérifier avec MQTT Explorer que le Ohmigo et ses paramètres sont bien accessibles :

Si on regarde la figure au-dessus, on voit que l'envoi d'une valeur de résistance se fait en modifiant le topic MQTT :
aha/18fe34ed492b/oowifi_resistance/cmd
Il vous faudra bien évidemment ajuster les commandes MQTT à votre cas.
Étalonnage
Maintenant que le module Ohmigo est relié au poêle et intégré à Home Assistant, il faut définir la façon dont il va agir et envoyer des informations corrigées au poêle via les instructions reçues depuis home Assistant.
La procédure est simple, mais un peu fastidieuse :
- On positionne le Ohmigo en mode Résistance
- On injecte manuellement des valeurs de résistance via l'interface Home Assistant
- On regarde la valeur de température vue par le poêle et on note la correspondance Résistance / Température
- On balaie ainsi toute la plage des températures qu'on souhaite simuler (typiquement 0 - 50 °C mais on peut se focaliser sur une plage plus petite)

On reporte nos relevés (Ohms / °C) dans un tableau et on calcule une courbe mathématique par régression linéaire (EXCEL fait ça tout seul = Courbe de tendance) :

On voit bien sûr cette illustration qu'on a une courbe parfaitement ajustée sur les points d'étalonnage, via un modèle mathématique simple de type quadratique (polynomiale de degré 2), mais on peut l'approcher de façon tout a fait correcte avec une interpolation linéaire (i.e. polynomiale de degré 1, de type R=a*T+b):
Rohm = f (T°C) = a * T2 + b * T + c
avec a, b et c des constantes. Dans mon cas ces constantes valent :
- a = 0.0173
- b = 6.9597
- c = 813.1
Finalement, pour injecter la résistance en milliohms qui correspond à la température que je veux, il faut que je la convertisse avec ces paramètres :
R = ((0.0173 * T2) + (6.9597 * T) + 813.1) * 1000
Traduit avec les paramètres qu'on a déjà détaillés :
CRI = f(ETS+RTC) = ((0.0173 * (ETS+RTC)2) + (6.9597 * (ETS+RTC)) + 813.1) * 1000
Grâce au modèle on va ajuster la résistance qui est envoyée au poêle en fonction de la température qu'on souhaite lui faire croire, qui est une combinaison de la mesure (ETS) et de la correction par rapport à sa consigne interne (ETC).
Reste maintenant à automatiser toute la procédure et à prendre le contrôle du fonctionnement.
Domotisation et pilotage
Entrées / Helpers
Il faut définir les entités qui vont nous permettre de piloter automatiquement toute l'installation.
Remarque : Pour éviter toute erreur de retranscription, j'utilise mes propres helpers, il vous faudra ajuster en conséquence.
- sensor.temperature_rdc : Capteur déporté de température ambiante (°C)
- input_number.correction_sonde_poele : entrée numérique, offset de correction entre la consigne du poele STT et la consigne réelle RTT (°C)
- input_number.temperature_base_poele : la température de consigne STT figée en dur dans le poêle (°C)
- climate.poele : le thermostat virtuel qui va piloter l'installation
- switch.poele_a_granules_chauffe : le commutateur qui sera piloté par le climate.poele (voir section dédiée)
- input_boolean.poele_a_granules_virtuel : un booleen qui permettra de suivre les phases de marche/arret du climate (voir section dédiée)
- input_number.marge_mode_continu : l'écart à la température de consigne pour maintenir le poêle en mode chauffe minimale. Cette valeur est à ajuster manuellement par essais dans mon cas.
- timer.minuteur_poele_mode_continu : un timer qui permet de limiter dans le temps le fonctionnement au mini de puissance.
- switch.poele_mode_chauffe_continue : un interrupteur pour activer ou désactiver le mode de chauffe continue à faible puissance
Thermostat / Climate
Pour permettre à Home Assistant de gérer le poêle, on crée un thermostat virtuel.
Un thermostat a pour fonction, via un binary_switch, de mettre en route la chauffe et de l'arrêter en fonction :
- De la température de consigne
- De la mesure de température ambiante et d'une hystérésis
C'est un choix arbitraire :
- On peut décider de gérer intégralement les démarrages et arrêts du poêle depuis Home Assistant via le thermostat, en définissant un binary_switch qui injecte des valeurs extrêmes de température dans le Ohmigo et ainsi forcer le poêle à démarrer ou s'arrêter
- On peut choisir, de laisser le poêle démarrer et s'arrêter avec sa propre régulation et se contenter de lui envoyer des valeurs corrigées pour lui "forcer la main" de façon plus douce.
Pour définir le thermostat (climate.poele) on utilise les entrées génériques de l'interface Home Assistant :

On définit ensuite les paramètres qu'il va utiliser (je reviendrai sur l'actionneur qu'il faut créer avant) :

Les hystérésis correspondent à celles qui sont en dur dans le poêle. C'est à adapter suivant les poêles, et les réglages, mais le mien n'a qu'une hystérésis SHY froide, que j'ai mise en dur dans le poêle à 0,5 °C.
Ensuite, on définit les préréglages de température qu'on veut :

Enfin, dans le fichier configuration.yaml, on peut apporter quelques réglages plus fins à ce thermostat. Ici une précision d'affichage de 0,1 °C et un pas de température de 0,5 °C :
homeassistant:
customize:
climate.stove:
precision: 0.1
target_temp_step: 0.5


Commutateur thermostat / Switch virtuel
Pour pouvoir définir le thermostat / climate, il faut un commutateur (switch), qu'il faut définir (ici en YAML).
Pour laisser le poêle lancer sa chauffe et l'arrêter selon sa régulation interne, il faut que le switch qu'on définit ne soit que de l'affichage qui nous permette de savoir que le thermostat en marche ou pas, mais sans influence réelle sur le poêle. D'où sa qualification de virtuel.
Pour ce faire, le switch qu'on définit n'a qu'une action d'affichage, et se contente de passer à ON ou OFF un input.boolean :
#################
# Poele Chauffe #
#################
template:
- switch:
- name: "Poêle à granulés CHAUFFE"
unique_id: poele_a_granules_chauffe
state: "{{ is_state('input_boolean.poele_a_granules_virtuel', 'on') }}"
turn_on:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.poele_a_granules_virtuel
turn_off:
- action: input_boolean.turn_off
target:
entity_id: input_boolean.poele_a_granules_virtuel
Mode Continu
L'objectif du projet est aussi de disposer d'un mode de chauffe continu, hors de la gestion en démarrage / arrêts avec hystérésis.
À l'approche de la température de consigne STT, la régulation interne du poêle réduit la puissance thermique.
Typiquement, entre 0,5 °C et 0,2 °C avant l'atteinte de la consigne, le régime d'alimentation de granulés se cale sur sa puissance mini le temps d'atteindre STT.
L'ajustement de ce seuil se fait à l'aide d'un paramètre dans l'interface du poêle qui "tient compte de l'inertie thermique de la zone à chauffer".
Pour ça on définit un paramètreinput_number.marge_mode_continu.
Sa valeur s'ajuste par essais expérimentaux. Elle dépend de la régulation interne du poele et du réglage de l'amortissement. Elle dépend aussi de la précision de l'affichage du poêle qui induit des biais lors de l'étalonnage.
Par principe la valeur de cette marge est négative (-0,2 à -0,5 °C pour mon poêle), mais suivant la précision de l'étalonnage, on peut aboutir à une valeur positive (+0,1 °C dans mon cas réel).
Si la valeur est trop basse le poêle ne réduira pas sa puissance, si elle est trop élevée il s'arrêtera. Il n'y a pas d'autre solution que de faire quelques essais pour stabiliser la valeur.
Enfin Pour éviter tout problème (surchauffe de la maison, fonctionnement prolongé en absence, mâchefer, …) j'ai introduit un timer sur le fonctionnement du mode continu qui permet un retour au fonctionnement normal au bout de 2 h (à ajuster chez vous).
Le mode continu enclenché par un switch manuel, qui met en route le timer associé :
- si le timer est actif alors mode continu
- si le timer est inactif alors mode normal
######################
# Poele Mode Continu #
######################
template:
- switch:
- name: "Poêle Mode Chauffe Continue"
unique_id: poele_mode_chauffe_continue
state: "{{ is_state('timer.minuteur_poele_mode_continu', 'active') }}"
turn_on:
- action: timer.start
target:
entity_id: timer.minuteur_poele_mode_continu
turn_off:
- action: timer.finish
target:
entity_id: timer.minuteur_poele_mode_continu
Les automatisations de pilotage tiennent compte de l'état de ce timer.
Scripts
On a défini l'affichage de l'installation via le climate, reste à construire le cœur de réacteur sous formes de script qu'on va rappeler au besoin dans les automatisations.
La première chose, c'est de créer un script qui lance la marche forcée du poêle.
alias: Demarrage chauffe poele FORCEE
sequence:
- action: mqtt.publish
metadata: {}
data:
topic: aha/18fe34ed492b/oowifi_resistance/cmd_t
payload: "849000"
description: ""Le script :
- Envoie au topic MQTT qui correspond à la résistance
- une valeur de résistance en milliohms qui correspond à 5 °C
- Le poêle voit une température ambiante de 5 °C, il s'allume
De la même façon, on crée un script d'extinction forcée ou cette fois, on lui envoie une résistance correspondant à une température ambiante de 45 °C :
alias: Arret chauffe poele FORCE
sequence:
- action: mqtt.publish
metadata: {}
data:
topic: aha/18fe34ed492b/oowifi_resistance/cmd_t
payload: "1161000"
description: ""Pour piloter la température de consigne réelle (RTT), on définit un script qui définit la valeur de correction de la consigne du poêle (STT) :
alias: correction temperature cible poele
sequence:
- action: input_number.set_value
metadata: {}
data:
value: >-
{{ (states('input_number.temperature_base_poele') | float -
state_attr('climate.poele', 'temperature') | float) | round(1) }}
target:
entity_id: input_number.correction_sonde_poele
description: ""On définit ensuite le script qui va faire la mise à jour des valeurs de résistance du Ohmigo en fonction de la mesure de température déportée et de la correction.
Comme vous pouvez le voir le script intègre la gestion du mode continu, en fonction de l'état du timer associé. La logique est la suivante :
- Si on est en mode continu : on vérifie la température de la sonde
- Si elle dépasse (consigne + marge mode continu) alors on maintient de manière forcée une fausse valeur à égale à consigne + marge mode continu. et le poêle se maintient alors à la plus faible allure
- Si elle est inférieure alors on envoie la bonne valeur corrigée
- Si on n'est pas en mode continu alors on envoie simplement la bonne valeur corrigée
alias: update resistance ohmigo (Mode continu)
sequence:
- if:
- condition: state
entity_id: timer.minuteur_poele_mode_continu
state: active
then:
- variables:
temp_sonde: "{{states('sensor.temperature_rdc')|float}}"
temp_mode_continu: >-
{{state_attr('climate.poele', 'temperature') | float +
states('input_number.marge_mode_continu')|float}}
alias: Etablissement des variables
- alias: Si la mesure dépasse la consigne + marge
if:
- condition: template
value_template: "{{temp_sonde > temp_mode_continu}}"
then:
- variables:
temp_sonde: "{{temp_mode_continu}}"
alias: Mesure forcée a consigne + marge
- alias: update resistance avec mesure forcée
action: mqtt.publish
metadata: {}
data:
evaluate_payload: false
retain: true
topic: aha/18fe34ed492b/oowifi_resistance/cmd_t
payload: >-
{{ ((0.0173 * ((temp_sonde +
states('input_number.correction_sonde_poele')|float )**2) +
6.9597 * (temp_sonde +
states('input_number.correction_sonde_poele')|float) + 813.1)
*1000)|round(0) }}
qos: "1"
enabled: true
- action: input_number.set_value
target:
entity_id: input_number.resistance_ohmigo
data:
value: >-
{{ ( 0.0173 * ((temp_sonde + (
states('input_number.correction_sonde_poele') | float )) **2 ) +
6.9597 * (temp_sonde +
(states('input_number.correction_sonde_poele') | float )) +
813.1 ) | round(3) }}
enabled: true
else:
- alias: update resistance avec mesure
action: mqtt.publish
metadata: {}
data:
evaluate_payload: false
retain: true
topic: aha/18fe34ed492b/oowifi_resistance/cmd_t
payload: >-
{{ ((0.0173 * ((temp_sonde +
states('input_number.correction_sonde_poele')|float )**2) +
6.9597 * (temp_sonde +
states('input_number.correction_sonde_poele')|float) + 813.1)
*1000)|round(0)}}
qos: "1"
enabled: true
- action: input_number.set_value
target:
entity_id: input_number.resistance_ohmigo
data:
value: >-
{{ ( 0.0173 * ((temp_sonde + (
states('input_number.correction_sonde_poele') | float )) **2 ) +
6.9597 * (temp_sonde +
(states('input_number.correction_sonde_poele') | float )) +
813.1 ) | round(3) }}
else:
- alias: Update résistance avec mesure
action: mqtt.publish
metadata: {}
data:
evaluate_payload: false
retain: true
topic: aha/18fe34ed492b/oowifi_resistance/cmd_t
payload: >-
{{ ((0.0173 * (((states('sensor.temperature_rdc')|float) +
(states('input_number.correction_sonde_poele')|float) )**2) + 6.9597
* ((states('sensor.temperature_rdc')|float) +
(states('input_number.correction_sonde_poele')|float) )+ 813.1)
*1000)|round(0) }}
qos: "1"
enabled: true
- action: input_number.set_value
target:
entity_id: input_number.resistance_ohmigo
data:
value: >-
{{ ( 0.0173 * (((states('sensor.temperature_rdc') | float ) + (
states('input_number.correction_sonde_poele') | float )) **2 ) +
6.9597 * (( states('sensor.temperature_rdc') | float ) +
(states('input_number.correction_sonde_poele') | float )) + 813.1 )
| round(3) }}
description: ""
mode: restartOn dispose maintenant de l'ensemble des scripts nécessaires pour piloter le Ohmigo et injecter les valeurs qu'on souhaite dans la régulation du poêle. Reste à construire les automatisations.
Automatisations
Le système tel que je l'ai conçu est composé de 5 automatisations simples qui s'articulent entre elles pour gérer automatiquement toute l'installation et éviter des boucles imbriquées et autres rétroactions.
Automatisation 1 : Gestion du thermostat / climate
- Quand on démarre le thermostat :
- On active l'automatisation 2 qui sert à mettre à jour en continu les valeurs de température
- On force la mise à jour de la température une première fois
- Quand on Arrête le thermostat :
- On désactive l'automatisation 2, pour arrêter la mise à jour des températures
- On ordonne au poêle de s'arrêter
- On arrête le calendrier de chauffe (c'est un choix arbitraire, à vous de voir si c'est utile)
alias: Gestion Thermostat Poele
description: ""
triggers:
- alias: Démarrage thermostat
trigger: state
entity_id:
- climate.poele
from: "off"
to: heat
id: demarrage thermostat
- alias: arret thermostat
trigger: state
entity_id:
- climate.poele
from: heat
to: "off"
id: arret thermostat
conditions: []
actions:
- choose:
- conditions:
- condition: trigger
id:
- arret thermostat
sequence:
- action: automation.turn_off
data:
stop_actions: true
target:
entity_id: automation.update_ohmigo_resistance
- action: script.turn_on
metadata: {}
data: {}
target:
entity_id: script.arret_chauffe_poele_force
- action: switch.turn_off
metadata: {}
data: {}
target:
entity_id:
- switch.schedule_planning_temperatures
- conditions:
- condition: trigger
id:
- demarrage thermostat
sequence:
- action: automation.turn_on
data: {}
target:
entity_id: automation.update_ohmigo_resistance
- action: automation.trigger
metadata: {}
data:
skip_condition: true
target:
entity_id: automation.update_ohmigo_resistance
mode: singleAutomatisation 2 : Mise à jour de la résistance du Ohmigo
À chaque changement de valeur du capteur de température, on met à jour la résistance envoyée au Ohmigo
alias: update ohmigo resistance
description: ""
triggers:
- trigger: state
entity_id:
- sensor.temperature_rdc
conditions: []
actions:
- action: script.turn_on
metadata: {}
data: {}
target:
entity_id:
- script.update_resistance_ohmigo_mode_continu
enabled: true
mode: singleAutomatisation 3 : Mise à jour de l'écart STT / RTT
À chaque fois qu'on modifie la consigne de température sur le thermostat, on recalcule l'écart entre la consigne réelle RTT et la consigne du poêle STT qui nous servira à ajuster les résistances du Ohmigo
alias: Correction consigne temperature Poele
description: ""
triggers:
- trigger: state
entity_id:
- climate.poele
attribute: temperature
conditions: []
actions:
- action: script.turn_on
metadata: {}
data: {}
target:
entity_id: script.correction_temperature_cible_poele
- if:
- condition: state
entity_id: climate.poele
state: heat
then:
- action: script.turn_on
metadata: {}
data: {}
target:
entity_id:
- script.update_resistance_ohmigo_mode_continu
enabled: true
mode: single
Automatisations 4 et 5 : Gestion des erreurs en mode continu.
Comme on travaille dans une zone très étroite de température et/ou le poêle fonctionne à sa limite basse, on introduit une gestion des problèmes en mode continu.
La première automatisation enclenche la surveillance :
alias: Marche / Arrêt mode continu
description: ""
triggers:
- trigger: state
entity_id:
- timer.minuteur_poele_mode_continu
to: active
from: idle
id: "démarrage mode continu "
- trigger: state
entity_id:
- timer.minuteur_poele_mode_continu
to: idle
from: active
id: arrêt mode continu
conditions: []
actions:
- action: script.turn_on
metadata: {}
data: {}
target:
entity_id: script.update_resistance_ohmigo_mode_continu
- if:
- condition: trigger
id:
- "démarrage mode continu "
then:
- action: automation.turn_on
metadata: {}
data: {}
target:
entity_id: automation.alerte_erreur_mode_continu
- if:
- condition: trigger
id:
- arrêt mode continu
then:
- action: automation.turn_off
metadata: {}
data:
stop_actions: true
target:
entity_id: automation.alerte_erreur_mode_continu
mode: single
La seconde surveille la consommation électrique du poêle et avertit si elle devient nulle alors que le poêle est censé fonctionner en continu :
alias: Alerte erreur mode continu
description: ""
triggers:
- trigger: numeric_state
entity_id:
- sensor.prise_poele_power
for:
hours: 0
minutes: 5
seconds: 0
below: 1
conditions: []
actions:
- action: notify.maison_soucieu
metadata: {}
data:
message: >-
le poêle s'est arrêté alors qu'il est en mode continu. allez vérifier
sur l'écran du poêle.
title: Attention
mode: singleVoilà, le poêle est désormais intégralement piloté via Home Assistant avec une programmation horaire bien plus fine issue d'un scheduler, adaptable facilement.
Interfaçage
L'interfaçage du système, passe par un Scheduler / Scheduler Card qui permet une programmation par horodatage et une modification des consignes de chauffe bien plus flexible et automatisable :

type: custom:scheduler-card
title: true
tags:
- chauffage
discover_existing: false
show_header_toggle: false
sort_by:
- relative-time
- state
include:
- climate.poele
exclude: []
Pour de la gestion ponctuelle, on peut prendre la main par l'application par exemple pour l'actionner à distance via une carte dédiée (ici un bubble card) :

type: custom:bubble-card
card_type: climate
entity: climate.poele
sub_button:
main:
- name: HVAC modes menu
select_attribute: hvac_modes
state_background: true
show_arrow: false
sub_button_type: select
- entity: climate.poele
select_attribute: preset_modes
state_background: false
show_background: true
sub_button_type: select
bottom: []
show_last_changed: false
show_state: false
state_color: false
show_name: true
card_layout: normal
show_icon: true
scrolling_effect: false
show_attribute: true
attribute: current_temperature
j'utilise aussi une carte "Simple Thermostat" sur laquelle j'ai rajouté un switch qui active ou désactive le calendrier de chauffe

type: custom:simple-thermostat
entity: climate.poele
layout:
mode:
headings: false
icons: true
header:
icon: mdi:fireplace
toggle:
entity: switch.schedule_planning_temperatures
name: Mode AUTO
hide:
state: true
control:
hvac: true
preset:
none: false
away:
name: Absent
icon: mdi:door-closed
comfort:
name: Confort
icon: mdi:sofa
eco:
name: Eco
icon: mdi:leaf
home:
name: Normal
icon: mdi:home-thermometer
sleep:
name: Nuit
icon: mdi:sleep
Exemple du fonctionnement du poêle en mode normal sur une journée froide, avec le calendrier de chauffe qui évolue sur la journée :
- À 5 h du matin le poêle se déclenche avec une consigne à 20 °C
- Au lever, il remonte sa consigne à 20,5 °C
- Le soir devant la télé le poêle régule à 21 °C
- La nuit la consigne redescend à 18 °C

Le poêle s'allume quand on atteint environ -0,6 °C par rapport à la consigne réelle (RTT) et se coupe quand on dépasse d'environ +0,2 °C la consigne réelle.
La courbe orange est le suivi du switch virtuel qu'on a créé explicitement pour définir le climate. On peut voir qu'il est tout à fait en cohérence avec le fonctionnement réel du poêle.
Compléments
Température d'ambiance
Plutôt que de me reposer sur un seul capteur, qui finalement ne fera que décaler le problème de la sonde filaire un peu plus loin dans l'espace et dans le temps, et pour éviter tout problème en cas de perte de ce capteur, j'ai construit une température pondérée à partir de 3 capteurs disséminés au rez-de-chaussée de la maison (sensor.temperature_rdc) et j'ai introduit un système de fallback qui empèche l'arrêt du chauffage en cas de perte d'une des sondes.
{% set sensors = [
('sensor.capteur_temperature_salon_temperature', 3),
('sensor.capteur_salle_a_manger_temperature', 1),
('sensor.capteur_cuisine_temperature', 1)
] %}
{% set total = namespace(value=0, weight=0) %}
{% for entity, weight in sensors %}
{% set val = states(entity) %}
{% if is_number(val) %}
{% set total.value = total.value + (val | float) * weight %}
{% set total.weight = total.weight + weight %}
{% endif %}
{% endfor %}
{% if total.weight > 0 %}
{{ (total.value / total.weight) | round(1) }}
{% else %}
{# This returns the sensor's own current state (last known value) #}
{# if all sub-sensors are unavailable. #}
{{ this.state if is_number(this.state) else 'unavailable' }}
{% endif %}La température est une pondération de 3 capteurs dont le poids varie en fonction de leur proximité avec le poêle ce qui permet :
- De tenir compte de l'inertie de la pièce
- De limiter la surchauffe immédiatement a proximité du poêle
- De poursuivre la chauffe suffisamment pour que les zones les plus éloignées soient aussi correctmenet chauffées
- D'avoir une valeur de repli cohérente et non fixe en cas de perte de 1 ou 2 sondes.
Visualisation sous AWTRIX
J'ai également rajouté une visualisation via le AWTRIX qui permet d'avoir un retour d'état visuel via le composant HACS adéquat :

alias: Notifications AWTRIX
description: ""
triggers:
- alias: Poele en programmation auto
trigger: state
entity_id:
- switch.schedule_planning_temperatures
from: "off"
to: "on"
id: poele en programmation auto
- alias: Poele en controle manuel
trigger: state
entity_id:
- switch.schedule_planning_temperatures
from: "on"
to: "off"
id: poele en controle manuel
- trigger: state
entity_id:
- climate.poele
from: "off"
to: heat
alias: Demarrage thermostat poele
id: demarrage thermostat poele
- alias: Arret thermostat poele
trigger: state
entity_id:
- climate.poele
from: heat
to: "off"
id: arret thermostat poele
conditions: []
actions:
- choose:
- conditions:
- condition: trigger
id:
- poele en programmation auto
sequence:
- action: awtrix.awtrix_018f14_push_app_data
data:
data:
text: Poele en mode automatique
rainbow: false
icon: "6208"
duration: 30
pushIcon: 0
lifetime: 120
repeat: -1
name: controle_poele
- action: awtrix.awtrix_018f14_switch_app
metadata: {}
data:
name: controle_poele
- conditions:
- condition: trigger
id:
- poele en controle manuel
sequence:
- action: awtrix.awtrix_018f14_push_app_data
data:
name: controle_poele
data:
text: Poele en mode manuel
rainbow: false
icon: "59259"
duration: 30
pushIcon: 0
lifetime: 120
repeat: -1
- action: awtrix.awtrix_018f14_switch_app
metadata: {}
data:
name: controle_poele
- conditions:
- condition: trigger
id:
- demarrage thermostat poele
sequence:
- action: awtrix.awtrix_018f14_push_app_data
data:
name: chauffage
data:
text: Chauffage en service
rainbow: false
icon: "27076"
duration: 30
pushIcon: 0
lifetime: 120
repeat: -1
- action: awtrix.awtrix_018f14_switch_app
metadata: {}
data:
name: chauffage
- conditions:
- condition: trigger
id:
- arret thermostat poele
sequence:
- action: awtrix.awtrix_018f14_push_app_data
data:
name: chauffage
data:
text: chauffage a l'arret
rainbow: false
icon: "16755"
duration: 30
pushIcon: 0
lifetime: 120
repeat: -1
- action: awtrix.awtrix_018f14_switch_app
metadata: {}
data:
name: chauffage
Fallback perte Ohmigo
Grâce à un tracker Nmap, on suit la connectivité du Ohmigo et en cas de problème, on revient sur la sonde physique et le poêle reprend sa régulation normale sur sa sonde et sa consigne STT (d'où l'intérêt de figer STT à une température "raisonnable") :
alias: Fallback Ohmigo
description: ""
triggers:
- trigger: state
entity_id:
- device_tracker.ohmigo
to: not_home
from: null
for:
hours: 0
minutes: 5
seconds: 0
id: perte ohmigo nmap
- trigger: state
entity_id:
- device_tracker.ohmigo
to: home
from: null
for:
hours: 0
minutes: 5
seconds: 0
id: recuperation ohmigo nmap
conditions: []
actions:
- choose:
- conditions:
- condition: trigger
id:
- perte ohmigo
- perte ohmigo nmap
sequence:
- action: switch.turn_off
metadata: {}
data: {}
target:
entity_id: switch.fallback_poele
- conditions:
- condition: trigger
id:
- recuperation ohmigo
- recuperation ohmigo nmap
sequence:
- action: switch.turn_on
metadata: {}
data: {}
target:
entity_id: switch.fallback_poele
mode: single
Mesure du niveau de pellets dans le réservoir
Pour mesurer la quantité de pellets restant dans le poêle, je suis parti d'un simple capteur à ultrasons SR04 relié à un ESP32 et installé sous le capot du réservoir :


La difficulté réside dans le fait que :
- La surface du stock de pellets n'est pas uniforme
- la surface évolue en créant des trous par extraction des granulés, en fond de réservoir
- la surface évolue quand ces trous se remplissent, lorsque les pellets roulent.
- le réservoir est une cavité métallique pas vraiment idéale pour des ultrasons
- le remplissage du réservoir se fait en ouvrant le capot
Si on se contente de suivre les valeurs du capteur, on a alors un signal hautement instable et variable et il est impossible de connaître avec une bonne précision le niveau de granulés restants.
Ainsi si le niveau baisse trop vite par rapport à la tendance à long terme, c'est qu'on est en train de creuser un trou et on l'ignore en se calant sur la tendance a long terme puis on reprend le suivi lorsque la tendance revient.
La tendance à court terme permet de visualiser les baisses abruptes de niveau qui sont le signe d'un remplissage.
Merci ChatGPT / Claude / Gemini pour la mise en œuvre de l'idée, ça dépasse mes compétences !!
Voici l'algorithme de traitement du signal au format mermaid :

flowchart TD
A[📡 Ultrasonic Sensor Raw Data - Trigger GPIO5, Echo GPIO18, Update 200ms, Noise High] --> B[⚙️ Filtering and Bounding - NaN Rejection, Lid Open Threshold 0.75 m, Last Valid Value Retention]
B --> C[📊 Temporal Stabilization and Trend Extraction - Median Filter Fast window_fast pts 5 to 15 min, Slow window_slow pts 15 to 60 min, Circular Buffer 120, Update 1s]
C --> D[🛠️ Physical State Reconstruction - Max Consumption Rate 10 cm/h, Refill Delta 0.15 m, Refill Slope Trigger -200 cm/h, Refill Slope Reset -80 cm/h, System Ready Initialization]
D --> E[⚡ Short-term Trend Analysis - Fast Slope pente_pellets_5min, Detect Rapid Drop for Refill Event]
E --> F[🎯 Refill Event Detection - Increment Counters daily_refill_count, weekly_kg_total, monthly_kg_total]
D --> G[🏠 Exposed State for Home Assistant - distance_pellets_reconstruit, binary_sensor Reservoir Empty]
C --> H[📈 Historical Trends - daily_refill_count, weekly_kg_total, monthly_kg_total]
%% Classes de style
classDef raw fill:#f9f,stroke:#333,stroke-width:1px,color:#000
classDef filter fill:#ffeb99,stroke:#333,stroke-width:1px,color:#000
classDef median fill:#99ddff,stroke:#333,stroke-width:1px,color:#000
classDef reconstruct fill:#9f9,stroke:#333,stroke-width:1px,color:#000
classDef event fill:#ff9999,stroke:#333,stroke-width:1px,color:#000
classDef ha fill:#cccccc,stroke:#333,stroke-width:1px,color:#000
classDef monitor fill:#ffd699,stroke:#333,stroke-width:1px,color:#000
le YAML de l'ESP32 :
# ==============================================================================
# CONFIGURATION GÉNÉRALE & SUBSTITUTIONS
# Ces variables définissent les constantes métier pour le calcul des stocks.
# On utilise une logique de "pente" pour différencier la consommation (lente)
# du remplissage (rapide/brutal).
# ==============================================================================
substitutions:
device_name: "niveau-pellets"
friendly_name: "Niveau Pellets"
pente_buffer_size: "120" # Capacité du buffer (120 points x 30s = 60 min)
slope_multiplier: "12000.0" # Facteur de conversion mathématique : m/30s vers cm/h
lid_open_threshold: "0.75" # Seuil d'exclusion : au-delà de 75cm, le capot est jugé ouvert
refill_delta_m: "0.15" # Delta minimum (15cm) pour valider physiquement un sac
refill_slope_trigger: "-200.0" # Seuil de pente négative pour détection de remplissage
refill_slope_reset: "-80.0" # Hystérésis pour réarmer la détection de remplissage
consumption_max_cm_h: "10.0" # Bride de sécurité : consommation max théorique du poêle
# ==============================================================================
# PARAMÈTRES SYSTÈME & RÉSEAU
# Utilisation du framework ESP-IDF pour une meilleure stabilité des tâches BLE/WiFi.
# Le Bluetooth Proxy permet d'étendre la portée du Bluetooth via Home Assistant.
# ==============================================================================
esphome:
name: "${device_name}"
friendly_name: "${friendly_name}"
min_version: 2025.1.0
build_path: build/niveau-pellets
includes:
- theil_sen.h
esp32:
board: esp32dev
framework:
type: esp-idf
esp32_ble:
max_connections: 5
esp32_ble_tracker:
scan_parameters:
interval: 320ms
window: 300ms
active: true
bluetooth_proxy:
active: true
connection_slots: 5
logger:
level: DEBUG
baud_rate: 0
wifi:
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
- ssid: !secret iot_ssid
password: !secret iot_password
manual_ip:
static_ip: 192.168.0.26
gateway: 192.168.0.1
subnet: 255.255.255.0
power_save_mode: NONE
enable_btm: true
enable_rrm: true
api:
encryption:
key: !secret niveau_pellets_api_key
ota:
- platform: esphome
password: !secret niveau_pellets_admin
# ==============================================================================
# GESTION DU TEMPS & RÉINITIALISATION DES COMPTEURS
# Synchronisation NTP via Home Assistant pour gérer les cycles de consommation.
# Remise à zéro automatique des totaux (Journalier, Hebdomadaire, Mensuel).
# ==============================================================================
time:
- platform: homeassistant
timezone: "Europe/Paris"
id: homeassistant_time
on_time:
- seconds: 0
minutes: 0
hours: 0
then:
- lambda: id(daily_refill_count) = 0;
- seconds: 0
minutes: 0
hours: 0
days_of_week: MON
then:
- lambda: id(weekly_kg_total) = 0;
- seconds: 0
minutes: 0
hours: 0
days_of_month: 1
then:
- lambda: id(monthly_kg_total) = 0;
# ==============================================================================
# RÉGLAGES DYNAMIQUES (Number)
# Permet l'ajustement en temps réel de la sensibilité de l'algorithme sans reflasher.
# - Fenêtres de pente : définit la robustesse face au bruit.
# - Seuil vide : définit l'alerte de niveau bas.
# ==============================================================================
number:
- platform: template
name: "Points pente Rapide"
id: window_fast
min_value: 3
max_value: 30
step: 1
initial_value: 10
unit_of_measurement: "pts"
restore_value: yes
optimistic: yes
- platform: template
name: "Points pente Lente"
id: window_slow
min_value: 15
max_value: 120
step: 1
initial_value: 30
unit_of_measurement: "pts"
restore_value: yes
optimistic: yes
- platform: template
name: "Seuil réservoir vide"
id: empty_threshold
min_value: 0.0
max_value: 0.75
step: 0.01
initial_value: 0.30
unit_of_measurement: "m"
restore_value: yes
optimistic: yes
# ==============================================================================
# VARIABLES GLOBALES
# Stockage en RAM (et Flash pour 'restore_value') des états persistants.
# Sert de mémoire tampon pour l'algorithme de reconstruction du niveau.
# ==============================================================================
globals:
- id: last_valid_distance
type: float
restore_value: yes
initial_value: '0.01'
- id: last_reconstructed_level
type: float
restore_value: yes
initial_value: '0.01'
- id: system_ready
type: bool
initial_value: 'false'
- id: pente_index
type: int
initial_value: '0'
- id: pente_count
type: int
initial_value: '0'
- id: daily_refill_count
type: int
restore_value: yes
initial_value: '0'
- id: weekly_kg_total
type: int
restore_value: yes
initial_value: '0'
- id: monthly_kg_total
type: int
restore_value: yes
initial_value: '0'
- id: refill_latch
type: bool
initial_value: 'false'
# ==============================================================================
# SECTION CAPTEURS
# ==============================================================================
sensor:
- platform: wifi_signal
name: "Qualité Signal WiFi"
state_class: measurement
- platform: internal_temperature
name: "Température CPU ESP32"
state_class: measurement
# ----------------------------------------------------------------------------
# STATISTIQUES DE CONSOMMATION
# Conversion des variables globales en entités capteurs pour Home Assistant.
# ----------------------------------------------------------------------------
- platform: template
name: "Rechargement Journalier"
unit_of_measurement: "sacs"
icon: "mdi:sack"
state_class: measurement
accuracy_decimals: 0
lambda: return (float)id(daily_refill_count);
- platform: template
name: "Consommation Hebdomadaire"
unit_of_measurement: "kg"
icon: "mdi:weight-kilogram"
state_class: measurement
accuracy_decimals: 0
lambda: return (float)id(weekly_kg_total);
- platform: template
name: "Consommation Mensuelle"
unit_of_measurement: "kg"
icon: "mdi:weight-kilogram"
state_class: measurement
accuracy_decimals: 0
lambda: return (float)id(monthly_kg_total);
# ----------------------------------------------------------------------------
# 1. ACQUISITION BRUTE
# Capteur HC-SR04. On utilise un filtre médian agressif pour éliminer les
# échos parasites dus à la poussière ou aux parois du silo.
# ----------------------------------------------------------------------------
- platform: ultrasonic
trigger_pin: GPIO5
echo_pin: GPIO18
name: "Distance pellets brute"
id: distance_pellets_brute
icon: "mdi:signal-distance-variant"
update_interval: 200ms
filters:
- median:
window_size: 41
send_every: 5
# ----------------------------------------------------------------------------
# 2. BORNAGE ET GESTION DU CAPOT
# Étape de "Sanity Check". Si la mesure dépasse le seuil physique (lid_open),
# on fige la valeur précédente pour éviter de fausser les calculs de pente
# pendant que l'utilisateur remplit le silo.
# ----------------------------------------------------------------------------
- platform: template
name: "Distance pellets bornee"
id: distance_pellets_bornee
icon: "mdi:filter-check"
unit_of_measurement: "m"
accuracy_decimals: 3
state_class: measurement
update_interval: 1s
lambda: |-
float val = id(distance_pellets_brute)->state;
float last = id(last_valid_distance);
if (isnan(val)) return last;
if (val > ${lid_open_threshold}) {
return last;
}
if (val >= 0.01) {
if (val < last || fabs(val - last) < 0.05) {
id(last_valid_distance) = val;
return val;
}
}
return last;
# ----------------------------------------------------------------------------
# 3. BUFFERISATION CIRCULAIRE
# Stockage des mesures dans un tableau (global_pente_buffer) pour permettre
# l'analyse statistique sur le temps long. Un filtre médian temporel est
# appliqué pour lisser la courbe avant l'analyse de pente.
# ----------------------------------------------------------------------------
- platform: template
name: "Distance pellets filtre median"
id: distance_pellets_filtre_median
icon: "mdi:filter-plus"
unit_of_measurement: "m"
accuracy_decimals: 3
state_class: measurement
update_interval: 1s
lambda: return id(distance_pellets_bornee)->state;
on_value:
then:
- lambda: |-
if (!isnan(x)) {
global_pente_buffer[id(pente_index)] = x;
id(pente_index) = (id(pente_index) + 1) % ${pente_buffer_size};
if (id(pente_count) < ${pente_buffer_size}) id(pente_count)++;
}
filters:
- median:
window_size: 300
send_every: 30
- lambda: |-
return isnan(x) ? id(last_valid_distance) : x;
# ----------------------------------------------------------------------------
# 4 & 5. ESTIMATEUR DE PENTE (THEIL-SEN)
# Utilisation de l'algorithme robuste de Theil-Sen pour calculer la tendance.
# Contrairement à une régression linéaire classique, cet estimateur ignore
# les valeurs aberrantes (outliers), idéal pour un milieu poussiéreux.
# ----------------------------------------------------------------------------
- platform: template
name: "Pente pellets rapide"
id: pente_pellets_5min
icon: "mdi:trending-up"
unit_of_measurement: "cm/h"
accuracy_decimals: 2
state_class: measurement
update_interval: 30s
lambda: |-
int n = std::min(id(pente_count), (int)id(window_fast)->state);
float s = compute_theil_sen_circular(n, id(pente_index), ${pente_buffer_size});
float res = s * (float)${slope_multiplier};
return (float)std::clamp(res, -5000.0f, 5000.0f);
- platform: template
name: "Pente pellets lente"
id: pente_pellets_15min
icon: "mdi:trending-up"
unit_of_measurement: "cm/h"
accuracy_decimals: 2
state_class: measurement
update_interval: 30s
lambda: |-
int n = std::min(id(pente_count), (int)id(window_slow)->state);
float s = compute_theil_sen_circular(n, id(pente_index), ${pente_buffer_size});
float res = s * (float)${slope_multiplier};
return (float)std::clamp(res, -5000.0f, 5000.0f);
# ----------------------------------------------------------------------------
# 6. MOTEUR DE RECONSTRUCTION LOGIQUE
# C'est ici que l'intelligence métier réside :
# - Détecte un remplissage si la pente chute brutalement et le niveau monte.
# - Verrouille (Latch) pour ne compter qu'un seul sac à la fois.
# - Filtre la descente (consommation) en limitant la vitesse de variation
# physiquement possible du poêle pour ignorer les bruits de mesure.
# ----------------------------------------------------------------------------
- platform: template
name: "Distance pellets reconstruit"
id: distance_pellets_reconstruit
icon: "mdi:ruler"
unit_of_measurement: "m"
accuracy_decimals: 3
state_class: measurement
update_interval: 30s
lambda: |-
float target = id(distance_pellets_filtre_median)->state;
float current = id(last_reconstructed_level);
float p_slow = isnan(id(pente_pellets_15min)->state) ? 0.0f : id(pente_pellets_15min)->state;
float p_fast = isnan(id(pente_pellets_5min)->state) ? 0.0f : id(pente_pellets_5min)->state;
if (!id(system_ready)) {
if (!isnan(target)) {
id(last_reconstructed_level) = target;
id(system_ready) = true;
ESP_LOGI("pellets", "System Ready. Level: %.3f m", target);
}
return id(last_reconstructed_level);
}
if (p_fast < (float)${refill_slope_trigger} && target < current - (float)${refill_delta_m}) {
if (!id(refill_latch)) {
id(daily_refill_count)++;
id(weekly_kg_total) += 15;
id(monthly_kg_total) += 15;
id(refill_latch) = true;
ESP_LOGI("pellets", "Sack added!");
}
id(last_reconstructed_level) = target;
}
else {
if (p_fast > (float)${refill_slope_reset}) id(refill_latch) = false;
if (p_slow > 0) {
float diff_m = target - current;
if (diff_m > 0) {
float limited_p = std::min(p_slow, (float)${consumption_max_cm_h});
float max_step_m = limited_p / (float)${slope_multiplier};
id(last_reconstructed_level) += std::min(diff_m, max_step_m);
}
}
}
return id(last_reconstructed_level);
# ==============================================================================
# CAPTEURS BINAIRES
# Alerte de niveau critique basée sur la distance reconstruite.
# Utilisation d'un 'delayed_on' pour éviter les fausses alertes passagères.
# ==============================================================================
binary_sensor:
- platform: template
name: "Réservoir Vide"
device_class: problem
lambda: return id(distance_pellets_reconstruit)->state > id(empty_threshold)->state;
filters:
- delayed_on: 30min
# ==============================================================================
# ACTIONS MANUELLES (Buttons)
# Permet de corriger manuellement les compteurs ou de redémarrer l'unité.
# Le forçage de remplissage recalibre immédiatement le niveau reconstruit.
# ==============================================================================
button:
- platform: template
name: "Forcer Remplissage (+15kg)"
id: btn_force_refill
icon: "mdi:sack-percent"
on_press:
- lambda: |-
float current_level = id(distance_pellets_filtre_median).state;
if (isnan(current_level)) {
ESP_LOGW("manual_refill", "Capteur indisponible (NaN), action annulée.");
return;
}
id(last_reconstructed_level) = current_level;
id(daily_refill_count) += 1;
id(weekly_kg_total) += 15;
id(monthly_kg_total) += 15;
id(refill_latch) = false;
ESP_LOGI("manual_refill", "Remplissage forcé exécuté. Nouveau niveau : %.3f m", current_level);
id(distance_pellets_reconstruit).update();
- platform: restart
name: "Redémarrer ESP32 Pellets"
icon: "mdi:restart"
Le capteur est aussi équipé :
- d'un bouton qui permet de réarmer les remplissages en cas de pb de détection automatique
- de curseurs pour régler le seuil de réservoir vide et l'inertie des tendances
- d'un système de mémorisation des dernieres valeurs valides qui survit à un reboot
Il faut lui adjoindre sa routine de calcul des tendances basées sur un algo Theil-Sen qui permet de faire des tendances propres même avec des données assez éparpillées theil_sen.h :
#include "esphome.h"
#include <algorithm>
// Buffer global physique pour 120 points (60 minutes de données)
static float global_pente_buffer[120] = {0};
float compute_theil_sen_circular(int n, int head, int size) {
if (n < 3) return 0.0f;
// Capacité maximale pour n=120 points (7140 pentes)
const int MAX_SLOPES = 7140;
static float slopes[MAX_SLOPES];
int k = 0;
// Calcul de l'index de départ dans le buffer circulaire
int start_idx = (head - n + size) % size;
for (int i = 0; i < n - 1; i++) {
for (int j = i + 1; j < n; j++) {
float y_i = global_pente_buffer[(start_idx + i) % size];
float y_j = global_pente_buffer[(start_idx + j) % size];
if (k < MAX_SLOPES) {
// Pente = Delta Distance / Delta Temps (en unités d'index)
slopes[k++] = (y_j - y_i) / (float)(j - i);
}
}
}
if (k == 0) return 0.0f;
// Recherche de la médiane par tri partiel (O(n) moyen)
std::nth_element(slopes, slopes + k / 2, slopes + k);
return slopes[k / 2];
}
et le résultat illustré sur le traitement des données. On passe d'un signal bruité et fortement variable à une courbe stabilisée qui donne une approximation réaliste du contenu du réservoir.
Les chutes brutales de distance sont bien capturées par le capteur qui traduit ça comme un remplissage et incrémente la consommation de granulés (1 sac = 15 kilos). On dispose d'un bouton qui permet de déclencher manuellement la prise en compte d'un remplissage si jamais ça ne s'est pas fait automatiquement.

L'ESP fait remonter dans HA toutes les données ainsi que les paramètres ajustables pour introduire plus ou moins d'inertie au système. Plus on met d'inertie plus la courbe sera lissée, mais plus le temps de réponse sera lent.

Conclusion
Moyennant un module préfabriqué, mais potentiellement remplaçable par une version DIY low cost, et quelques automatisations basiques, on aboutit à une prise de contrôle complète d'un poêle à granulés, ou de tout autre équipement (chaudière, pilotage radiateurs…) reposant sur la mesure de température par résistance.
La partie la plus compliquée a finalement été la construction du capteur de niveau de granulés qui demande un gros traitement du signal et un algorithme un peu compliqué. Potentiellement, on peut s'épargner une partie de ce traitement en changeant le type de capteur.
L'investissement est très vite amorti en termes de confort, de facilité d'utilisation et de flexibilité. La consommation de granulés s'en ressent aussi, tout en restant plus proche des besoins réels.
J'ai fait le choix de ne pas me séparer de la régulation de puissance du poêle, mais on pourrait sans doute exploiter la possibilité "Over Climate" de Versatile Thermostat pour ajuster la régulation et anticiper les besoins en fonction de la température extérieure. Cela demande de creuser le sujet et de faire un certain nombre d'essais, pour caler les TPI et vérifier que ça ne pose pas de problème, car ça revient à empiler des régulateurs, ce qui n'est jamais aisé.
Enfin, les modifications effectuées à l'équipement sont très facilement réversibles (il suffit de rebrancher la sonde directement sur la carte mère du poêle) ce qui permet de revenir à l'état d'origine simplement en cas de revente.

