Advice about controlling heating water circulation pump with smart TRV valve positions

Summary: I would like advice about the best way to control a heating water circulation pump based on smart radiator valve positions.

Note: I have already searched these forums for the same topic, and not found my question, but since I am a newcomer I may have overlooked the obvious.

Introduction:
I have a home which is heated using a central furnace/boiler that circulates hot water to radiators in the rooms. I have recently set up a Matter/Thread system in which each radiator has an Eve Thermo Thermostatic Radiator Valve (TRV) mounted to it. The house is divided into two zones, each of which has a separate circulation pump. The temperature of the water in the furnace/boiler is set by the outdoor temperature.

My issue:
I would like some advice about the ``best way’’ to automate the two circulation pumps. One of the entities which is exposed by the TRV is the valve position. I would like to set things up so that if all of the TRVs in a particular zone have valve position 0 (closed) then the circulation pump is off, else it is on.

I am a newcoming to HA, so have only done a “proof of principle” using one TRV valve, and turning on/off an Eve Energy relay. You can see my .yaml automations below.

My question: what is the best way to go about this? Each of the two zones has about 25 TRVs in it. Should I have a single automation per zone that triggers on a state change in any of these? Should I have one automation per TRV? Should I have one script/automation per circulation pump that runs each five minutes and checks the valve states, then flips the circulation pumps on/off as needed?

The goal is that this should be as simple as possible, subject to being robust against failure of any particular TRV. If a TRV does not provide data, I would like to be alerted, and the pump should be turned on.

To simplify matters, perhaps there is a way for me to assign the Eve Thermo to one of two zones, and then to address all of the valve entities in each zone as a single object. As I said, I am a newcomer and don’t know much.

A further complication: I have noticed that sometimes valve data is missing for many hours. This is not because of an unreliable thread network. It is (perhaps) because I first built my system using Apple Home, then added HA as a second Matter hub/ecosystem. So it may be that only Apple Home, as the first Matter controller, gets all data and HA only gets some data. Another explanation is that my system has two Apple Homepods as thread border routers, and I’ve just added a third: an Open Thread Border Router running under HA. I have the impression that this is fairly cutting-edge and there may be some unexpected interactions between them. But I’m not sure about what is behind this problem. If helpful, I can post a plot showing valve position, which illustrates the data dropouts.

Proof of principle automations:

alias: CirculationPumpOn
description: Turns on the circulation pump
triggers:
  - trigger: numeric_state
    entity_id:
      - sensor.eve_thermo_20ebp1701_valve_position
    above: 0
conditions: []
actions:
  - type: turn_on
    device_id: cfd42e085df395de82c2019bccda89af
    entity_id: d3bb4a986ca9a6c10f3c661e3ecfb5ff
    domain: switch
mode: single

alias: CirculationPumpOff
description: Turns off the circulation pump
triggers:
  - value_template: "{{ states('sensor.eve_thermo_20ebp1701_valve_position') == '0' }}"
    trigger: template
conditions: []
actions:
  - type: turn_off
    device_id: cfd42e085df395de82c2019bccda89af
    entity_id: d3bb4a986ca9a6c10f3c661e3ecfb5ff
    domain: switch
mode: single

Thank you in advance for any advice about this. It may be a standard issue with a standard solution, in which case I apologize in advance for my ignorance.

Thanks,
Bruce

Hi Bruce, I’d love to help but there are a huge number of questions within your question. Heating engineering and watt transfer to your emitters is one topic, nothing to do with HA which is very relevant. Another is ensuring the boiler isn’t pumping against a closed head which potentially can cause it to boil and fail in a spectacular manner. There are issues with circulation flow etc etc. Really all that stuff is the job of a heating engineer to figure out. The usual what is the resistance of my index circuit and then what pump do I need, problem. Then there is the issue of the needed flow rate to transfer the required water volume with the relevant number of watts into the emitters. etc. All of the above sort of dictate what your valve “should be” doing to enable the heating to occur.

I expect you already know about those topics and really are wondering how HA can drive it.

