Perfect Heating Automation with Sonoff TRVZB

intresting reading!

@berkans: as i am running a heatpump i am seeking just a possibility to open or close the valve on my sonoff trv - no need for anything in between ON and OFF. i am going with the climate-entity to set it to OFF and HEAT … is this correct or can i set the valve-opening directly?

Hi,
I’m looking for the best way to regulate the temperature using this valve.
Currently, I use the valve in “Heating” mode. Node-RED sends the desired value with this parameter “occupied_heating_setpoint”, but the valve operates in a fully open or fully closed state, which is not very efficient.

Has anyone tested or explored the possibility of regulating the progressive opening of the valve using the valve_closing_degree parameter?
With an external regulator and either the TRVZB probe or an external probe, could this parameter be used to progressively open the valve to achieve the desired setpoint temperature?

Yes, I’m using Versatile thermostat for that. It has progressive control and it can use external temperature sensor.

1 Like

Yes I am using directly the valve position, together with PID controller
Works very well

1 Like

I have similar situation as @MatcherMike discribed. When I enable TRV Calibration automation local temperature is starting to flicks when I change target temperature on better thermostat. Looks like conditions is all time false. I have to add (0) for float as @uz-it wrote because without it automation wan’t work. I disabled all reporting on TRV and without automation calibration is ramming stable. Any ideas why?

I’ve been using the Sonoff TRVZB since last week and also the Aqara external sensors.
I have developed a similar solution and would like to share it here because I had a similar problem where the two triggers kept triggering each other.

Sometimes the solution is so simple, I just added a 5 minute break at the end.

alias: Badezimmer Externer Sensor
description: ""
triggers:
  - trigger: state
    entity_id:
      - sensor.sensor_badezimmer_temperature
  - trigger: state
    entity_id:
      - climate.trv_bad
    attribute: current_temperature
    enabled: true
conditions:
  - condition: template
    value_template: "{{ has_value('sensor.sensor_badezimmer_temperature') }}"
  - condition: template
    value_template: "{{ has_value('number.trv_bad_local_temperature_calibration') }}"
  - condition: template
    value_template: "{{ state_attr('climate.trv_bad', 'current_temperature') != None }}"
  - condition: template
    value_template: |
      {{
        (state_attr('climate.trv_bad', 'current_temperature')|float(default=0) -
        states('sensor.sensor_badezimmer_temperature')|float(default=0)) !=
        states('number.trv_bad_local_temperature_calibration')|float(default=0)
      }}
actions:
  - action: number.set_value
    metadata: {}
    data:
      value: >-
        {{
        (states('number.trv_bad_local_temperature_calibration')|float(default=0)
        -  (state_attr('climate.trv_bad',
        'current_temperature')|float(default=0) -
        states('sensor.sensor_badezimmer_temperature')|float(default=0))) |
        round(1) }}
    target:
      entity_id: number.trv_bad_local_temperature_calibration
  - delay:
      hours: 0
      minutes: 5
      seconds: 0
mode: single

Links: https://github.com/Koenkk/zigbee2mqtt/discussions/24487#discussioncomment-11104159

1 Like

hello all,

i have managed to make a automation to automatically ajust the calibrations, but sometimes i get strange values, to high calibration or to low clibration, but also strangely only on thermostat 2, thermostat 1 seems to work proerly, and i dont know why:

alias: Calibrate Thermostats Local Temperature
description: Calibrate Thermostats Local Temperature
triggers:
  - entity_id: sensor.mi_living_room_thermometer_temperature
    trigger: state
conditions: []
actions:
  - target:
      entity_id: number.termostat_1_local_temperature_calibration
    data:
      value: >-
        {{ ((states('sensor.mi_living_room_thermometer_temperature') | float) -
        (state_attr('climate.termostat_1', 'current_temperature') | float)) +
        (states('number.termostat_1_local_temperature_calibration') | float) |
        round(2) | float }}
    action: number.set_value
  - target:
      entity_id: number.termostat_2_local_temperature_calibration
    data:
      value: >-
        {{ ((states('sensor.mi_living_room_thermometer_temperature') | float) -
        (state_attr('climate.termostat_2', 'current_temperature') | float)) +
        (states('number.termostat_2_local_temperature_calibration') | float) |
        round(2) | float }}
    action: number.set_value
mode: single

what i am struggle-ing with is, i would like the automation to run only if the difference ist +/- 0.1 degrees, or the (Then Do) statement to run

I tryed an or statement at triggers:

{{ (states('sensor.mi_living_room_thermometer_temperature') | float) - (state_attr('climate.termostat_2', 'current_temperature') | float) | abs > 0.1 }}
{{ (states('sensor.mi_living_room_thermometer_temperature') | float) - (state_attr('climate.termostat_2', 'current_temperature') | float) | abs < 0.1 }}

