Thermostat Zigbee Bosch BTH-RM : blueprint Home Assistant pour piloter votre chauffage

Guide complet pour intégrer le thermostat Zigbee Bosch BTH-RM dans Home Assistant via Zigbee2MQTT. Blueprint YAML prêt à l'emploi, pièges à éviter et obligation thermostat 2027.

Thermostat Bosch BTH-RM blanc fixé sur un mur beige affichant 20,5°C avec sa molette rotative et sa barre LED bleue, en arrière-plan un salon avec un fauteuil en velours terracotta, un lampadaire doré et un parquet en chevrons
Le thermostat Bosch BTH-RM, une solution Zigbee locale pour piloter votre chauffage via Home Assistant
toc Sommaire
Résumer avec l'IA

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é climate dans 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.

  1. Activez le mode appairage dans Zigbee2MQTT (bouton "Autoriser la connexion" ou permit_join: true)
  2. Sur le thermostat Bosch, appuyez simultanément sur les boutons + et - pendant 3 à 5 secondes
  3. Le symbole Zigbee clignote sur l'écran : le thermostat cherche le réseau
  4. 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 :

Open your Home Assistant instance and show the blueprint import dialog with this blueprint pre-filled.

Méthode manuelle :

  1. Téléchargez le fichier YAML depuis le dépôt GitHub
  2. Copiez-le dans votre dossier config/blueprints/automation/custom/
  3. Rechargez les automations dans Home Assistant
  4. Créez une nouvelle automation basée sur ce blueprint
  5. 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_detection est 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.

Romain Delfosse
Romain Delfosse Digital Governance & Platform Strategy