So far as HA is concerned, it is event driven. So my advice [and I am only a few months into HA myself] would be to have the code loop each time anything in the system changes state and evaluate that and respond accordingly.

I realise I have not addressed many of the sub and implied questions, but time is precious and perhaps they are best broken out into a number of bite sized questions of their own.

Hi Anthony,
thank you for the helpful reply!

You are correct that these topics are important, but I have already taken them into account.

  1. Heat transfer to the radiators is appropriate. I have tested this with the circulation pump running 24 x 7 and the room temperatures are nicely controlled without significant overshoot. The furnace/radiator system was correctly designed and sized, and I am not interfering with this.
  2. Boiler pumping against a closed head is also not an issue. The boiler maintains a fixed temperature (determined by outdoor temperature) and has an expansion tank. So even with all radiator valves closed and the circulation pump off, the system behaves sensibly.
  3. Issues with circulation flow etc: the circulation pumps are Grundfos Alpha intelligent pumps in self-adjusting mode. They regulate their speed so that when many TRV are open, they pump water at higher flow rates, and when most or all TRV are closed, they slow down to minimal. I would simply like to reduce that to zero when all TRV are closed.
  4. Resistance of the index circuit, etc, solved by item 3 above.

I have thought about these topics and taken them into account. I’ve been using a system of smart radiator valves for the past 16 years (now ancient technology from ELV) but decided this year to replace it with something more modern. The main benefits are that I can program the system centrally at my computer, and also see the battery status for all devices in one overview. And, of course, that I can turn off the two circulation pumps when they are not needed.

So what I’d like advice about is not how to configure my heating system. That is a solved problem. I’d like advice about how to correctly structure a HA automation. There are many ways to approach it, and I thought that some of the people here could give me the benefit of their experience and lessons learned.

Agreed, and this is what my “proof of principle” code does. But there are many ways to structure this. Tor example, would it be better to drive the event loop with a timer which triggers once per minute? Or better to trigger off the valve positions as I have done in my code above? One automation per TRV or one per circulation pump or ??

I’m nevertheless very grateful for your reply! If I don’t get replies regarding structure, then I’ll do as you suggest.
Cheers,
Bruce

In that case really your only issue is whether to run the pump for each zone or not. If any of these entities state is greater than 0 run pump else if all entities are 0 (i.e. closed) kill the pump off. ?

I would use one automation per pump with a separate trigger for each TRV head all in the one Automation. Then I would use building blocks [if / else conditional if’s] to logically respond to the trigger appropriately.

Hi Anthony,
thanks again for the helpful reply!

Yes, what I want to accomplish is very simple. If all TRV valves closed, turn off switch. If any TRV valve open, turn on switch. But there are a dozen ways to code that up.

So, my question is purely a programming one: what is the best way to structure this.

Ok. Can I ask why? For example, why not two automations per pump, one to turn it on and one to turn it off?

Is there a way to define a variable which is (for example) the sum of the values of all TRV valves, by using pattern matching (say, regular expressions)? So I don’t need to list all 25 of them, but can define the variable with a single regexp and then test if it is > 0 to see if any of the valves are open?

I might be inclined to run four automations. 2 which set a boolean for each valve to true, which have all the relevant TRV head states as triggers. 2 which trigger on the boolean change which open the valve. 2 which trigger on boolean change which close the valve.

Your only problem now is logically mitigating closed TRV’s from ones which still need heat in the same zone.

Here’s the next iteration. This does both on and off, and if any of the entities is not defined, it should turn on the pump, which is the desired behaviour.

Main thing I don’t like is that each valve entity is given twice. Is there a clean/simple way to avoid that?

alias: CirculationPumpOnOff
description: Turns circulation pump ON if any valve open, OFF if all valves closed
trigger:
  - platform: state
    entity_id:
      - sensor.eve_thermo_20ebp1701_valve_position
      - sensor.eve_thermo_20ebp1701_valve_position_2
      - sensor.eve_thermo_20ebp1701_valve_position_3
      - sensor.eve_thermo_20ebp1701_valve_position_4
      - sensor.eve_thermo_20ebp1701_valve_position_5
