Loading...
E.G.

KNX Heating in Home Assistant: 21 Automations Become 6

November 11, 2026
Part 4 of 4Series: Moving a house to a new Home Assistant instanceAll parts
  1. 01Home Assistant Migration: Old and New in Parallel
  2. 02Home Assistant Packages Instead of Include Files
  3. 03Home Assistant Backups on S3-Compatible Storage, Not AWS
  4. 04KNX Heating in Home Assistant: 21 Automations Become 6
Table of Contents

The heating logic of my KNX house was 21 automations on the old Home Assistant instance. On the new instance it is 6. I moved it on 26 September as one step of a larger migration, from an instance on 2024.11.1 to a new one on 2026.8, and I kept a journal entry while doing it. During the move I changed what happens when a window is closed at night, and the next morning the two instances showed different operating modes for all 11 thermostats. The background on how the room controllers are driven is in my post on KNX thermostat preset modes.

What moved

The old file held three groups. 16 automations handled open windows in 8 rooms. 4 handled the night mode at 21:00 and the day mode at 05:00, including applying the mode to 11 thermostats. 1 automation re-sent the mode after a Home Assistant start, and it had been switched off since January. On the new instance the 16 are 1, the 4 stay 4, and the startup automation came along, still switched off. I first copied the night-mode helper unchanged and removed initial: off on 9 October.

All six are installed on the new instance but disabled with initial_state: false. They would switch the operating mode of the KNX room controllers on the bus, and the old instance is still doing that job, unchanged. I did not want two instances writing operating modes to the same controllers.

Two small things turned up while copying. One old entity id says day mode 06:00 while the automation runs at 05:00, and on the new instance the id matches the time. The old registry also still carries an orphaned entry for a temperature snapshot automation that was deleted long ago, shown as unavailable, and I left it on the old instance.

16 copies become one automation with a table

The 16 window automations were copies made in the UI. Each room had one named deactivate (window open for 5 seconds, set frost protection) and one named activate (window closed for 5 seconds, set comfort). Within each group of eight they differed only in the names of the window sensor and the thermostat. The idea itself is covered in my post on switching the heating off when a window opens, and it works. With 16 copies, even a change that only concerns closing a window has to be made 8 times.

The new version is one automation with two triggers, open and closed, told apart by a trigger id. The room-specific part is a table held in a variable that maps each window sensor to its thermostat, and the action looks up the thermostat by the entity that triggered. The room labels and entity ids below are placeholders.

automation:
  - id: heating_window_frost_protection
    alias: "Heating: window open -> frost protection, closed -> current mode"
    initial_state: false          # shadow mode during the migration
    mode: parallel
    variables:
      thermostat_by_window: &thermostat_by_window   # one line per room, 8 in the real file
        binary_sensor.window_room_a: climate.thermostat_room_a
        binary_sensor.window_room_b: climate.thermostat_room_b
        binary_sensor.window_room_c: climate.thermostat_room_c
    triggers:
      - trigger: state
        entity_id: &windows
          - binary_sensor.window_room_a
          - binary_sensor.window_room_b
          - binary_sensor.window_room_c
        from: "off"
        to: "on"
        for:
          seconds: 5
        id: open
      - trigger: state
        entity_id: *windows
        from: "on"
        to: "off"
        for:
          seconds: 5
        id: closed
    actions:
      - action: climate.set_preset_mode
        target:
          entity_id: "{{ thermostat_by_window[trigger.entity_id] }}"
        data:
          # BEHAVIOUR CHANGE: the old version always set comfort on closing
          preset_mode: >-
            {% if trigger.id == 'open' %}building_protection
            {% elif is_state('input_boolean.heating_night_mode', 'on') %}economy
            {% else %}comfort{% endif %}

The window list appears once in the file, and the second trigger reuses it through a YAML anchor, &windows and *windows. For a new room I add one line to the table, one to the window list and one to the list of thermostats. The preset that the KNX climate entity uses for frost protection is called building_protection.

The next excerpt continues the same automation list and shows the night automation as it is since 9 October. It reuses the window table through the anchor &thermostat_by_window and defines a second anchor, &all_thermostats, for the list of the 11 thermostats. From those two it computes the thermostats whose window is open and switches only the others. The day automation and the startup automation use the same variables block and are not shown.

  - id: heating_night_mode_activate
    alias: "Heating: apply night mode to all thermostats"
    initial_state: false
    triggers:
      - trigger: state
        entity_id: input_boolean.heating_night_mode
        from: "off"
        to: "on"
    variables:
      thermostat_by_window: *thermostat_by_window
      all_thermostats: &all_thermostats   # 11 entries in the real file
        - climate.thermostat_room_a
        - climate.thermostat_room_b
        - climate.thermostat_room_c
      window_open: >-
        {% set ns = namespace(l=[]) %}
        {% for window, thermostat in thermostat_by_window.items() %}
          {% if is_state(window, 'on') %}{% set ns.l = ns.l + [thermostat] %}{% endif %}
        {% endfor %}
        {{ ns.l }}
      targets: "{{ all_thermostats | reject('in', window_open) | list }}"
    actions:
      - if: "{{ targets | count > 0 }}"
        then:
          - action: climate.set_preset_mode
            data:
              preset_mode: economy
            target:
              entity_id: "{{ targets }}"

