
Introduction
NOTE: I am in the United Kingdom – heating systems and boilers may operate diferently in other countries.

I’ve been using Hive for over a decade, and while it was impressive when it launched, it’s no longer ideal for a larger home by today’s standards, and has the following key limitations:
- It treats the entire house as a single heating zone.
- It cannot detect when one room is colder than the others or heat that room independently.
- It has no zone scheduling to heat different areas of the home at different times.
- It offers no way to set individual rooms to their own target temperatures.
These shortcomings became the core requirements for the new heating system I’ve now successfully built in Home Assistant.
After discovering Shelly’s BLU TRVs (thermostatic radiator valves), I decided to install them on most of my radiators and integrate the heating control into Home Assistant.
History
From the 1950s to the early 2000s, most homes used a single wall thermostat and manually balanced radiators, an approach that treated the whole house as one thermal zone.
TRVs became common from the late 1970s and lockshield balancing matured through the 1980s–1990s, giving rooms basic individual control, but the system still relied on one sensor whose placement, drafts, sunlight, or poor balancing could easily destabilise the whole house.
As homes changed – with new insulation, windows, and radiator upgrades – these systems often needed rebalancing and still wasted energy by overheating some rooms while under‑heating others. Platforms like Hive modernised the single‑zone model with digital scheduling and remote control, but didn’t change how heat was distributed.
Today’s expectations of per‑room control, multiple sensors, proper balancing, and smarter logic go far beyond what the old single‑thermostat architecture was ever designed to handle.
Shelly BLU TRVs
I chose Shelly BLU TRVs (thermostatic radiator valves) because the rest of my home automation landscape is already Shelly, and their ecosystem aligns with my preferences while integrating cleanly with Home Assistant.

They are excellent for giving Home Assistant control over individual radiators, but they do come with a few drawbacks:
- They rely on Bluetooth to talk to a Wi‑Fi gateway, which makes them vulnerable to interference and missed messages.
- They use alkaline batteries, so they need periodic replacements.
- They assume a proportional elationship between valve opening and heating performance, rendering PID valve modulation ineffective.
- They don’t communicate with each other and rely on an external controller, such as Home Assistant, to coordinate their behaviour.
However, their core capability is fundamental to this project as it lets me control the heat going to each individual radiator and get accurate temperature feedback from that specific room. This functionality forms the foundation of my requirements for the new heating system.
The TRV has two modes of operation: it can operate as a TRV with PID control of the valve according to it’s internal or an external temperature sensor, or as a simple remote control valve.
Heating System Theory
In a TRV review video I watched, a heating engineer highlighted several critical factors that have to be considered when designing effective heating control, which prompted further research.
Boiler Operation
In the UK, most homes are heated using gas boilers, which supply hot water to radiators throughout the house. These boilers are typically controlled by a relay (as with Hive), switching the boiler on when heat is demanded.
Modern boilers modulate their burner output to maintain a stable flow temperature, but they still require a minimum water flow rate to operate correctly.
Two key conditions whle operating the boiler must be avoided, dead-heading and short-cycling:
Dead-Heading
When the boiler fires, the pump circulates water through the radiator circuit. It’s therefore essential that not all radiators are shut off at the same time. If the pump has nowhere to push the water, the system becomes “dead‑headed,” which can damage the pump, cause overheating, and trigger boiler lockouts.
To prevent dead‑heading, at least one radiator (often a towel rail) is usually left without a TRV so it always remains open.
Short-Cycling
Short‑cycling is harmful because it forces the boiler to switch on and off repeatedly, wasting energy, increasing wear on components, and reducing the overall lifespan of the system.
Short cycling wastes energy, because the system’s thermal inertia prevents the boiler and radiators from reaching efficient operating temperature before shutting off.
Radiator Operation
Radiators have a fixed maximum heat output. They heat up until they reach saturation, the point where the entire radiator surface is fully hot and able to sustain that maximum heat output.
Radiators require a certain minimum flow‑rate to reach saturation, and this threshold is usually well below the maximum possible flow through the valve. Increasing flow‑rate reduces the time it takes for the radiator to reach saturation, but once saturated, any flow above the ‘holding’ level does not increase the radiator’s heat output
Radiator Valves
Radiators have two valves: the TRV valve and the lockshield valve. It is important to note that most TRV valves are non‑linear, and operates similarly to an old‑fashioned globe‑type water tap: opening the valve a small amount allows a large amount of water through, while opening it the rest of the way only increases flow by a minimal amount.