action:
  - choose:
      - conditions:
          - condition: and
            conditions:
              - condition: numeric_state
                entity_id: sensor.eve_thermo_20ebp1701_valve_position
                below: 1
              - condition: numeric_state
                entity_id: sensor.eve_thermo_20ebp1701_valve_position_2
                below: 1
              - condition: numeric_state
                entity_id: sensor.eve_thermo_20ebp1701_valve_position_3
                below: 1
              - condition: numeric_state
                entity_id: sensor.eve_thermo_20ebp1701_valve_position_4
                below: 1
              - condition: numeric_state
                entity_id: sensor.eve_thermo_20ebp1701_valve_position_5
                below: 1
        sequence:
          - type: turn_off
            device_id: cfd42e085df395de82c2019bccda89af
            entity_id: switch.d3bb4a986ca9a6c10f3c661e3ecfb5ff
            domain: switch
    default:
      - type: turn_on
        device_id: cfd42e085df395de82c2019bccda89af
        entity_id: switch.d3bb4a986ca9a6c10f3c661e3ecfb5ff
        domain: switch
mode: single

Do you mean listed once for the trigger and once for the evaluation portion of the choose conditions? If so no, they are entirely different aspects of the automation and that will be fine.

Hi Anthony,

OK. I was hoping to avoid the repetition of the variable names, since there will be about 50 of them. But it seems unavoidable.

I made some further modifications to my script.

  1. If a valve is closing, the script is triggered when it is still open and does not yet have value 0. So I have inserted an 8 second delay after the trigger, and before I evaluate the valve value. This gives it time to close fully and reach value 0.
  2. If several TRV change their positions at similar times, for example, if there are several TRVs located in the same room, then we need to restart the script to get additional delay. So I’ve changed the mode at the end to ‘restart’.
  3. I’ve added logging for debugging purposes
  4. Used an implied AND in the conditions block
  5. Simplified the switch on/off syntax.

I am not convinced that this is going to be the “most reliable way”. I’m now going to experiment with an automation triggered once per minute that simply evaluates all of the valve positions. I feel that this might be more robust, meaning less likely to leave the circulation pump switch in the wrong position for a long period.

alias: CirculationPumpOnOff7
description: Turns circulation pump ON if any valve open, OFF if all valves closed
triggers:
  - entity_id:
      - sensor.eve_thermo_20ebp1701_valve_position
      - sensor.eve_thermo_20ebp1701_valve_position_2
      - sensor.eve_thermo_20ebp1701_valve_position_3
      - sensor.eve_thermo_20ebp1701_valve_position_4
      - sensor.eve_thermo_20ebp1701_valve_position_5
    trigger: state
actions:
  - delay: "00:00:08"
  - action: logbook.log
    data:
      name: CirculationPump
      message: >
        {{ now().strftime('%H:%M:%S') }} valve_1={{
        states('sensor.eve_thermo_20ebp1701_valve_position') }}, valve_2={{
        states('sensor.eve_thermo_20ebp1701_valve_position_2') }}, valve_3={{
        states('sensor.eve_thermo_20ebp1701_valve_position_3') }}, valve_4={{
        states('sensor.eve_thermo_20ebp1701_valve_position_4') }}, valve_5={{
        states('sensor.eve_thermo_20ebp1701_valve_position_5') }}
  - choose:
      - conditions:
          - condition: numeric_state
            entity_id: sensor.eve_thermo_20ebp1701_valve_position
            below: 1
          - condition: numeric_state
            entity_id: sensor.eve_thermo_20ebp1701_valve_position_2
            below: 1
          - condition: numeric_state
            entity_id: sensor.eve_thermo_20ebp1701_valve_position_3
            below: 1
          - condition: numeric_state
            entity_id: sensor.eve_thermo_20ebp1701_valve_position_4
            below: 1
          - condition: numeric_state
            entity_id: sensor.eve_thermo_20ebp1701_valve_position_5
            below: 1
        sequence:
          - target:
              device_id: cfd42e085df395de82c2019bccda89af
            action: switch.turn_off
            data: {}
    default:
      - target:
          device_id: cfd42e085df395de82c2019bccda89af
        action: switch.turn_on
        data: {}
