Hello everyone,
while I really appreciate the original post, with all the descriptions and implementations, after spending quite a bit of time over the last few weeks trying to get it working, I had to give up and switch to an alternative method.
Main part of the problem is the issue of adding 2°C to the thermostat to force the TRV to open (hysteresis), the TRV thermostat being in synchronization with the virtual thermostat provided by Better Thermostat kept desynchronizing and staying (the BT one) at +2°C without being reset.
Second problem is the one already pointed out by other users whereby the calibration still leaves the temperature read by the TRV flickering especially when the target temperature is changed forcing the TRV to do continuous open/close/open/close and either it remained in continuous heating or the room remained cold.
Therefore, I completely changed my approach and relied solely on degree of closing and degree of opening, forcing the actual state as I need despite the fact that the TRV could remain in idle according to the detected temperatures. I share what I have produced below, in case it may be useful to other users as a cue as an idea.
This allowed me to bypass both the problem of internal temperature calibration and the problem of hysteresis. Hysteresis is now configured with the “tolerance” parameter in Better Thermostat, so the internal implementation of the TRV is completely ignored.
First, I created a numerical representing the virtual degrees of valve opening, then going to act on the two opposing factors provided by the TRV
Code
input_number:
casa_p1_camera1_termostato_valve_opening_degree:
name: casa_p1_camera1_termostato_valve_opening_degree
unit_of_measurement: "%"
icon: mdi:valve-open
min: 0
max: 100
step: 1
Then with an automation I can align the actual values of the TRV according to what is defined on the virtual one
Code
- id: casa_p1_camera1_termostato_valve_opening_degree_on_change
alias: casa_p1_camera1_termostato_valve_opening_degree_on_change
description: Update the TRV valve opening degree based on the virtual opening degree
mode: single
# When the virtual opening degree changes and remains stable for a few seconds, update the TRV opening degree
trigger:
- platform: state
entity_id: input_number.casa_p1_camera1_termostato_valve_opening_degree
not_to:
- unknown
- unavailable
for: "00:00:03"
actions:
# Update the TRV valve opening degree based on the virtual opening degree
- service: number.set_value
target:
entity_id: number.casa_p1_camera1_radiatore_valvola_valve_opening_degree
data:
value: "{{ trigger.to_state.state | float }}"
# Update the TRV valve closing degree based on the virtual opening degree, using the opposite value
- service: number.set_value
target:
entity_id: number.casa_p1_camera1_radiatore_valvola_valve_closing_degree
data:
value: "{{ (100 - trigger.to_state.state | float) }}"
As you can see, I can open the valve by setting the “closing degree” to 0, so that if even the thermostat is idle I can control it.
Here is an example, the TRV is actually in IDLE state, the virtual valve is set to 100%, so to accomplish that, the closing degree is forced to 0%
Screenshot
The only downside here is that in the TRV display there is not “flame” icon, but a very little loss…
I’m still using the temperature calibration automation in order to keep track of the actual temperature, but in this scenario is useless for controlling the heating/idle status. May some little improvements but the main functionality is essentially the same
Code
- id: casa_p1_camera1_radiatore_valvola_true_temperature_calibration
alias: casa_p1_camera1_radiatore_valvola_true_temperature_calibration
description: Calibrate the temperature of the TRV "casa_p1_camera1_radiatore_valvola" based on actual room temperature provided by sensor "casa_p1_camera1_ambiente_temperature"
# The offset changes made by this automation changes the "current_temperature" attribute itself, so the automation trigger itself
# This mode allow the automation to perform one execution at a time (the A != B condition avoid infinite recursion)
mode: queued
triggers:
# Room temperature changed
- trigger: state
entity_id: sensor.casa_p1_camera1_ambiente_temperature
not_to:
- unknown
- unavailable
# TRV temperature changed, meaning that a calibration were made (recursion) or the TRV internal thermometer changed value, so the calibration is now wrong
- trigger: state
entity_id: climate.casa_p1_camera1_radiatore_valvola
attribute: current_temperature
not_to:
- unknown
- unavailable
# TRV target temperature changed, this should not be needed but I discovered that when the TRV temperature changes, the current_temperature attribute also changes, going a bit wild up and down
# With this additional trigger we can keep up with all the changes
- trigger: state
entity_id: climate.casa_p1_camera1_radiatore_valvola
attribute: temperature
not_to:
- unknown
- unavailable
conditions:
# No mode/action conditions here, true temperature adjustment are always done, even if heat system is off
- and:
# Check if the all involved sensors are available
- condition: template
value_template: >
{{
states('sensor.casa_p1_camera1_ambiente_temperature') | is_number
and
state_attr('climate.casa_p1_camera1_radiatore_valvola', 'current_temperature') | is_number
and
states('number.casa_p1_camera1_radiatore_valvola_local_temperature_calibration') | is_number
}}
# Check if the current TRV temperature is different than the true temperature
# This allow multiple executions to avoid infinite recursion
- condition: template
alias: "Check if the TRV current temperature is different than the true temperature"
# I'm using round(1) on both, as the external temperature sensors provides a 2-digits value while the TRV only provides a 1-digit value.
value_template: >
{{
state_attr('climate.casa_p1_camera1_radiatore_valvola', 'current_temperature') | float | round(1)
!=
states('sensor.casa_p1_camera1_ambiente_temperature') | float | round(1)
}}
actions:
- variables:
# Room temperature reading provided by the TRV
trvtemp: "{{ state_attr('climate.casa_p1_camera1_radiatore_valvola', 'current_temperature') | float }}"
# Room temperature reading provided by the external sensor
roomtemp: "{{ states('sensor.casa_p1_camera1_ambiente_temperature') | float }}"
# Current value of the temperature calibration (needed to apply an offset on the formula)
trvcalib: "{{ states('number.casa_p1_camera1_radiatore_valvola_local_temperature_calibration') | float }}"
- action: number.set_value
target:
entity_id: number.casa_p1_camera1_radiatore_valvola_local_temperature_calibration
data:
# Calculate the new value for the calibration, 1-digit value as the TRV supports up to 1-digit
value: "{{ (trvcalib - (trvtemp - roomtemp)) | round(1) }}"
Before moving to the automation that set the virtual valve to actuate the real TRV, I want to introduce an additional control I created, about the heat requirement.
Instead of always having the valve entirely opened or entirely closed, I copied the sensor from the link I added as comment in order to get a more precise value
Code
template:
- sensor:
# Use TPI algorithm in order to calculate the valve open percentage
# https://github.com/jmcollin78/versatile_thermostat/blob/077d2d4cc62ced9c40db1d83d72dac4ed4d95d90/documentation/en/algorithms.md#principle
# https://github.com/KartoffelToby/better_thermostat/issues/1358#issuecomment-2568857879
- name: casa_p1_camera1_termostato_heat_requirement
unique_id: casa_p1_camera1_termostato_heat_requirement
unit_of_measurement: "%"
availability: >
{{
state_attr('climate.casa_p1_camera1_termostato', 'temperature') | is_number
and
state_attr('climate.casa_p1_camera1_termostato', 'current_temperature') | is_number
and
states('sensor.weather_station_outdoor_temperature') | is_number
}}
icon: mdi:fire
# Default coef_int=0.6
# If reaching the target temperature is too slow, increase coef_int to provide more power to the heater,
# If reaching the target temperature is too fast and oscillations occur around the target, decrease coef_int to provide less power to the radiator.
# Default coef_ext=0.01
# If the target temperature is not reached after stabilization, increase coef_ext (the on_percent is too low),
# If the target temperature is exceeded after stabilization, decrease coef_ext (the on_percent is too high),
state: >
{{
(
min(
max(
(
0.6 * ( state_attr('climate.casa_p1_camera1_termostato', 'temperature') - state_attr('climate.casa_p1_camera1_termostato', 'current_temperature') )
+
0.05 * ( state_attr('climate.casa_p1_camera1_termostato', 'temperature') - (states('sensor.weather_station_outdoor_temperature') | float) )
),
0
),
1) * 100
) | round(0)
}}
When the Better Thermostat goes into heating mode, I have a first automation that applies the heat requirement from the formula so that the TRV is configured with given value
Code
# This automation entirely ignore all the TRV funcionalities and force the valve to open based on degrees parameters only
# This approach fixes 2 different problems:
# 1) The histereis problem of the TRV, I don't need to boost the temperature to force the valve to open, I can open the valve even if it's still in idle mode
# 2) I don't rely on the temperature sensor and the offset regulation with the external temperature
- id: casa_p1_camera1_termostato_on_action_heating
alias: casa_p1_camera1_termostato_on_action_heating
mode: single
description: When the virtual thermostat goes into heating mode, activate the TRV
triggers:
# This trigger is fired when I manually change the thermostat target temperature, in this case the heating requirement is calculate with the new target
# Wait a few second for the temperature to stabilize, if I'm quickly changing the thermostat temperature it avoids too many triggers
- trigger: state
entity_id: climate.casa_p1_camera1_termostato
attribute: temperature
not_to:
- unknown
- unavailable
for: "00:00:03"
# This is when the thermostat enter in heating mode by itself as natural change of the temperature, going below the target temperature, no user action
# Wait a few seconds for the heating state to stabilize, sometimes during manual changes it may be in heating for a few seconds and then back to idle again
- trigger: state
entity_id: climate.casa_p1_camera1_termostato
attribute: hvac_action
to: "heating"
for: "00:00:03"
conditions:
# The thermostat is currently in "heat" mode
- condition: state
alias: "Check if the Thermostat is in 'heat' mode"
entity_id: climate.casa_p1_camera1_termostato
state: "heat"
# The thermostat is currently in "heating" action (this is redundant for the second trigger, but needed for the first trigger)
- condition: state
alias: "Check if the Thermostat is in 'heat' mode"
entity_id: climate.casa_p1_camera1_termostato
attribute: hvac_action
state: "heating"
actions:
# Force the heat requirement entity to update, based on current target temperature and currente temperature
- service: homeassistant.update_entity
data:
entity_id:
- sensor.casa_p1_camera1_termostato_heat_requirement
# If the TRV is in heat mode, the opening degree is the heat requirement
- service: input_number.set_value
target:
entity_id: input_number.casa_p1_camera1_termostato_valve_opening_degree
data:
value: "{{ states('sensor.casa_p1_camera1_termostato_heat_requirement') | float }}"
And a second, opposite, automation that close the valve when the Better Thermostat goes into idle mode
Code
# This automation allow to close the TRV when the virtual thermostat goes into idle mode
# The opening degree is set to 0 and the closing degree to 100
# In this way I totally ignore the TRV temperature functionality and I force the TRV to close
- id: casa_p1_camera1_termostato_on_action_idle
alias: casa_p1_camera1_termostato_on_action_idle
mode: single
description: When the virtual thermostat goes into idle action, disable activate the TRV
triggers:
# This is when the thermostat enter in idle mode
# Wait a few seconds for the idle to stabilize, sometimes during manual changes it may be in idle for a few seconds and then back to heating again
- trigger: state
entity_id: climate.casa_p1_camera1_termostato
attribute: hvac_action
to: "idle"
for: "00:00:03"
actions:
# Entirely close the virtual TRV, no more heating needed
- service: input_number.set_value
target:
entity_id: input_number.casa_p1_camera1_termostato_valve_opening_degree
data:
value: "0"
You can see here that the Heat Requirement is calculated to be 90%, and while the TRV remains IDLE, the closing degree as 10% allow the valve to be opened at needed ratio
Screenshot
I have a very last automation, but it is secondary, that during heating mode if the heat requirements changes a lost compared to the initial value, it updates the value instead of waiting the next heating cycle
Code
# When the thermostat goes into heating mode, by default the valve opening degree is updated based on the heat requirement calculated by the TPI algorithm
# But because during heating the room temperature goes up and down, the heat requirement also changes so it should be updated accordingly
# To avoid too many adjustments (and battery drain) I wait for a change of at least a sufficient number of units in the heat requirement before updating the valve degree
- id: casa_p1_camera1_termostato_on_heat_requirement_change
alias: casa_p1_camera1_termostato_on_heat_requirement_change
mode: single
triggers:
# When the heat requirement value change on value
- trigger: state
entity_id: sensor.casa_p1_camera1_termostato_heat_requirement
not_to:
- unknown
- unavailable
conditions:
- and:
# Check if the current opening degree value is a number, otherwise the formula will fail
- condition: template
value_template: >
{{ states("input_number.casa_p1_camera1_termostato_valve_opening_degree") | is_number }}
# Check if the thermostat is in heat mode
- condition: state
alias: "Check if the Thermostat is in 'heat' mode"
entity_id: climate.casa_p1_camera1_termostato
state: "heat"
# Thermostat is currently in heating action
- condition: state
alias: "Check if the Thermostat is in 'heating' action"
entity_id: climate.casa_p1_camera1_termostato
attribute: hvac_action
state: "heating"
# Check if heat requirement changed compared to the current opening of the valve, for at least 10 units
# Using the "absolute" formula allow us to cover both positive and negative changes
- condition: template
value_template: >
{{
(
trigger.to_state.state | float
-
states("input_number.casa_p1_camera1_termostato_valve_opening_degree") | float
) | abs > 10
}}
actions:
# Update the valve opening degree based on the heat requirement
- service: input_number.set_value
target:
entity_id: input_number.casa_p1_camera1_termostato_valve_opening_degree
data:
value: "{{ trigger.to_state.state | float }}"
Once you get past the difficulty of seeing that the TRV is idle even though it is actually warming up, everything works for now