TRV Heads and Heating Zones

TRV Heads and Heating Zones

Module 19 · Lesson 5

TRV Heads and Heating Zones

In Lesson 4 you learned to read climate entities. Now you're going into TRV heads: heating zones with no installation rebuild, a temperature offset, an open window, and your first cautious automation driven by comfort_mode.

This lesson covers the Measurement + TRV heads route from the heating_integration_path helper. You're not controlling the boiler or heat pump here: only rooms, through TRV heads. If you have a Tado or Plugwise system with manufacturer zones, many of the same principles apply, but check your own integration's documentation for the offset and window-detection details.

Plan on about 105 to 115 minutes. From here on, I'll shorten Home Assistant to HA.

A reminder from Lesson 4
A climate entity on a TRV head describes that one head: not the whole boiler.
In this lesson, you can only turn on comfort_automation_enabled after building and manually testing the automation. This is the first point in the module where automation makes sense: but only for TRV heads, not the heat source.

A real-life problem: TRVs closed, the boiler still heating

You mounted five Zigbee TRV heads. In HA they all show a target temperature of 68°F and either an idle state or a closed valve. By the sofa in the living room it's 64°F, because the head measured 72°F at the radiator and closed early. Meanwhile, the boiler in the basement keeps running, because the weather compensation sees demand from another zone or is maintaining a minimum. Frustration: "the smart heads don't work."

They often do work: they just do their own part of the job. A TRV head limits water flow through one radiator. It doesn't send a "start the boiler" or "stop the boiler" signal, unless you have a separate bridging system (an optional bridge some manufacturers offer). In many houses that's normal: heads closed in individual rooms, while the heat source keeps running a bit longer for other plumbing and control reasons.

A second problem: you switch comfort_mode to Eco and expect an instant effect across the whole house. The heads respond with a delay, batteries report less often, and the radiators' thermal mass says "wait an hour." A third problem: a window's cracked open, and the head keeps holding 70°F at the radiator: you're wasting energy. You solve these three situations with a zone plan, an offset, an open-window response, and a cautious automation: not an aggressive loop running every few minutes.

What's actually the risk

Expecting boiler control: five heads are meant to "turn off" the gas boiler with no integration with the heat source.

Changing the temperature every few minutes: an automation reacts to every sensor tick and wears down valves and batteries.

One temperature for every room: the bedroom and bathroom get the same target as the living room.

Ignoring the offset: the head measures at the radiator, and you judge comfort at the sofa.

An open window with no response: the radiator runs full blast with the window cracked open.

A heating zone with a TRV head

In this lesson, a zone is practically one room (or one radiator) that you regulate with its own head. A zone has its own climate entity, its own room temperature sensor (if you have one), and its own target temperature. You don't have to build a "zone" in the plumbing sense right away: in HA, a logical room-by-room split is enough.

An example starting zone map:

Room TRV entity Room sensor Note
Living room climate.living_room_trv sensor.living_room_temperature An offset is likely
Bedroom climate.bedroom_trv sensor.bedroom_temperature Lower temp. in Night mode
Bathroom no head / fixed radiator sensor.bathroom_humidity Humidity: Lesson 8

Not every room needs a TRV head. Sometimes a radiator has no TRV at all (a bathroom, an entryway). Then the zone is just a measurement: and that's fine at this stage of the module too.

A heating zone note

# Heating zone
Room: ____
TRV climate entity: ____
Room sensor: ____
Window / contact sensor: ____

Head temperature with a stable room: ____
Room sensor temperature: ____
Difference: ____
Offset / correction: ____

Plan B without HA: ____
Does the head have manual control: yes / no / not sure
Does the head stay open, closed, or hold its last position during a failure: ____

TRV head, room sensor, and boiler: who's responsible for what

TRV head: regulates flow through the radiator in its own room. In HA: a climate entity.

A room sensor: measures temperature where you actually sit. In HA: sensor.*_temperature. Used for diagnostics and offsets, doesn't replace the head.

The boiler / heat pump / district heating: produces and delivers heat by its own logic. The TRVs usually don't turn it off.

TRVs closed, boiler heating: when that's OK
In an installation with radiators and the boiler's own controller, you often accept that the heat source runs for the whole loop, while the TRVs only limit individual rooms. The problem starts when you expect full boiler optimization purely from the heads: with no integration with the source. In that case, consider a zone thermostat, OpenTherm, or another route from Lesson 3.

Temperature offset and an external sensor

A TRV head measures temperature at the radiator: usually higher than in the middle of the room. If climate.living_room_trv reports a current_temperature of 72°F, and sensor.living_room_temperature shows 66°F, don't panic. That's a common setup. The question is: which signal are you using to judge comfort, and does the integration let you correct the reading?

Possible correction routes, depending on the model and integration (ZHA, Zigbee2MQTT, Z-Wave JS):

An on-device offset: a calibration parameter in the head's settings in HA or in Zigbee2MQTT (e.g. local_temperature_calibration).

An external sensor: some heads let you assign a separate temperature sensor as the measurement source. Check your model's documentation.