but it only works for thermostat 1, the thermostat 2 is not reacting. the automation looks like this:

alias: Calibrate Thermostats Local Temperature
description: Calibrate Thermostats Local Temperature
triggers:
  - entity_id: sensor.mi_living_room_thermometer_temperature
    trigger: state
conditions:
  - condition: or
    conditions:
      - condition: template
        value_template: >
          {{ (states('sensor.mi_living_room_thermometer_temperature') | float) -
          (state_attr('climate.termostat_1', 'current_temperature') | float) |
          abs < 0.1 }}
        enabled: true
    enabled: true
  - condition: or
    conditions:
      - condition: template
        value_template: >
          {{ (states('sensor.mi_living_room_thermometer_temperature') | float) -
          (state_attr('climate.termostat_1', 'current_temperature') | float) |
          abs > 0.1 }}
        enabled: true
    enabled: true
  - condition: or
    conditions:
      - condition: template
        value_template: >
          {{ (states('sensor.mi_living_room_thermometer_temperature') | float) -
          (state_attr('climate.termostat_2', 'current_temperature') | float) |
          abs < 0.1 }}
        enabled: true
    enabled: true
  - condition: or
    conditions:
      - condition: template
        value_template: >
          {{ (states('sensor.mi_living_room_thermometer_temperature') | float) -
          (state_attr('climate.termostat_2', 'current_temperature') | float) |
          abs > 0.1 }}
        enabled: true
    enabled: true
actions:
  - target:
      entity_id: number.termostat_1_local_temperature_calibration
    data:
      value: >-
        {{ ((states('sensor.mi_living_room_thermometer_temperature') | float) -
        (state_attr('climate.termostat_1', 'current_temperature') | float)) +
        (states('number.termostat_1_local_temperature_calibration') | float) |
        round(2) | float }}
    action: number.set_value
  - target:
      entity_id: number.termostat_2_local_temperature_calibration
    data:
      value: >-
        {{ ((states('sensor.mi_living_room_thermometer_temperature') | float) -
        (state_attr('climate.termostat_2', 'current_temperature') | float)) +
        (states('number.termostat_2_local_temperature_calibration') | float) |
        round(2) | float }}
    action: number.set_value
mode: single

@nightshadow How did you update your TRBZB to get progressive valve control to be available? Could you share how you configured Versatile thermostat to do this?
Thanks

For my side, I try an other approche with Node Red, I use the parameter valve_opening_degree and valve_closing_degree to limit and regule opening state.
But it’s not perect.
Sometimes the vavle stay in heating mode despite that Local temperature is over the Occupied heating setpoint

With Better Thermostat, it full open or close… no regulation.

I m not sure if the poeple know the difference between regulation and on off

Read the documentation for Versatile thermostat, you’ll find everything there.

Firstly, I want to thank @berkans for this detailed and well rationalised explanation.

I have Sonoff TRVZBs on all my radiators except one (bathroom). I’ve been running better thermostat (BT) on each (with external sensors) for over a year now and there’s always been a bit of weirdness but nothing too broken.

Previously we had a Nest Thermostat in the living room which was ok, but it meant always heating the living room up, even if we aren’t in there. As my partner works from home this is fairly frequent.

So I finally bit the bullet and put a Shelly Plus 2PM to switch on and off the boiler and let home assistant do it. I also used a Shelly add on to put heat sensors on the outflow and return pipes. So now I more control and data.

Your post made me aware of why I was seeing some of the strangeness I described above: the rubbish built in temp sensor (I had mostly been using external with BT anyway but I paid more attention to it), and the TRVZB hysteresis. We’ve certainly had regular occurances where the UI was telling us the radiator should be heating but was stone cold. A big manual change would then ‘wake it up’.
Clearly this was why.

The TRV Calibration automation worked well for me more or less straight away. Took me a little while to figure out how to handle two TRVs in one room in a single automation but that’s now fine too.

Where I had significant problems was with the Hyesteresis automation.

Firstly I was having a problem where the TRVZB climate entity would STILL sometimes fail to open the valve and not change to heating mode, even though I saw it momentarily change the target by +2C. This was fixed by slightly increasing the delay from 2secs to 3-5s (I used a bit of trial and error to find what was reliable). I guess this is maybe zigbee network and device dependent?

Secondly I was finding that the automation would frequently (but not always) fail to restore the original temperature in the final step. This would sometimes happen even if the TRVZB climate target was successfully restored, then the BT target (still +2 degrees) would override it and I’d be stuck with the room heating 2C higher than we wanted. Best guess is that maybe the BT algorithm is too slow to respond or gets confused.