The lockshield valve is used to balance the system by controlling the flow rate through each radiator. Because some radiators are closer to the boiler, and some are on upper floors, each one experiences different ‘resistance’ to water flow. When the boiler pump circulates hot water around the system, certain radiators will naturally receive more flow than others. Adjusting the lockshield valves ensure that every radiator receives an appropriate share of hot water.
In a per‑room heating configuration, the lockshield valve can be used to slow a radiator’s warm‑up rate so it better matches the room’s thermal response and reduces temperature overshoot.
The chart below shows how quickly the study heats up when the boiler fires. Although the study is better insulated than the rest of the house (see the Garage Conversion project), its radiator is by far the closest to the boiler and therefore receives the hottest water.
The study target temperature is set to 21.5°C as shown by the red line:

You may notice that the study cools faster than the other rooms, even though it’s better insulated. This happens because it starts at a higher temperature, so it loses heat more rapidly. With the door open to the dining room and the rest of the house, additional heat also escapes into those cooler spaces.
Why Radiator Physics Make PID Control Ineffective

Radiators behave nothing like the smooth, proportional actuators that PID control expects. A typical UK radiator saturates at low valve openings (often around 20–30% travel), meaning it becomes fully hot and delivers its maximum heat output long before the valve is anywhere near fully open. Below this point, flow is too low for predictable heat delivery; above it, extra valve opening provides no additional heat. This creates a highly non‑linear, almost binary response: either the radiator is cold, or it is saturated.
For PID to work at all, the PID output would need to be mapped to valve position in a strongly inverse, non‑linear way: when the room is cold, the valve must open fully to drive the radiator rapidly to saturation; but as the room approaches target temperature, the valve position must collapse into a very narrow low‑flow band to avoid overshoot. In practice, this “realistic modulation zone” is so small and unpredictable that PID cannot meaningfully modulate heat output. Even a seemingly modest valve opening (e.g., 20–25%) can still saturate the radiator and cause overshoot when the room is close to it’s target temperature.
Because radiators deliver almost all their useful heat in the saturated state and provide no stable proportional response, simple on/off control with hysteresis is usually far more effective than PID for maintaining stable room temperatures.
Control Delegation
When a Shelly BLU TRV operates in its normal thermostatic mode, it decides for itself when to open or close the valve based on the temperature it senses. However, if you use it purely as a motorised valve, the TRV no longer makes those decisions. In that mode, Home Assistant becomes responsible for determining when the valve should open or close, using its own temperature readings and automation logic.

If Home Assistant takes over control of the valve, you need strong safeguards and reliable automations to keep the system within safe operating limits. Because Bluetooth becomes part of the control loop and isn’t always dependable, the setup also requires additional guardrails to ensure Home Assistant maintains a stable connection to the TRV.
Control Strategy
I have implemented a control system in Home Assistant where each room reports it’s temperature, which is compared against its target, and if the temperature has dropped below it’s pre-configured offset value, it demands heat.
When any room demands heat, the boiler is switched on, and when all rooms no longer demand heat the boiler is switched off.
The choice of where to place radiator valve control authority must balance the reliability of local, but inefficient TRV‑based PID control, against the accuracy of remote control via Home Assistant, which depends on a less reliable Bluetooth and WiFi connection.
Control Ownership
I am trialling a balanced approach in which the TRV retains full authority over valve actuation, while I externally set its target temperature. Room temperature is measured using a Shelly H&T sensor, which provides a more representative reading of the space than the TRV’s onboard sensor. Accuracy depends on placement, so sensor position must balance responsiveness with a realistic measurement of the room environment.
System Flow Protection
To prevent pump dead‑heading, two radiators remain permanently open: the bathroom towel rail and the lounge radiator, which cannot accept a TRV. These provide a guaranteed minimum flow path regardless of individual room demand
Temperature Hysteresis
The TRV’s target temperature is set via Home Assistant, and boiler firing is controlled using the following logic:
- Turn ON: If ANY rooms target temperature falls to more than 0.5°C below the TRV target temperature
- Turn OFF: If ALL rooms temperatures are above their TRV target temperatures
This creates a global 0.5 °C hysteresis band, implemented using a numeric helper for easy adjustment. A secondary, natural hysteresis arises from the delay between the radiator warming the room and the Shelly H&T registering that rise. Sensor placement can influence this effect, but must not compromise accurate room‑level measurement.
This strategy inevitably introduces some overshoot and undershoot, but these can be moderated through lockshield balancing, the hysteresis band, and the boiler’s flow‑temperature setting.
The Grafana chart below shows the hysteresis, and considering humans respond more to the presence or absence of a heat source than to small temperature changes, the 1-2 °C itself is not noticeable. What is noticeable is if the radiator is on or not. Because radiant heat dominates perception, you may feel warmer in a room that is actually 1 °C colder if the radiator is on.
This chart highlights the inefficient use of battery energy and excessive noise and wear and tear created by the PID controller:

Given most of the flow occurs around 30% of the valve opening (assuming a non-linear valve), all of the activity above that is wasted.
Short-Cycling Prevention
With a control strategy that allows each room to request heat independently, it’s possible for one room to stop demanding heat only for another to request it moments later. To prevent rapid cycling of the boiler in these situations, I have implemented a boiler lockout timer. While the timer is active, the boiler cannot be switched on or off, and any request to change state activates the timer.
TRV Installation & Configuration
I have a Raspberry PI Server with Home Asssitant, which I use for Home Automation. All I had to do therefore was install the TRVs and add them to Home Assistant.
This isn’t as straightforward as adding Wi‑Fi relays or other networked devices. The TRVs communicate over Bluetooth and rely on dedicated Wi‑Fi gateways. While a single gateway can handle multiple TRVs, most rooms only have one radiator, and for reliable Bluetooth reception each room ideally needs its own gateway.
Configuration is done through the web interface exposed by the gateway’s Wi‑Fi access‑point connection. I use static IP addresses outside of my DHCP range for all of my home automation devices. Then as standard on Shelly devices, update the firmware and modify the settings, device name etc.
After you have the gateway configured, you have to mount the TRVs and remove the shipping battery tab, after which they will calibrate themselves. Pairing is best done via the web interface, and once paired you will have access to the TRV settings.
Once the gateway is on the network, Home Assistant discovers it, and if the TRVs have been paired, they’re detected and added alongside it.
The Shelly TRVs have built-in temperature sensors, but given that they are close to the radiator, I chose to add external sensors, which the gateway and TRV supports. This allows for more accurate room temperature monitoring, although it could have drawbacks depending on how you plan to control the TRVs. A built-in sensor will react quicker to the change in temperature, as opposed to a sensor across the room.

