Zigbee2MQTT - Danfoss Ally TRV Room Load Balancing

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.

Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.
updated version:

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

source

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
6 Likes

Wonderful blueprint - thank you very much for your good work! I setup the automation today and so far it’s working without any flaw.

I use the Popp Smart Thermostat which is 100% identical to Danfoss Ally TRV, so I had to change the manufacturer name in the blueprint to “Popp”. Maybe this is worth adding as an option in your blueprint?

Thanks for the feedback, and I’m glad to hear it is working well for you.
Yes, I’ve read elsewhere that this Popp thermostat is identical to Danfoss Ally. It’s a good idea to include it in the blueprint filter. However, looking at the selector documentation, it seems the manufacuturer filter variable only accepts a string, which would mean only a single value…

I might try - maybe it works with a list as well. If not, maybe I should just filter the device by it having a climate entity. Then it would show any climate devices, and it would be up to the user to avoid choosing an incompatible one, but would not limit it so strictly. I think I read there was another manufacturer with a compatible (same?) thermostat.

Edit: yes, looks like a list is not supported in these selectors. There’s a feature request open: Allow using a list in blueprint selectors

Maybe it is worth just removing the manufacturer filter and offering to select any climate devices…

I took the liberty of copying this blueprint to adapt it for the Hive TRV version since it is based off Danfoss Ally. Hive TRV Load Balancing Home Assistant Blueprint · GitHub

I am also working on creating a radiator boost mode setting which boosts the main thermostat’s heating if the radiator valve needs heat.

1 Like

This doesn’t work for me. Zigbee2MQTT says the Entity ‘My Thermostat Name’ is unknown. My guess is that using device_attr(repeat.item, 'name') doesn’t work and should be something like device_attr(repeat.item, 'friendly_name')? I have yet to test this myself.

I managed to solve it using: zigbee2mqtt/{{ device_attr(repeat.item, 'identifiers')|replace("{('mqtt', 'zigbee2mqtt_",'')|replace("')}",'') }}/set/load_room_mean

Thanks for sharing this blueprint.
Sadly, I use deCONZ as coordinator and I have no clue on how to adapt your template to deCONZ.

@swoop any suggestions?

Hi,

Indeed, the blueprint would only work if the Zigbee2MQTT and Home Assistant device names were the same. I have updated the blueprint and it now updates the number.xxxx_load_room_mean entities, instead of pushing a direct MQTT message.
This should hopefully mitigate such issues.

1 Like

Hi,

I’ve updated the blueprint to use home assistant exposed entities instead of pushing the value directly over MQTT.
I don’t use deCONZ, and don’t know what attributes it exposes, but if it exposes the load_estimate and (settable) room_load_mean attrbutes of the TRV, then the new version might be easier to adapt.

Hi, i have used your blueprint with z2m for a while and it was working great. Sadly i really had a lot of issues with z2m and i changed the coordinator and use zha now. Is it possible, to convert this blueprint to zha maybe using the zha toolkit?

Made it work with ZHA

Made a script I call every 15 minutes

  • It fetches 3 load_estimates using zha_toolkit.attr_read and saving to sensor. (Notice that the attribute in my script corresponds to 0x404a
  • It calculates the average (I have not implemented to ignore <-500 and inactive…
  • It sends the mean to each Ally (0x4040) via zha_toolkit.attr_write (important to set attr_type to 0x29 as I was having difficulties with the default 0x21)
  • It then reads back the value so I can confirm it was set

Whole code

  • Be sure to first create a input_number.stue_load_estimate or similar to save the mean in
alias: Get load_estimate
sequence:
  - service: zha_toolkit.attr_read
    data:
      ieee: climate.ally_hvide_thermostat
      cluster: 513
      attribute: 16458
      state_id: sensor.ally_hvide_load_estimate_radiator
      allow_create: true
  - service: zha_toolkit.attr_read
    data:
      ieee: climate.ally_spisebord_thermostat
      cluster: 513
      attribute: 16458
      state_id: sensor.ally_spisebord_load_estimate_radiator
      allow_create: true
  - service: zha_toolkit.attr_read
    data:
      ieee: climate.ally_kontor_thermostat
      cluster: 513
      attribute: 16458
      state_id: sensor.ally_kontor_load_estimate_radiator
      allow_create: true
  - service: input_number.set_value
    data:
      entity_id: input_number.stue_load_estimate
      value: |-
        {{ ((
              states('sensor.ally_hvide_load_estimate_radiator') | int +
              states('sensor.ally_spisebord_load_estimate_radiator') | int +
              states('sensor.ally_kontor_load_estimate_radiator') | int
            ) / 3) | round(0) | int }}
  - service: zha_toolkit.attr_write
    data:
      ieee: climate.ally_kontor_thermostat
      cluster: 513
      attribute: 16448
      attr_val: "{{ states('input_number.stue_load_estimate') | int }}"
      attr_type: 41
    enabled: true
  - service: zha_toolkit.attr_write
    data:
      ieee: climate.ally_spisebord_thermostat
      cluster: 513
      attribute: 16448
      attr_val: "{{ states('input_number.stue_load_estimate') | int }}"
      attr_type: 41
    enabled: true
  - service: zha_toolkit.attr_write
    data:
      ieee: climate.ally_hvide_thermostat
      cluster: 513
      attribute: 16448
      attr_val: "{{ states('input_number.stue_load_estimate') | int }}"
      attr_type: 41
    enabled: true
  - service: zha_toolkit.attr_read
    data:
      ieee: climate.ally_hvide_thermostat
      cluster: 513
      attribute: 16448
      state_id: sensor.ally_hvide_load_mean
      allow_create: true
  - service: zha_toolkit.attr_read
    data:
      ieee: climate.ally_spisebord_thermostat
      cluster: 513
      attribute: 16448
      state_id: sensor.ally_spisebord_load_mean
      allow_create: true
  - service: zha_toolkit.attr_read
    data:
      ieee: climate.ally_kontor_thermostat
      cluster: 513
      attribute: 16448
      state_id: sensor.ally_kontor_load_mean
      allow_create: true
2 Likes