For now I have found a fix which is simply to modify your automation to take the BT target temp as the “original temperature”. Then in the final step I also do a set temperature of the original on BT climate entity.

Still gradually developing my understanding of how this works and there may be a better way.

A question I have for everyone.

  1. Why is it suggested to use an automation to perform the TRV calibration rather just let BT do it? If they are using the same external sensor shouldn’t the result be the same. Is it just that we don’t have visibility of what BT is actually doing behind the scenes or did you try this and get a worse result?

I spoke too soon above. I still had issues after the “fix”. I’ve now changed tack and switched from Better Thermostat to Versatile thermostat. The main advantage is direct valve control and what seems like better documentation and a bigger user base.

However I’m still seeing issues with the TRVZBs. I’ve posted about it in the Versatile Thermostat thread.

1 Like

Hi everyone,
maybe I’m missing a part but about the original post and the custom automation made with HA to calibrate the valve offset using an external temperature sensor.

After I configured the SonOff valve in Better Thermostat, I can see that the offset is changing automatically, so I guess Better Thermostat is able to support this kind of parameter.

If so, why a custom automation is needed to do this?

Hello, I’m a bit late to the party here… I am the guy who exposed quite a few of the settings of the TRVZB in Z2M (Commits · Koenkk/zigbee-herdsman-converters · GitHub).

I’ve just seen @berkans comment about “external temperature sensor integration” which I was previously unaware of, so I thought I would take a further look…

Using a Sonoff Bridge and the eWeLink app, I was able to “connect” my Sonoff SNZB-02D to the TRV. The end result of doing this is that the TRV reports its local temperature as the one shown on the display of the SNZB-02D.

Initial investigations suggest that this synchronisation appears to be co-ordinated by the Sonoff Bridge: the SNZB-02D advertises its current temperature to the Bridge and then the Bridge sends a command to the TRV to update its local temperature. As a result, there is no “temperature calibration”, the local temperature follows the external sensor.

It will be pretty easy for me to expose this field in Z2M. The question is where should the orchestration occur? It would be simple enough to create an automation/blueprint in HA that is triggered on the temperature changing on the external sensor, which then tells the TRV to update its internal temperature…but then I wonder whether this is something that could also be achieved directly in Z2M. I will start a discussion with @Koenkk in his GitHub repo - unless he feels like commenting on it here also. :slight_smile:

1 Like

Discussion here: Sonoff TRVZB temperature synchronisation with external sensor · Koenkk/zigbee2mqtt · Discussion #26308 · GitHub

1 Like

This would be a very useful addition. I mostly-like the TRV2B (like the speed of heat up due to its on/off logic for the valve, but dislike the resulting reduction in battery life), but it is a bugger to calibrate. I use openenergymonitor temperature sensors, and being able to integrate more closely would be nice.

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

First of all, I apologize for the late reply.

At the beginning, I tried using the Better Thermostat (BT) settings for TRV calibration. However, even though all the settings seemed correct in BT, the BT temperature and the MQTT climate TRV temperature never matched.

So I decided to switch to automating the temperature calibration via MQTT. But that approach also led to different problems.

Fortunately, we no longer need to update via MQTT. Zigbee2MQTT has now added a direct external option, allowing manual calibration by simply selecting the external sensor—no MQTT commands needed.
That said, this feature works outside of Z2M’s internal logic, so you still need to create a simple automation. But it’s much easier now, and from what I’ve seen so far, calibration works very effectively.

I’ve updated my post to reflect this change.

As for the hysteresis issue, with the firmware v1.3.0 you can now change the value between 1 and 0.2.

In my observations, the BT and the original MQTT climate device were not sharing matching temperature values. In short, BT wasn’t doing exactly what it was supposed to.

Even though the temperature shown in BT was aligned with the external sensor, when I checked via MQTT, the climate device reported completely different values. That’s why I preferred using an automation instead.

However, as I’ve updated in my post, this process is now much easier, and I actually recommend not doing it through BT anymore.

Hello,
First of all, congratulations—these are smart and creative solutions. The best setup is always the one that works well for you and brings satisfaction.

I’ve recently updated my own write-up. There’s no longer a need to create a separate automation for hysteresis control, as this feature is now built-in with firmware version 1.3.0.
Also, the external temperature option now works flawlessly with a simple automation or blueprint.

The Sonoff TRV has truly become a reliable and advanced device when used with Zigbee2MQTT.
With the settings I shared for Better Thermostat, the stability has improved even further.

One of the most challenging aspects used to be the valve values reported over MQTT, but with the latest additions, it has reached an excellent and highly usable state.

@photomoose Thank you as well for all your efforts and for sharing the blueprint — it’s truly appreciated.