Correcting the target temperature: if you don't have an on-device offset, you can deliberately set a slightly higher target temperature on the head to achieve the comfort you want in the middle of the room. Note it in your zone record: this isn't a hack, it's practical calibration.

You set an offset patiently: compare readings over a few hours in a stable room, correct by 2 to 4°F, and observe again. Don't jump every hour: the head has thermal mass regardless.

An open window

Airing out a room with the radiator running is a pure waste of energy. Some heads have built-in detection of a sudden temperature drop (an "open window" feature). Other setups pair a window contact sensor (binary_sensor.living_room_window) with lowering the room's target temperature.

The rule in this module: the response to an open window is a gentle setback or a pause in heating for that zone, not aggressively shutting off the whole house. Check in Developer Tools whether your head exposes a window-detection attribute or diagnostic entity: naming varies by manufacturer.

Plan B with TRV heads
If HA is down, check whether you can set the temperature by hand on the head, or whether it has a manual mode. With a dead battery, behavior depends on the model and the valve mechanics: it might hold its last position, might show an error, and in the worst case the valve might not work correctly. Check the documentation and test one head before buying a full set.

Temperature helpers for comfort modes

The comfort_mode helper from Lesson 1 says which mode you want (Comfort, Eco, Night...). Now you're adding input_number helpers with specific temperatures: so the automation doesn't have hard-coded numbers buried in the YAML. Adjust the values to your own house.

Target temperatures per mode

input_number:
  comfort_temperature:
    name: "Temperature: Comfort"
    min: 16
    max: 26
    step: 0.5
    unit_of_measurement: "°C"
    mode: box
    icon: mdi:thermometer
    initial: 21

  eco_temperature:
    name: "Temperature: Eco"
    min: 14
    max: 24
    step: 0.5
    unit_of_measurement: "°C"
    mode: box
    icon: mdi:leaf
    initial: 19

  night_temperature:
    name: "Temperature: Night"
    min: 14
    max: 22
    step: 0.5
    unit_of_measurement: "°C"
    mode: box
    icon: mdi:weather-night
    initial: 18

  away_temperature:
    name: "Temperature: Away"
    min: 10
    max: 20
    step: 0.5
    unit_of_measurement: "°C"
    mode: box
    icon: mdi:car
    initial: 16

  guests_temperature:
    name: "Temperature: Guests"
    min: 16
    max: 26
    step: 0.5
    unit_of_measurement: "°C"
    mode: box
    icon: mdi:account-group
    initial: 22

These helpers are a starting version for one zone or a simple house. With several rooms, a per-room variant works better, e.g. living_room_comfort_temperature, bedroom_night_temperature. We're not expanding this right away, so your first automation stays easy to read.

The Summer mode in the script below turns off the TRV's climate entity (climate.turn_off): it doesn't leave the previous heating temperature in place. Ventilation and CO2 automations in later lessons can still run. One head is enough to start with, for one room. Add the rest of the zones once the first one is stable.

Your first automation: the TRV's temperature based on mode

We pull the temperature-setting logic into a script. You call it on a mode change, a temperature helper change, turning on the automation, a window state change, and after the head returns from unavailable. That way, closing the window restores the temperature that follows from the current mode: it doesn't leave Eco stuck permanently.

script:
  set_living_room_trv_temperature:
    alias: "Comfort - set the living room TRV temperature"
    mode: restart
    sequence:
      - condition: state
        entity_id: input_boolean.comfort_automation_enabled
        state: "on"

      - condition: template
        value_template: >
          {{ states(''climate.living_room_trv'')
             not in [''unknown'', ''unavailable''] }}

      - choose:
          - conditions:
              - condition: state
                entity_id: binary_sensor.living_room_window
                state: "on"
            sequence:
              - action: climate.set_temperature
                target:
                  entity_id: climate.living_room_trv
                data:
                  temperature: >
                    {{ states(''input_number.away_temperature'') | float(16) }}

          - conditions:
              - condition: state
                entity_id: input_select.comfort_mode
                state: "Comfort"
            sequence:
              - action: climate.set_temperature
                target:
                  entity_id: climate.living_room_trv
                data:
                  temperature: >
                    {{ states(''input_number.comfort_temperature'') | float(21) }}

          - conditions:
              - condition: state
                entity_id: input_select.comfort_mode
                state: "Eco"
            sequence:
              - action: climate.set_temperature
                target:
                  entity_id: climate.living_room_trv
                data:
                  temperature: >
                    {{ states(''input_number.eco_temperature'') | float(19) }}

          - conditions:
              - condition: state
                entity_id: input_select.comfort_mode
                state: "Night"
            sequence:
              - action: climate.set_temperature
                target:
                  entity_id: climate.living_room_trv
                data:
                  temperature: >
                    {{ states(''input_number.night_temperature'') | float(18) }}

          - conditions:
              - condition: state
                entity_id: input_select.comfort_mode
                state: "Away"
            sequence:
              - action: climate.set_temperature
                target:
                  entity_id: climate.living_room_trv
                data:
                  temperature: >
                    {{ states(''input_number.away_temperature'') | float(16) }}

          - conditions:
              - condition: state
                entity_id: input_select.comfort_mode
                state: "Guests"
            sequence:
              - action: climate.set_temperature
                target:
                  entity_id: climate.living_room_trv
                data:
                  temperature: >
                    {{ states(''input_number.guests_temperature'') | float(22) }}

          - conditions:
              - condition: state
                entity_id: input_select.comfort_mode
                state: "Summer"
            sequence:
              - action: climate.turn_off
                target:
                  entity_id: climate.living_room_trv