mode: restart

This seems to be simpler and more reliable. It runs once per minute and checks all of the valve positions. If all are less than 1% (which means closed) then it turns off the circulation pump. If any of the valves are open or undefined, then the pump is turned on.

alias: CirculationPumpOnOff9
description: Turns circulation pump ON if any valve open, OFF if all valves closed
triggers:
  - trigger: time_pattern
    minutes: /1
actions:
  - choose:
      - conditions:
          - condition: numeric_state
            entity_id: sensor.eve_thermo_20ebp1701_valve_position
            below: 1
          - condition: numeric_state
            entity_id: sensor.eve_thermo_20ebp1701_valve_position_2
            below: 1
          - condition: numeric_state
            entity_id: sensor.eve_thermo_20ebp1701_valve_position_3
            below: 1
          - condition: numeric_state
            entity_id: sensor.eve_thermo_20ebp1701_valve_position_4
            below: 1
          - condition: numeric_state
            entity_id: sensor.eve_thermo_20ebp1701_valve_position_5
            below: 1
        sequence:
          - action: switch.turn_off
            target:
              device_id: cfd42e085df395de82c2019bccda89af
    default:
      - action: switch.turn_on
        target:
          device_id: cfd42e085df395de82c2019bccda89af
mode: single

You can do it that way, but I have no idea if your valves are trustworthy, you seem to think not.
Otherwise I use a - replace command in PowerShell for dealing with lots of entity names.

I have 17 separate heating zones and 6 entities per zone + all the lovelace code to deal with overrides and reporting. The standard climate cards don’t work for me so I have had to make my own in button-card bla bla - it’s been months of work.

Here the template code the PowerShell_ise editor, with some room names redacted for privacy reasons.

function fg {$args}

cls

$RoomList = fg  back_door spare_room our_bedroom our_bathroom front_hall drawing_room upper_hall upstairs_loo dining_room dressing_room
$TextInfo = (Get-Culture).TextInfo