A bug that could happen every night

On the old instance, closing a window always set comfort, including at midnight. Anyone who aired a room at night put that room back on comfort, and the next scheduled mode change only came at 05:00. Each of the 8 activate copies had this behaviour.

The new automation sets the mode that currently applies when a window is closed, economy at night and comfort during the day. It is still switched off, like everything else from this step. That is the elif branch in the template above, which checks the night-mode helper. It is a deliberate behaviour change and there is a comment in the code saying so. I have no measurement of how often it happened or what it cost in heating energy. When I copied the automations, the scheduled switch at 21:00 and at 05:00 wrote the new mode to all 11 thermostats without looking at the windows, so a room with an open window left frost protection at that moment. The old instance does the same. On 9 October I changed it on the new one, as the excerpt above shows. The night, day and startup automations now skip thermostats whose window is open, and the window automation sets the current mode when the sensor goes from on to off and stays there for five seconds; a sensor that comes back from unknown or unavailable to off does not trigger it. I have checked the target list through the template API with one window simulated as open, which gave 10 of 11 thermostats. A real run is still missing because the automations are off. A window sensor that is unknown counts as closed, and three thermostats have no window sensor at all.

Missing operating mode feedback after a restart

The next morning I compared both instances. The old one showed all thermostats on comfort, the new one showed them on standby. The new instance had followed the change at 05:00 correctly by listening on the bus. After a restart at 07:19 it queried the operating mode and got standby back.

The cause is in the ETS project. My configuration has no readable feedback for the active operating mode. Home Assistant therefore queries the object that is used to switch the mode, and that object does not return the mode that is actually active. The old instance showed comfort only because it had written comfort itself. After its own restart the evening before it had shown standby as well, which was wrong too.

What the controllers do report back is the setpoint temperature. On that morning they reported 20.0 °C, which matches the comfort value; 18.0 °C would be the night value. That is only a hint at the mode: a later test showed that the reported setpoint also moves with a local adjustment at the controller, by −1 to −2 K in the room I tested (details in the thermostat post linked at the end). I have written about thermostats showing standby after a restart before, including the startup automation that works around it. On the old instance that automation has been switched off since January, and this comparison showed the symptom on both instances.

I mistook Z-BMU for a status object

The ETS export lists two objects per room controller. One is named BMU plus the room, for Betriebsmodus-Umschaltung, the operating mode switch-over. Next to it is one named Z-BMU plus the room, with the datapoint type for operating modes. I read the Z as Zustand, the German word for state, and the pattern held for all 11 rooms. It looked like the 11 status addresses I was missing.

Z stands for Zwangsführung, forced operation. The object is not readable. An earlier cleanup of this configuration had already removed exactly these addresses as wrong feedback, and that cleanup is documented in the repository. I caught it because I checked that documentation before entering the addresses again.

Abbreviations in an ETS project are vendor-specific. Before I enter any address as a status address now, I open the object in ETS and look at its flags. For the query after a restart I need an object that returns the active mode and answers read requests, and the object name does not tell me that.

What this means for the switch-over

After a restart both instances can show the wrong operating mode until they see a mode change on the bus or write one themselves. That makes the startup automation, disabled since January, more important than it looked. It had an inconsistency when I copied it: at night it set economy, while the night-mode automation set standby. On 9 October I settled that. Economy now applies at night in the window, night and startup automations. I also removed initial: off from the night-mode helper, because with it the helper was off after every restart and the startup automation would have set comfort at night. Now it restores its previous value, which can still be wrong if Home Assistant was down at the switching time.

The next step is in ETS. I still have to check there whether these controllers offer a readable object for the active operating mode. If they do, it gets its own group address, and that address goes into the Home Assistant configuration. I copied the automations before checking that. Until it is done the reported setpoint is the best hint I have, keeping in mind that a local adjustment at the controller shifts it too, as described in the post on base and active setpoint.

Frequently asked questions

Why does a KNX thermostat show standby in Home Assistant after a restart?

In my setup the query after a restart reads the object that switches the mode, and that returns standby even when comfort is active. I can only speak for this ETS project.

How do I check whether a KNX group address can be used as a status address?

Check in ETS whether the object behind the group address reports the active operating mode and has its read flag set. The object name is no evidence, because abbreviations are vendor-specific.

Is one automation with a mapping table better than one automation per room?

For my 8 rooms, yes. I change the window logic in one place instead of in 16 automations, and the price is a YAML file with a template instead of the UI.

Related articles

Questions or additions?

Comments run on GitHub Discussions, so you need a GitHub account to write one.