Edit: This blueprint has been updated. The updated version sets the calculated value via number.<trv_name>_load_room_mean
home assistant entities, instead of publishing the attribute value directly via mqtt. This should mitigate issue when the device name in home assistant and zigbee2mqtt is not the same. Additionally, I have removed the “manufacturer” constraint when choosing devices, because some other manufacturers have TRVs which are fully compatible with Danfoss Ally TRV. Still, when choosing, you should only choose Danfoss Ally (or fully compatible) TRVs.
The new version should be backwards-compatible with the old one. I’ve moved the original blueprint code to the bottom of this post in case someone still needs it.
This blueprint/automation sends the current estimated load from other TRVs in the same room, so the TRVs can adjust their heat output to “balance” heating between themselves. This should be especially useful if the same room has radiators of different size.
To use this, the TRV attribute “Load Balancing Enable” should be set to “true” (default setting). Then create an automation from this blueprint and select all Danfoss Ally TRVs in the same room.
Using this feature only makes sense for multiple TRVs in the same room, set to the same target temperature.
The automation implements the algorithm from Danfoss programming guide. More details below.
blueprint:
domain: automation
name: Danfoss Ally Room Load Balancing
description: >
Calculate and send room mean load every 15min to
load balance TRVs in the same room, via zigbee2mqtt.
Uses Danfoss recommended calculation.
input:
ally_devices:
name: Danfoss Ally TRV Device
description: Select all Danfoss Ally TRVs located in the same room
selector:
device:
entity:
domain: climate
multiple: true
variables:
devices: !input ally_devices
devices_all_entities: "{{ devices|map('device_entities')|sum(start=[]) }}"
room_load_mean: >
{% set load_estimate_entities = devices_all_entities
|select('match', 'sensor.*_load_estimate$') %}
{% set valid_states = expand(load_estimate_entities)
|selectattr('last_changed', 'ge', now() - timedelta(minutes = 90))
|map(attribute="state")|map("int",-8000)|select("ge",-500)|list %}
{% if valid_states|count == 0 %}
Unknown
{% else %}
{{ (valid_states|sum / valid_states|count) | round }}
{% endif %}
target_load_room_mean_entities: >
{{
devices_all_entities
|select('match', 'number.*_load_room_mean$')
|list
}}
trigger:
- platform: time_pattern
minutes: /15
condition:
- condition: template
value_template: "{{ is_number(room_load_mean) }}"
action:
- service: number.set_value
target:
entity_id: "{{ target_load_room_mean_entities }}"
data:
value: "{{ room_load_mean }}"
mode: single
Here is the quote from Danfoss documentation, which this automation attempts to implement:
For Load Balancing is intended a feature aimed to distribute
the room need for heat (load) between 2 or more radiators in
the same room.
Each eTRV in the room will report its own load level to the
gateway via the attribute 0x404A “Load estimate on this
radiator”.
The gateway calculates an average of the load for all the
radiators in the room and distributes it to all the eTRVs in the
room via the attribute 0x4040 ”Load Radiator Room Mean”
every 15 minutes.
The Gateway must discard all the values below -500 (too
low) down to -8000 (invalid/inactive) and values older than
90 minutes. The average must then be calculated with the
values from the other eTRVs in the room.
The feature is enabled by default on the eTRV. The eTRV will
start reacting to the information from the gateway as soon as
the average room load information is received.
Notes:
- This control must not be used in rooms with only 1 eTRV.
- If the load radiator Room Mean information is not sent by
the gateway for more than 90 minutes the eTRV will go
This blueprint aims to do the whole calculation in the automation, but if you prefer, you can extract the value calculation from the blueprint (room_load_mean
variable) and create a template sensor with that, so you can always use the value, which is how I used it previously. Then you can create an automation (not from this blueprint) to check the condition (not unknown and not below -500), and send it off to the TRVs every 15min.
I also recommend to use an external temperature sensor for more accurate operation. There are multiple blueprints available for this which work well. Here’s one example I uploaded last week - ZHA, Zigbee2MQTT - Danfoss Ally send external temperature to TRV
A few notes about ZHA
This blueprint is for Zigbee2MQTT. If you are looking to adapt it to ZHA, it will be more complicated, because ZHA does not expose the needed attributes. You will need to use zha-toolkit to read the load_estimate to a state, then calculate the mean similar to my template in the blueprint, and then push it to the TRVs. Also, you will need to use a custom quirk, because in ZHA, the Danfoss quirk has unsigned integer for the load_estimate
and room_load_mean
, whereas it needs to be a signed integer, as negative values are possible and quite common.
Since zha_toolkit does not receive device reports, you can only query for the needed attribute. This will be less efficient as reports, and I think you would not be able to ignore values older than 90 minutes, as zha-toolkit will “get” the attributes every time.
Old version of this blueprint:
blueprint:
domain: automation
name: Danfoss Ally Room Load Balancing
description: >
Calculate and send room mean load every 15min to
load balance TRVs in the same room, via zigbee2mqtt.
Uses Danfoss recommended calculation.
input:
ally_devices:
name: Danfoss Ally TRV Device
description: Select all Danfoss Ally TRVs located in the same room
selector:
device:
manufacturer: Danfoss
entity:
domain: climate
multiple: true
variables:
devices: !input ally_devices
room_load_mean: >
{% set load_estimate_entities = devices|map("device_entities")
|sum(start=[])|select('match', '.*load_estimate$')|list %}
{% set valid_states = expand(load_estimate_entities)|select
|selectattr('last_changed', 'ge', now() - timedelta(minutes = 90))
|map(attribute="state")|map("int",-8000)|select("ge",-500)|list %}
{% if valid_states|count == 0 %}
Unknown
{% else %}
{{ (valid_states|sum / valid_states|count) | round }}
{% endif %}
trigger:
- platform: time_pattern
minutes: /15
condition:
- condition: template
value_template: "{{ room_load_mean != 'Unknown' }}"
action:
- repeat:
for_each: !input ally_devices
sequence:
- service: mqtt.publish
data:
topic: "zigbee2mqtt/{{ device_attr(repeat.item, 'name') }}/set/load_room_mean"
payload_template: "{{ room_load_mean }}"
mode: single