The script sets the temperature that follows from the current mode, or lowers it while the window is open. You don't store a "previous value": once the window closes, the script recalculates the setting from scratch. With the window open, we use the Away temperature (a safely low value); some heads have their own window mode: check the documentation. Eco can end up too high a setting while airing out a room.

alias: "Comfort - sync the living room TRV"
mode: restart

triggers:
  - trigger: state
    entity_id:
      - input_select.comfort_mode
      - input_number.comfort_temperature
      - input_number.eco_temperature
      - input_number.night_temperature
      - input_number.away_temperature
      - input_number.guests_temperature
      - input_boolean.comfort_automation_enabled
      - binary_sensor.living_room_window

conditions:
  - condition: state
    entity_id: input_boolean.comfort_automation_enabled
    state: "on"

actions:
  - action: script.set_living_room_trv_temperature
alias: "Comfort - living room TRV after returning from unavailable"
mode: single

triggers:
  - trigger: state
    entity_id: climate.living_room_trv
    from:
      - unavailable
      - unknown

conditions:
  - condition: state
    entity_id: input_boolean.comfort_automation_enabled
    state: "on"
  - condition: template
    value_template: >
      {{ states(''climate.living_room_trv'')
         not in [''unavailable'', ''unknown''] }}

actions:
  - action: script.set_living_room_trv_temperature

The trigger on the automation helper's change means that once you turn on comfort_automation_enabled, the script sets the head right away: you don't wait for the next mode change. The separate automation for returning from unavailable limits unnecessary commands (you're not listening for every single climate attribute change). If you don't have a window contact sensor, remove the window branch from the script and the binary_sensor.living_room_window trigger.

A zone dashboard: extending your diagnostics

Add a room card to the dashboard from Lesson 4: the TRV head, the room sensor, the temperature helpers, and the comfort mode, all side by side.

type: entities
title: Zone: living room
entities:
  - entity: climate.living_room_trv
    name: Living room TRV
  - entity: sensor.living_room_temperature
    name: Room temperature
  - entity: input_select.comfort_mode
    name: Comfort mode
  - entity: input_number.comfort_temperature
    name: Comfort target
  - entity: input_number.eco_temperature
    name: Eco target
  - entity: input_boolean.comfort_automation_enabled
    name: Comfort automation

How to test this lesson

1. For one head, note the difference between its current_temperature and the room sensor. Decide whether you need an offset.

2. Add the input_number temperature helpers and set values for your house.

3. Build the automation for one head. Test it with comfort_automation_enabled off, then on.

4. Switch comfort_mode to Eco and Night: watch how long the head and room take to react (minutes, not seconds).

5. Check your Plan B: a manual change on the head with HA turned off.

Common mistakes

An automation on every sensor change: the head gets a new target temperature every few minutes and burns through its battery.

The same temperature in the bedroom and living room: Night should be lower in the bedroom, don't copy one helper everywhere without thinking.

No automation switch: without comfort_automation_enabled, you have no fast emergency stop for HA's logic.

Expecting an instant effect on the boiler: heads regulate rooms, not always the heat source.

What not to do

Don't build a loop like "if sensor.living_room_temperature < X, set climate" with no hysteresis and no long for: that's material for later, more mature scenarios.

Don't copy the automation onto five heads before the first one has run stably for several days.

Don't try to control the boiler from TRV heads: there are other routes for that from Lesson 3 (Lessons 6 and 7).

Practical assignment

☐ Sketch a zone map: room → TRV head → room sensor (if you have one).

☐ For one head, record the temperature difference between the radiator and the room, and set an offset or a deliberate correction.

☐ Add the mode-temperature input_number helpers.

☐ Build and test the climate.set_temperature automation for one head, driven by comfort_mode.

☐ Extend your dashboard with a living-room zone card (or another room with a TRV head).

☐ Note whether your TRVs close while the boiler keeps heating: and whether you accept that, or look for a different integration.

Key takeaways

A TRV zone is a room with its own head: not the whole house, and not the boiler.

An offset and a room sensor resolve the "at the radiator" versus "at the sofa" gap.

The automation reacts to the mode and helpers, not to every single temperature reading.

comfort_automation_enabled is your circuit breaker: turn it on only after testing.

What's next

In Lesson 6 we'll go into Generic Thermostat and on/off control through a relay: with hysteresis, min_cycle_duration, and a low-risk test load, not straight onto the main gas boiler.

If you understand TRV zones and have your first cautious automation, you're no longer treating climate like a magic remote for the boiler: just like an interface to a specific room with specific limits.

Finished this lesson?