foreach ($room in $RoomList)
{

    $spl = $room.split('_')
    $sss = $spl[0]
    $ttt = $spl[1]
    $sss = $TextInfo.ToTitleCase($sss)
    $ttt = $TextInfo.ToTitleCase($ttt)
    $guid1 = [guid]::NewGuid()
    $guid2 = [guid]::NewGuid()
    $guid3 = [guid]::NewGuid()
    $guid4 = [guid]::NewGuid()
    $guid5 = [guid]::NewGuid()
    $guid6 = [guid]::NewGuid()
    $guid7 = [guid]::NewGuid()
$1 =
@'
# sss ttt #
  - name: rrrrrr_current_temperature
    unique_id: rrrr1
    device_class: temperature
    state_class: measurement
    unit_of_measurement: '°C'
    icon: mdi:thermometer
    state: >-
      {% set status = state_attr('climate.rrrrrr', 'current_temperature') %}
      {% if status is not none  %}
        {{ "%.2f" | format(status | float(0)) }}
      {% else %}
        unavailable
      {% endif %}

  - name: rrrrrr_target_temperature
    unique_id: rrrr2
    device_class: temperature
    state_class: measurement
    unit_of_measurement: '°C'
    icon: mdi:thermometer-lines
    state: >-
      {% set status = state_attr('climate.rrrrrr', 'status') %}
      {% if status is not none and 'setpoint_status' in status and 'target_heat_temperature' in status.setpoint_status %}
        {{ status.setpoint_status.target_heat_temperature | float(0) }}
      {% else %}
        unavailable
      {% endif %}

  - name: rrrrrr_previous_sp_temperature
    unique_id: rrrr3
    device_class: temperature
    state_class: measurement
    unit_of_measurement: '°C'
    icon: mdi:thermometer-lines
    state: >-
      {% set status = state_attr('climate.rrrrrr', 'status') %}
      {% if status is not none and 'setpoints' in status and 'this_sp_temp' in status.setpoints %}
        {{ status.setpoints.this_sp_temp | float(0) }}
      {% else %}
        unavailable
      {% endif %}

  - name: rrrrrr_next_sp_temperature
    unique_id: rrrr4
    device_class: temperature
    state_class: measurement
    unit_of_measurement: '°C'
    icon: mdi:thermometer-lines
    state: >-
      {% set status = state_attr('climate.rrrrrr', 'status') %}
      {% if status is not none and 'setpoints' in status and 'next_sp_temp' in status.setpoints %}
        {{ status.setpoints.next_sp_temp | float(0) }}
      {% else %}
        unavailable
      {% endif %}

  - name: rrrrrr_mode
    unique_id: rrrr5
    icon: mdi:calendar-arrow-right
    state: >-
      {% set status = state_attr('climate.rrrrrr', 'status') %}
      {% if status is not none and 'setpoint_status' in status and 'setpoint_mode' in status.setpoint_status %}
        {{ status.setpoint_status.setpoint_mode }}
      {% else %}
        unavailable
      {% endif %}

  - name: rrrrrr_next_setpoint
    unique_id: rrrr6
    icon: mdi:update
    state: >-
      {% set mode = states('sensor.rrrrrr_mode') %}
      {% if mode is not none and mode == 'TemporaryOverride' %}
        {% set status = state_attr('climate.rrrrrr', 'status') %}
        {% if status is not none and 'setpoint_status' in status and 'until' in status.setpoint_status %}
          {{ as_timestamp(status.setpoint_status.until) | timestamp_custom('%I:%M %p') }}
        {% endif %}
      {% elif mode == 'PermanentOverride' %}
        {{ 'Permenant' }}
      {% else %}
        {% set status = state_attr('climate.rrrrrr', 'status') %}
        {% if status is not none and 'setpoints' in status and 'next_sp_from' in status.setpoints %}
          {{ as_timestamp(status.setpoints.next_sp_from) | timestamp_custom('%I:%M %p') }}
        {% else %}
          unavailable
        {% endif %}
      {% endif %}

  - name: evohome_rrrrrr_battery
    unique_id: rrrr7
    device_class: battery
      state: >
        {% if state_attr('climate.rrrrrr', 'status').active_faults %}
          {% if state_attr('climate.rrrrrr', 'status').activeFaults[0].fault_type is search ('LowBattery') %}
            {{ 1 }}
          {% endif %}
        {% else %}
          {{ 0 }}
        {% endif %}
'@

    $1 -replace 'sss',$sss -replace 'ttt',$ttt -replace 'rrrrrr',$room -replace 'rrrr7',$guid7 -replace 'rrrr6',$guid6 -replace 'rrrr5',$guid5 -replace 'rrrr4',$guid4 -replace 'rrrr3',$guid3 -replace 'rrrr2',$guid2 -replace 'rrrr1',$guid1

}

Then I just paste the output into my template.yaml file which itself is defined in the configuration.yaml file.

Maybe you can make something useful out of that.

Hi Anthony,

I’ve noticed that sometimes the valve entities are missing. This may because I am using the Open Thread Border Router, and that is new and not yet working as it should. Or it may be because I am using my devices in two different ecosystems, Apple Home and Home Assistant. Or there may be other reasons, for example the Eve Matter firmware may still be buggy. So this is why I want a robust script.

I’m enough of a newcomer to HA that I don’t recognise what your code is for. I don’t think it is Automation code, which is what I have been posting. If this code for defining a dashboard or elements within that dashboard?

If so, I was hoping to find a way to modify the standard HA dashboard cards for the thermostat to indicate the valve percentage open. Do you know if that’s difficult to do?

Cheers,
Bruce

Understood on the TRV issues.

My code is external completely to Home Assistant.
Copy and paste my code it into Windows 10 or 11 PowerShell_ISE.exe

And then just run it.

You will see it produces about 1000 lines of template entities which solves your “I have loads of entity names”. You can specify them once in the $roomlist variable and then the code will fill in the replace strings. Code writing code.

It isn’t hard to write your own lovelace cards once you know how.
This Norwegian guy is amazing for that ! I am saving you about three months here !