Thermostat obligatoire d'ici 2030, radiateurs connectés, protocole Zigbee, Home Assistant… Vous cherchez à reprendre le contrôle de votre chauffage sans dépendre du cloud ? Après plusieurs semaines de tests, je partage mon blueprint YAML complet pour piloter un radiateur connecté avec le thermostat Bosch BTH-RM via Zigbee2MQTT.
Vous cherchez à intégrer le thermostat Bosch Room Thermostat II (RBSH-RTH0-BAT-ZB-EU) dans Home Assistant pour piloter un radiateur connecté ? Après plusieurs semaines de tests et d'itérations, je partage ici mon blueprint YAML complet, prêt à l'emploi, avec les explications des pièges que j'ai rencontrés.
Thermostat obligatoire en France : ce que dit la loi
Avant de parler technique, un rappel réglementaire important. Le décret n° 2023-444 du 7 juin 2023 impose l'installation d'un thermostat programmable dans tous les logements en France. Les échéances ont été ajustées fin 2025 par le décret n° 2025-1343 :
- Logements neufs : obligation au 1er janvier 2027 (pour les permis de construire déposés à partir de cette date)
- Logements existants : obligation reportée au 1er janvier 2030
- Depuis 2018 : déjà obligatoire lors du remplacement d'une chaudière
Le thermostat doit permettre au minimum quatre niveaux de programmation : confort, réduit, hors gel et arrêt, avec une régulation par pièce ou par zone. Bonne nouvelle : la loi n'impose pas un thermostat connecté — un simple thermostat programmable suffit. Mais tant qu'à s'équiper, autant aller plus loin.
Selon l'ADEME, un thermostat programmable permet d'économiser jusqu'à 15 % sur la facture de chauffage. Un thermostat connecté, grâce à l'adaptation intelligente aux habitudes de vie, peut atteindre 20 % d'économies selon le type de logement.
Côté financement, des aides existent : la prime "Coup de pouce chauffage" peut couvrir une partie de l'installation. Les propriétaires sont responsables de l'équipement, les locataires de l'entretien. Une exemption est prévue si le retour sur investissement dépasse 10 ans.
L'approche que je présente dans cet article va bien au-delà de l'obligation légale : un thermostat Zigbee Bosch couplé à Home Assistant offre une régulation pièce par pièce, une programmation avancée et zéro dépendance cloud — le tout en local.
Pourquoi choisir le thermostat Zigbee Bosch BTH-RM ?
Le Bosch Room Thermostat II est un thermostat Zigbee 3.0 sur batterie au design discret et épuré. Contrairement aux solutions Wi-Fi qui saturent votre réseau domestique et dépendent du cloud du fabricant, il communique en local via Zigbee — aucune dépendance cloud. Il embarque un capteur de température et d'humidité, un écran LED blanc lisible avec molette rotative, et une autonomie d'environ un an sur piles AA.
Compact (86 × 87 × 28 mm, 85 g), il s'intègre discrètement dans n'importe quelle pièce. Son principal intérêt pour les utilisateurs Home Assistant : il peut servir de télécommande physique pour piloter n'importe quel système de chauffage, à condition de créer le bon lien d'automatisation entre le thermostat Bosch et votre radiateur.
Prérequis : matériel et logiciel
Avant de commencer, assurez-vous d'avoir :
- Home Assistant installé et fonctionnel
- Zigbee2MQTT configuré avec un coordinateur Zigbee (Sonoff ZBDongle-P, Conbee II, etc.)
- Un radiateur connecté piloté via une entité
climatedans Home Assistant (generic thermostat, module fil pilote, radiateur Intuis, etc.) - Le thermostat Bosch BTH-RM appairé à votre réseau Zigbee
Appairage du Bosch BTH-RM avec Zigbee2MQTT
L'appairage est simple mais mérite quelques précisions. La documentation Zigbee2MQTT du BTH-RM détaille la procédure complète.
- Activez le mode appairage dans Zigbee2MQTT (bouton "Autoriser la connexion" ou
permit_join: true) - Sur le thermostat Bosch, appuyez simultanément sur les boutons + et - pendant 3 à 5 secondes
- Le symbole Zigbee clignote sur l'écran : le thermostat cherche le réseau
- Zigbee2MQTT détecte l'appareil comme Bosch BTH-RM
Une fois appairé, le thermostat expose plusieurs entités dans Home Assistant, dont l'entité climate qui nous intéresse.
La particularité du Bosch : le mode plage de température
C'est le premier piège que j'ai rencontré. Contrairement à la plupart des thermostats Zigbee qui exposent un simple attribut temperature pour la consigne, le Bosch utilise un mode plage avec deux attributs :
target_temp_low: consigne basse (ex : 21°C)target_temp_high: consigne haute (ex : 23°C)
Cela se confirme avec l'attribut supported_features: 386, qui indique le support de TARGET_TEMPERATURE_RANGE et non TARGET_TEMPERATURE simple. La documentation des entités climate de Home Assistant détaille ces différents modes.
En pratique, quand vous tournez la molette sur le thermostat, c'est target_temp_low qui change et qui correspond à votre consigne de chauffage. C'est cette valeur qu'il faut écouter dans vos automations.
Côté MQTT, la consigne s'appelle occupied_heating_setpoint et attend une valeur en degrés (pas en centidegrés). Voici un extrait du payload MQTT typique :
{
"local_temperature": 22.3,
"occupied_heating_setpoint": 21,
"occupied_cooling_setpoint": 23,
"system_mode": "heat",
"operating_mode": "manual",
"humidity": 47.6,
"setpoint_change_source": "manual",
"battery": 90
}
Blueprint Home Assistant : synchronisation bidirectionnelle thermostat et radiateur
Ce blueprint assure une synchronisation bidirectionnelle entre le thermostat Bosch et un thermostat générique Home Assistant qui pilote votre radiateur. Concrètement :
- Quand vous changez la température sur le Bosch → le radiateur suit
- Quand une automatisation modifie le thermostat cible → l'affichage du Bosch se met à jour
- Quand vous éteignez ou allumez l'un → l'autre suit
Installation
Méthode rapide : cliquez sur le bouton ci-dessous pour importer directement le blueprint dans votre Home Assistant :
Méthode manuelle :
- Téléchargez le fichier YAML depuis le dépôt GitHub
- Copiez-le dans votre dossier
config/blueprints/automation/custom/ - Rechargez les automations dans Home Assistant
- Créez une nouvelle automation basée sur ce blueprint
- Sélectionnez votre thermostat Bosch, renseignez son nom Zigbee2MQTT, et choisissez votre thermostat cible
Le code YAML
blueprint:
name: Bosch BTH-RM - Contrôle Thermostat V5
description: |
Contrôle bidirectionnel du thermostat avec le Bosch Room Thermostat II (BTH-RM)
Fonctionnalités :
- Synchronisation bidirectionnelle de la consigne de température
- Synchronisation de l'état ON/OFF
- Mode manuel automatique au démarrage
Note: Le Bosch utilise target_temp_low/high (mode plage)
domain: automation
input:
bosch_climate:
name: Thermostat Bosch BTH-RM
description: L'entité climate du Bosch Room Thermostat II
selector:
entity:
filter:
domain: climate
bosch_z2m_name:
name: Nom Zigbee2MQTT du Bosch
description: "Le nom exact du device dans Zigbee2MQTT (ex: chambre_climate_control)"
selector:
text:
target_thermostat:
name: Thermostat cible à contrôler
description: L'entité climate (thermostat générique) à piloter avec le Bosch
selector:
entity:
filter:
domain: climate
temp_min:
name: Température minimum
description: Température minimum autorisée
default: 5
selector:
number:
min: 4
max: 15
step: 0.5
unit_of_measurement: "°C"
temp_max:
name: Température maximum
description: Température maximum autorisée
default: 30
selector:
number:
min: 20
max: 35
step: 0.5
unit_of_measurement: "°C"
mode: queued
max_exceeded: silent
max: 5
trigger:
# Initialisation au démarrage de Home Assistant
- platform: homeassistant
event: start
id: initialization
# Tout changement sur le Bosch
- platform: state
entity_id: !input bosch_climate
id: bosch_change
# Tout changement sur le thermostat cible
- platform: state
entity_id: !input target_thermostat
id: target_change
condition:
# Ignorer les états unavailable/unknown
- condition: template
value_template: >
{{ trigger.id == 'initialization' or
(trigger.to_state.state not in ['unavailable', 'unknown'] and
trigger.from_state.state not in ['unavailable', 'unknown']) }}
action:
- variables:
bosch_entity: !input bosch_climate
bosch_name: !input bosch_z2m_name
target_entity: !input target_thermostat
min_temp: !input temp_min
max_temp: !input temp_max
- choose:
# ==========================================
# INITIALISATION AU DÉMARRAGE
# ==========================================
- conditions:
- condition: trigger
id: initialization
sequence:
- delay:
seconds: 10
# Mettre le Bosch en mode manuel
- service: mqtt.publish
data:
topic: "zigbee2mqtt/{{ bosch_name }}/set"
payload: '{"operating_mode": "manual"}'
- delay:
milliseconds: 500
# Synchroniser depuis le thermostat cible vers le Bosch
- variables:
init_temp: "{{ state_attr(target_entity, 'temperature') | float(20) }}"
init_state: "{{ states(target_entity) }}"
init_mode: "{{ 'off' if init_state == 'off' else 'heat' }}"
- service: mqtt.publish
data:
topic: "zigbee2mqtt/{{ bosch_name }}/set"
payload: >
{"occupied_heating_setpoint": {{ init_temp }}, "system_mode": "{{ init_mode }}"}
# ==========================================
# CHANGEMENT SUR LE BOSCH → TARGET
# ==========================================
- conditions:
- condition: trigger
id: bosch_change
sequence:
- variables:
# Le Bosch utilise target_temp_low pour le chauffage
bosch_temp: "{{ state_attr(bosch_entity, 'target_temp_low') | float(0) }}"
bosch_state: "{{ states(bosch_entity) }}"
target_temp: "{{ state_attr(target_entity, 'temperature') | float(0) }}"
target_state: "{{ states(target_entity) }}"
old_bosch_temp: "{{ trigger.from_state.attributes.target_temp_low | default(0) | float(0) }}"
old_bosch_state: "{{ trigger.from_state.state }}"
# Sync température si elle a changé sur le Bosch
- choose:
- conditions:
- "{{ bosch_temp > 0 }}"
- "{{ bosch_temp != old_bosch_temp }}"
- "{{ (bosch_temp - target_temp) | abs > 0.1 }}"
sequence:
- service: climate.set_temperature
target:
entity_id: "{{ target_entity }}"
data:
temperature: "{{ [[bosch_temp, min_temp] | max, max_temp] | min }}"
# Sync état ON/OFF si changé sur le Bosch
- choose:
- conditions:
- "{{ bosch_state != old_bosch_state }}"
- "{{ bosch_state == 'off' and target_state != 'off' }}"
sequence:
- service: climate.turn_off
target:
entity_id: "{{ target_entity }}"
- conditions:
- "{{ bosch_state != old_bosch_state }}"
- "{{ bosch_state != 'off' and target_state == 'off' }}"
sequence:
- service: climate.set_hvac_mode
target:
entity_id: "{{ target_entity }}"
data:
hvac_mode: heat
# ==========================================
# CHANGEMENT SUR LE TARGET → BOSCH
# ==========================================
- conditions:
- condition: trigger
id: target_change
sequence:
- variables:
bosch_temp: "{{ state_attr(bosch_entity, 'target_temp_low') | float(0) }}"
bosch_state: "{{ states(bosch_entity) }}"
target_temp: "{{ state_attr(target_entity, 'temperature') | float(0) }}"
target_state: "{{ states(target_entity) }}"
old_target_temp: "{{ trigger.from_state.attributes.temperature | default(0) | float(0) }}"
old_target_state: "{{ trigger.from_state.state }}"
# Sync température si elle a changé sur le target
- choose:
- conditions:
- "{{ target_temp > 0 }}"
- "{{ target_temp != old_target_temp }}"
- "{{ (target_temp - bosch_temp) | abs > 0.1 }}"
sequence:
- service: mqtt.publish
data:
topic: "zigbee2mqtt/{{ bosch_name }}/set"
payload: '{"occupied_heating_setpoint": {{ target_temp }}}'
# Sync état ON/OFF si changé sur le target
- choose:
- conditions:
- "{{ target_state != old_target_state }}"
- "{{ target_state == 'off' and bosch_state != 'off' }}"
sequence:
- service: mqtt.publish
data:
topic: "zigbee2mqtt/{{ bosch_name }}/set"
payload: '{"system_mode": "off"}'
- conditions:
- "{{ target_state != old_target_state }}"
- "{{ target_state != 'off' and bosch_state == 'off' }}"
sequence:
- service: mqtt.publish
data:
topic: "zigbee2mqtt/{{ bosch_name }}/set"
payload: '{"system_mode": "heat"}'
Explications des choix techniques
Quelques points méritent d'être détaillés pour ceux qui voudraient adapter ce blueprint.
Pourquoi mode: queued et max: 5 ? Le Bosch peut envoyer plusieurs mises à jour MQTT rapprochées quand on tourne la molette. Le mode queued garantit que chaque changement est traité dans l'ordre, sans en perdre. Le max: 5 évite une accumulation excessive si le réseau Zigbee ralentit.
Pourquoi target_temp_low et pas temperature ? Comme expliqué plus haut, le Bosch expose supported_features: 386 qui indique un mode plage. L'attribut temperature classique n'existe pas sur cette entité. C'est target_temp_low qui porte la consigne de chauffage. La documentation des entités climate détaille le calcul des supported_features.
Pourquoi publier via MQTT plutôt qu'utiliser climate.set_temperature ? Le Bosch en mode plage attend set_temperature avec target_temp_low ET target_temp_high. Passer par MQTT avec occupied_heating_setpoint est plus direct et plus fiable pour modifier uniquement la consigne de chauffage.
Pourquoi comparer trigger.from_state au lieu d'un simple seuil ? C'est le mécanisme anti-boucle principal. Au lieu d'ajouter un délai de 500 ms et d'espérer que l'état se soit stabilisé, on compare directement l'ancien état (trigger.from_state.attributes.target_temp_low) avec le nouveau. Si la valeur n'a pas réellement changé sur l'entité source du trigger, on ne fait rien. Cela élimine les boucles de synchronisation de manière déterministe plutôt que temporelle.
Pourquoi un clamp [[val, min] | max, max] | min plutôt que des conditions >= / <= ? Avec des conditions, une valeur hors limites est simplement ignorée — l'utilisateur tourne la molette et rien ne se passe, sans feedback. Avec un clamp, la valeur est ramenée dans les bornes autorisées : si l'utilisateur demande 35°C et que le max est 30°C, le radiateur reçoit 30°C. Le comportement est prévisible et transparent.
Pourquoi climate.set_hvac_mode: heat au lieu de climate.turn_on ? turn_on rétablit le dernier mode HVAC utilisé, qui pourrait être cool ou auto selon l'entité cible. set_hvac_mode: heat est explicite et garantit que le radiateur passe en mode chauffage, pas en mode climatisation.
Pourquoi un seul payload MQTT combiné à l'initialisation ? Au démarrage, l'ancienne version envoyait la consigne et l'état ON/OFF dans deux commandes MQTT séparées avec un délai de 2 secondes entre les deux. La version actuelle combine les deux dans un seul payload {"occupied_heating_setpoint": X, "system_mode": "heat"}. C'est plus rapide et évite un état transitoire où le thermostat aurait la bonne consigne mais le mauvais état (ou l'inverse).
Pièges à éviter avec le thermostat Bosch BTH-RM
Voici les problèmes que j'ai rencontrés pendant la mise en place, pour vous faire gagner du temps.
Le supported_features: 386 — Si vous copiez un blueprint conçu pour un thermostat classique (Tuya, Aqara…), il ne fonctionnera pas avec le Bosch. La plupart des blueprints communautaires écoutent l'attribut temperature, qui n'existe pas sur le BTH-RM. Vérifiez toujours les attributs de votre entité climate dans Outils de développement → États.
Le mode operating_mode — Le Bosch démarre parfois en mode schedule (programmation interne). Dans ce mode, il ignore les consignes envoyées par Home Assistant et suit sa propre programmation. Le blueprint force le passage en mode manual au démarrage de HA pour éviter ce problème.
La détection setpoint_change_source — Le payload MQTT du Bosch inclut un champ setpoint_change_source qui vaut manual quand l'utilisateur tourne la molette, et externally quand la consigne vient de l'extérieur (via MQTT). C'est utile pour le debug si vous observez des boucles de synchronisation. Le blueprint utilise une approche plus robuste : la comparaison trigger.from_state vs état actuel, qui détecte si la valeur a réellement changé sur l'entité source du trigger.
Le Delivery failed sur les commandes ZCL — Si vous voyez cette erreur dans les logs Zigbee2MQTT, c'est souvent lié au fait que le Bosch est un appareil sur batterie qui dort entre les transmissions. Assurez-vous que votre maillage Zigbee est solide (ajoutez des routeurs à proximité). Le thread dédié sur le forum Home Assistant documente bien ces cas.
Mon installation domotique chauffage complète
Pour vous donner le contexte, voici mon setup complet qui utilise ce blueprint :
| Pièce | Thermostat (contrôle) | Radiateur (chauffage) |
|---|---|---|
| Chambre | Bosch BTH-RM (Zigbee) | Intuis (Zigbee) |
| Salon | Aqara W100 (Zigbee) | NodOn fil pilote (Zigbee) |
| Autres pièces | Aqara W100 (Zigbee) | Intuis (Zigbee) |
Le blueprint présenté ici gère la chambre. Pour les Aqara W100, j'utilise un blueprint différent adapté à leurs spécificités (topics MQTT propriétaires, gestion du PMTSD). Si ça vous intéresse, je pourrai en faire un article dédié.
Conclusion : pour aller plus loin
Ce blueprint couvre le cas d'usage de base : synchronisation bidirectionnelle thermostat ↔ radiateur. Si vous souhaitez aller plus loin, voici quelques pistes que j'explore :
- Programmation horaire avec Schedy (AppDaemon) : planifier des plages de température différentes semaine/weekend, avec des overrides pour le télétravail ou les invités
- Détection fenêtre ouverte : couper le chauffage automatiquement quand le Bosch détecte une chute rapide de température (le
window_detectionest exposé dans les attributs Zigbee2MQTT) - Dashboard dédié : une carte Lovelace avec la température actuelle, la consigne, l'humidité et le niveau de batterie du Bosch
À noter : le Bosch BTH-RM supporte aussi le mode climatisation (cool) via occupied_cooling_setpoint et target_temp_high. Ce blueprint ne gère que le chauffage (heat / off). Si vous avez une clim réversible et souhaitez étendre le blueprint, le dépôt GitHub est ouvert aux contributions.
Avec l'obligation d'installer un thermostat programmable d'ici 2030, c'est le bon moment pour investir dans une solution domotique pérenne. Le code source du blueprint est disponible sur GitHub — les issues et pull requests sont les bienvenues. Et si vous cherchez à structurer votre stratégie domotique ou votre transformation digitale à plus grande échelle, échangeons.