Automation in Home Assistant
This is the difficult part. There are 3 things to consider:
- Determining when the boiler comes on based on the state of the TRVs and sensors
- Determining which rooms are heated and when
- Implementing fail-safe features to prevent short-cyclng and dead-heading and communication failure
Boiler Demand
As I iterated through the development and testing cycles, I modified the automation and added additional safeguards and edge‑case handling. Below are some of the issues I came up against:
- PID control of the valve does not translate to an effective indicator of heat demand
- Sensor states such as ‘unknown’ or ‘unavailable’, often caused by restarts or communication dropouts, prevent reliable temperature comparison.
- Missed triggers due to ‘unknown’ or ‘unavailable’ states
- Boiler short-cycling
- Boiler not turning off after a reasonable period
My initial idea was to use the TRV state to determine when the boiler should turn on or off. The problem is that the TRV’s PID control created false demand. For example, once a room reaches its target temperature, the PID loop might close the valve to around 3%. Technically the TRV is still ‘calling for heat’, so it keeps the boiler running, but with the valve almost closed the radiator barely heats at all.
I ultimately chose to ignore the PID behaviour and valve position, and instead base boiler demand solely on the actual and target temperatures. This approach removes PID behaviour from the boiler control equation entirely.
To prevent short‑cycling, I introduced a hysteresis loop by applying an offset to the room temperature, so the boiler only fires once it falls a defined amount below the target. To do this I created a template helper that effectively reduces the room temperatures by an amount defined by a numeric helper:
{% set sbht = states('sensor.main_bedroom_sbht_temperature') %}
{% set offset = states('input_number.current_temperature_hysteresis_offset') | float %}
{% if sbht not in ['unknown', 'unavailable', None] %}
{{ sbht | float + offset }}
{% else %}
999
{% endif %}
In this template, I return the room temperature with the hysteresis offset applied, typically around –0.5 °C. If the temperature sensor reports an ‘unknown’, ‘unavailable’, or ‘none’ state, which often happens after a Home Assistant restart or when the device briefly loses connection, the template outputs a value of 999. This ensures that when the actual temperature cannot be trusted, the automation will not trigger the boiler, because 999 acts as a deliberate ‘impossible’ fail‑safe value.
This value is then compared to the TRV target temperature. Since the target temperature can also be reported as ‘unavailable’ or ‘unknown’, I convert it to zero in those cases. This means that in every possible permutation of the comparison, if any value is not known for any reason, the automation will not trigger the boiler as a result of this check.
I ended up consolidating everything into a single automation for boiler demand. There’s enough overlap and shared triggers that keeping it all together made the most sense.
alias: ♨️ Heating Demand Supervisor
description: ""
triggers:
- trigger: state
entity_id:
- timer.boiler_lockout_timer
to:
- idle
id: Boiler Lockout Timer
- trigger: time_pattern
minutes: /5
id: Scheduled Time
- trigger: numeric_state
entity_id:
- sensor.current_sbht_temperature_offset_dining_room
below: sensor.trv_target_temperature_numeric_dining_room
id: Dining Room Temperature (Dining Room TRV)
- trigger: numeric_state
entity_id:
- sensor.current_sbht_temperature_offset_dining_room
below: sensor.trv_target_temperature_numeric_kitchen
id: Dining Room Temperature (Kitchen TRV)
- trigger: numeric_state
entity_id:
- sensor.current_sbht_temperature_offset_dining_room
below: sensor.trv_target_temperature_numeric_piano_room
id: Dining Room Temperature (Piano Room TRV)
- trigger: numeric_state
entity_id:
- sensor.current_sbht_temperature_offset_study
below: sensor.trv_target_temperature_numeric_study
id: Study Temperature
- trigger: numeric_state
entity_id:
- sensor.current_sbht_temperature_offset_main_bedroom
below: sensor.trv_target_temperature_numeric_main_bedroom
id: Main Bedroom Temperature
- trigger: numeric_state
entity_id:
- sensor.current_sbht_temperature_offset_child2s_bedroom
below: sensor.trv_current_temperature_numeric_child2s_bedroom
id: Child 2's Bedroom Temperature
- trigger: numeric_state
entity_id:
- sensor.current_sbht_temperature_offset_exercise_room
below: sensor.trv_current_temperature_numeric_exercise_room
id: Child 2's Bedroom Temperature
- trigger: state
entity_id:
- timer.boost_timer_dining_room
to:
- active
id: Boost Timer - Dining Room
- trigger: state
entity_id:
- timer.boost_timer_study
to:
- active
id: Boost Timer - Study
- trigger: state
entity_id:
- timer.boost_timer_main_bedroom
to:
- active
id: Boost Timer - Main Bedroom
- trigger: state
entity_id:
- timer.boost_timer_child2s_bedroom
to:
- active
id: Boost Timer - Child 2's Bedroom
- trigger: state
entity_id:
- timer.boost_timer_exercise_room
to:
- active
id: Boost Timer - Exercise Room
conditions:
- condition: state
entity_id: timer.boiler_lockout_timer
state:
- idle
actions:
- choose:
- conditions:
- condition: state
entity_id: switch.boiler_control_relay
state:
- "off"
- condition: or
conditions:
- condition: numeric_state
entity_id: sensor.current_sbht_temperature_offset_dining_room
below: sensor.trv_target_temperature_numeric_dining_room
- condition: numeric_state
entity_id: sensor.current_sbht_temperature_offset_dining_room
below: sensor.trv_target_temperature_numeric_kitchen
- condition: numeric_state
entity_id: sensor.current_sbht_temperature_offset_dining_room
below: sensor.trv_target_temperature_numeric_piano_room
- condition: numeric_state
entity_id: sensor.current_sbht_temperature_offset_study
below: sensor.trv_target_temperature_numeric_study
- condition: numeric_state
entity_id: sensor.current_sbht_temperature_offset_main_bedroom
below: sensor.trv_target_temperature_numeric_main_bedroom
- condition: numeric_state
entity_id: sensor.current_sbht_temperature_offset_child2s_bedroom
below: sensor.trv_target_temperature_numeric_child2s_bedroom
- condition: numeric_state
entity_id: sensor.current_sbht_temperature_offset_exercise_room
below: sensor.trv_target_temperature_numeric_exercise_room
- condition: state
entity_id: timer.boost_timer_dining_room
state:
- active
- condition: state
entity_id: timer.boost_timer_study
state:
- active
- condition: state
entity_id: timer.boost_timer_main_bedroom
state:
- active
- condition: state
entity_id: timer.boost_timer_child2s_bedroom
state:
- active
- condition: state
entity_id: timer.boost_timer_exercise_room
state:
- active
alias: >-
If any SBHT offset temperature is below the TRV target temperature
or any Boost timer is active
- condition: state
entity_id: input_boolean.master_heating_switch
state:
- "on"
sequence:
- action: timer.start
metadata: {}
target:
entity_id: timer.boiler_lockout_timer
data: {}
- action: switch.turn_on
metadata: {}
data: {}
enabled: true
target:
entity_id: switch.boiler_control_relay
alias: Turn on Boiler Control Relay
- action: notify.signal_log
data:
message: >-
{% set rooms = [
{
'name': 'Dining Room',
'below': states('sensor.current_sbht_temperature_offset_dining_room') | float
< states('sensor.trv_target_temperature_numeric_dining_room') | float
},
{
'name': 'Kitchen',
'below': states('sensor.current_sbht_temperature_offset_dining_room') | float
< states('sensor.trv_target_temperature_numeric_kitchen') | float
},
{
'name': 'Piano Room',
'below': states('sensor.current_sbht_temperature_offset_dining_room') | float
< states('sensor.trv_target_temperature_numeric_piano_room') | float
},
{
'name': 'Study',
'below': states('sensor.current_sbht_temperature_offset_study') | float
< states('sensor.trv_target_temperature_numeric_study') | float
},
{
'name': 'Main Bedroom',
'below': states('sensor.current_sbht_temperature_offset_main_bedroom') | float
< states('sensor.trv_target_temperature_numeric_main_bedroom') | float
},
{
'name': "Child 2's Bedroom",
'below': states('sensor.current_sbht_temperature_offset_child2s_bedroom') | float
< states('sensor.trv_current_temperature_numeric_child2s_bedroom') | float
},
{
'name': 'Exercise Room',
'below': states('sensor.current_sbht_temperature_offset_exercise_room') | float
< states('sensor.trv_target_temperature_numeric_exercise_room') | float
},
{
'name': 'Dining Room (Boost)',
'below': is_state('timer.boost_timer_dining_room', 'active')
},
{
'name': 'Study (Boost)',
'below': is_state('timer.boost_timer_study', 'active')
},
{
'name': 'Main Bedroom (Boost)',
'below': is_state('timer.boost_timer_main_bedroom', 'active')
},
{
'name': "Child 2's Bedroom (Boost)",
'below': is_state('timer.boost_timer_child2s_bedroom', 'active')
},
{
'name': 'Exercise Room (Boost)',
'below': is_state('timer.boost_timer_exercise_room', 'active')
}
] %}
{% set below = rooms | selectattr('below') |
map(attribute='name') | list %}
♨️ {{ now().strftime('%Y-%m-%d %H:%M:%S') }}
Heating Demand Supervisor --> Turning Boiler ON
- Room(s) demanding heat (Offset below target or Boost): {{
below | join(', ') if below | length > 0 else 'None' }}
- Triggered by: {{ trigger.id }}
alias: If Boiler is OFF and one or more TRVs demand heat, turn Boiler ON
- conditions:
- condition: state
entity_id: switch.boiler_control_relay
state:
- "on"
- condition: and
conditions:
- condition: numeric_state
entity_id: sensor.current_sbht_temperature_dining_room
above: sensor.trv_target_temperature_numeric_dining_room
- condition: numeric_state
entity_id: sensor.current_sbht_temperature_dining_room
above: sensor.trv_target_temperature_numeric_kitchen
- condition: numeric_state
entity_id: sensor.current_sbht_temperature_dining_room
above: sensor.trv_target_temperature_numeric_piano_room
- condition: numeric_state
entity_id: sensor.current_sbht_temperature_study
above: sensor.trv_target_temperature_numeric_study
- condition: numeric_state
entity_id: sensor.current_sbht_temperature_main_bedroom
above: sensor.trv_target_temperature_numeric_main_bedroom
- condition: numeric_state
entity_id: sensor.current_sbht_temperature_child2s_bedroom
above: sensor.trv_target_temperature_numeric_child2s_bedroom
- condition: numeric_state
entity_id: sensor.current_sbht_temperature_exercise_room
above: sensor.trv_target_temperature_numeric_exercise_room
- condition: state
entity_id: timer.boost_timer_dining_room
state:
- idle
- condition: state
entity_id: timer.boost_timer_study
state:
- idle
- condition: state
entity_id: timer.boost_timer_main_bedroom
state:
- idle
- condition: state
entity_id: timer.boost_timer_child2s_bedroom
state:
- idle
- condition: state
entity_id: timer.boost_timer_exercise_room
state:
- idle
alias: >-
If all SBHT temperatures are above the TRV target temperatures and
all Boost timers are idle
sequence:
- action: timer.start
metadata: {}
target:
entity_id: timer.boiler_lockout_timer
data: {}
- action: switch.turn_off
metadata: {}
data: {}
enabled: true
target:
entity_id: switch.boiler_control_relay
alias: Turn off Boiler Control Relay
- action: notify.signal_log
data:
message: >-
{% set rooms = [
{
'name': 'Dining Room',
'below': states('sensor.current_sbht_temperature_dining_room') | float
< states('sensor.trv_target_temperature_numeric_dining_room') | float
},
{
'name': 'Kitchen',
'below': states('sensor.current_sbht_temperature_dining_room') | float
< states('sensor.trv_target_temperature_numeric_kitchen') | float
},
{
'name': 'Piano Room',
'below': states('sensor.current_sbht_temperature_dining_room') | float
< states('sensor.trv_target_temperature_numeric_piano_room') | float
},
{
'name': 'Study',
'below': states('sensor.current_sbht_temperature_study') | float
< states('sensor.trv_target_temperature_numeric_study') | float
},
{
'name': 'Main Bedroom',
'below': states('sensor.current_sbht_temperature_main_bedroom') | float
< states('sensor.trv_target_temperature_numeric_main_bedroom') | float
},
{
'name': "Child 2's Bedroom",
'below': states('sensor.current_sbht_temperature_child2s_bedroom') | float
< states('sensor.trv_current_temperature_numeric_child2s_bedroom') | float
},
{
'name': 'Exercise Room',
'below': states('sensor.current_sbht_temperature_exercise_room') | float
< states('sensor.trv_target_temperature_numeric_exercise_room') | float
},
{
'name': 'Dining Room (Boost)',
'below': is_state('timer.boost_timer_dining_room', 'active')
},
{
'name': 'Study (Boost)',
'below': is_state('timer.boost_timer_study', 'active')
},
{
'name': 'Main Bedroom (Boost)',
'below': is_state('timer.boost_timer_main_bedroom', 'active')
},
{
'name': "Child 2's Bedroom (Boost)",
'below': is_state('timer.boost_timer_child2s_bedroom', 'active')
},
{
'name': 'Exercise Room (Boost)',
'below': is_state('timer.boost_timer_exercise_room', 'active')
}
] %}
{% set below = rooms | selectattr('below') |
map(attribute='name') | list %}
❄️ {{ now().strftime('%Y-%m-%d %H:%M:%S') }}
Heating Demand Supervisor --> Turning Boiler OFF
- Room(s) demanding heat (Current below target or Boost): {{
below | join(', ') if below | length > 0 else 'None' }}
- Triggered by: {{ trigger.id }}
alias: If Boiler is ON and all rooms are above target, turn Boiler OFF
default:
- action: notify.signal_debug
data:
message: >-
{% set rooms = [
{
'name': 'Dining Room',
'below': states('sensor.current_sbht_temperature_dining_room') | float
< states('sensor.trv_target_temperature_numeric_dining_room') | float
},
{
'name': 'Kitchen',
'below': states('sensor.current_sbht_temperature_dining_room') | float
< states('sensor.trv_target_temperature_numeric_kitchen') | float
},
{
'name': 'Piano Room',
'below': states('sensor.current_sbht_temperature_dining_room') | float
< states('sensor.trv_target_temperature_numeric_piano_room') | float
},
{
'name': 'Study',
'below': states('sensor.current_sbht_temperature_study') | float
< states('sensor.trv_target_temperature_numeric_study') | float
},
{
'name': 'Main Bedroom',
'below': states('sensor.current_sbht_temperature_main_bedroom') | float
< states('sensor.trv_target_temperature_numeric_main_bedroom') | float
},
{
'name': "Child 2's Bedroom",
'below': states('sensor.current_sbht_temperature_child2s_bedroom') | float
< states('sensor.trv_current_temperature_numeric_child2s_bedroom') | float
},
{
'name': 'Exercise Room',
'below': states('sensor.current_sbht_temperature_exercise_room') | float
< states('sensor.trv_target_temperature_numeric_exercise_room') | float
},
{
'name': 'Dining Room (Boost)',
'below': is_state('timer.boost_timer_dining_room', 'active')
},
{
'name': 'Study (Boost)',
'below': is_state('timer.boost_timer_study', 'active')
},
{
'name': 'Main Bedroom (Boost)',
'below': is_state('timer.boost_timer_main_bedroom', 'active')
},
{
'name': "Child 2's Bedroom (Boost)",
'below': is_state('timer.boost_timer_child2s_bedroom', 'active')
},
{
'name': 'Exercise Room (Boost)',
'below': is_state('timer.boost_timer_exercise_room', 'active')
}
] %}
{% set below = rooms | selectattr('below') | map(attribute='name') |
list %}
♨️❄️ {{ now().strftime('%Y-%m-%d %H:%M:%S') }}
Heating Demand Supervisor --> No Action
- Room(s) demanding heat (Current below target or Boost): {{ below |
join(', ') if below | length > 0 else 'None' }}
- Triggered by: {{ trigger.id }}
enabled: false
mode: single
The implementation of the TRV in Home Assistant (as of January 2026) is not complete, and is missing some useful functionality.
Boost
rest_command:
# Dining Room
trv_boost_dining_room:
url: "http://192.168.1.XXX/rpc/BluTrv.Call?id=200&method=TRV.SetBoost¶ms={\"id\":0,\"duration\":{{ states('input_number.boost_time_dining_room') | int * 60 }}}"
method: GET
trv_cancel_boost_dining_room:
url: "http://192.168.1.XX/rpc/BluTrv.Call?id=200&method=TRV.SetBoost¶ms={\"id\":0,\"duration\":0}"
method: GET
TRV Status fetcher
sensor:
# Fetchers for TRV Status
- platform: rest
name: "TRV Status - Dining Room"
unique_id: trv_status_dining_room
resource: "http://192.168.1.XXX/rpc/BluTrv.Call?id=200&method=TRV.GetStatus¶ms={\"id\":0}"
method: GET
scan_interval: 10
value_template: "{{ value_json.id }}"
json_attributes:
- pos
- steps
- current_C
- target_C
- boost
- schedule_rev
- errors
Sensor values from fetcher status
template:
- sensor:
# Dining Room
- name: "TRV Current Temperature - Dining Room"
unique_id: trv_current_temperature_dining_room
state: "{{ state_attr('sensor.trv_status_dining_room', 'current_C') }}"
unit_of_measurement: "°C"
- name: "TRV Current Temperature Numeric - Dining Room"
unique_id: trv_current_temperature_numeric_dining_room
state: "{{ state_attr('sensor.trv_status_dining_room', 'current_C') | float(999) }}"
unit_of_measurement: "°C"
- name: "TRV Target Temperature - Dining Room"
unique_id: trv_target_temperature_dining_room
state: "{{ state_attr('sensor.trv_status_dining_room', 'target_C') }}"
unit_of_measurement: "°C"
- name: "TRV Target Temperature Numeric - Dining Room"
unique_id: trv_target_temperature_numeric_dining_room
state: "{{ state_attr('sensor.trv_status_dining_room', 'target_C') | float(0) }}"
unit_of_measurement: "°C"
Obtain binary value for boost
template:
- binary_sensor:
- default_entity_id: binary_sensor.trv_boost_state_dining_room
name: "TRV Boost State – Dining Room"
unique_id: trv_boost_state_dining_room
state: "{{ state_attr('sensor.trv_status_dining_room', 'boost') is not none }}"
Create buttons for boosting
template:
- button:
# Dining Room
- name: "TRV Boost – Dining Room"
unique_id: trv_boost_dining_room_button
icon: mdi:fire
press:
- action: rest_command.trv_boost_dining_room
- name: "TRV Stop Boost – Dining Room"
unique_id: trv_stop_boost_dining_room_button
icon: mdi:fire-off
press:
- action: rest_command.trv_cancel_boost_dining_room
Leave a Reply