ZHA, Zigbee2MQTT - Danfoss Ally send external temperature to TRV

This is my take on the automation to send external temperature to Danfoss Ally TRV. These are my first attempts at creating a blueprint.

It differs from others with:

  • ZHA and Zigbee2MQTT variants
  • Uses a timer helper to trigger next start (when external sensor does not update for a long time)
  • Does not use a delay/restart, as it can cause situation when attribute is not updated too long
  • (Zigbee2MQTT only) Automatically detects correct timings based on radiator covered/uncovered setting

To use, you have to create a separate timer helper for each automation created from the blueprint.

For ZHA, you have to set the timer for the correct maximum update interval, according to radiator mode (30 min for covered, 3h for uncovered), and in blueprint input specify the minimum interval (5 min for covered, 30 min for uncovered)

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

blueprint:
  domain: automation
  name: Ally Temperature Update
  description: Update Danfoss Ally TRV external temperature with min/max refresh rate
  input:
    ally_device:
      name: Ally TRV Device
      description: Temperature reading will be sent to this device
      selector:
        device:
          manufacturer: Danfoss
          entity:
            domain: climate
    min_update_minutes:
      name: Minimum update interval
      description: >
        Updates will not be sent if time from last update is less than minimum interval.
        Normally 30min for uncovered, 5min for covered.
      selector:
        number:
          max: 360
          min: 1
          unit_of_measurement: minutes
          mode: box
    temp_sensor_id:
      name: Temperature Sensor
      description: External sensor from which the temperature will be read. Expects data format 12.3
      selector:
        entity:
          domain: sensor
          device_class: temperature
    max_update_timer_id:
      name: Timer entity
      description: >
        Timer that will be (re)started on update.
        Set this timer to slowest interval you want the device to update at.
        Normally 3h for uncovered, 30m for covered.
        Use separate timer for each automation.
      selector:
        entity:
          domain: timer
variables:
  device: !input ally_device
  ieee: "{{(device_attr(device, 'identifiers')|list)[0][1]}}"
  min_update_minutes: !input min_update_minutes
  temp_sensor_id: !input temp_sensor_id
trigger:
- platform: state
  entity_id:
  - !input temp_sensor_id
- platform: event
  event_type: timer.finished
  event_data:
    entity_id: !input max_update_timer_id
condition:
- condition: template
  value_template: >
    {{ as_timestamp(now()) - as_timestamp(state_attr(this.entity_id,'last_triggered'),0)|int
    > (60 * min_update_minutes) }}
action:
- service: zha.set_zigbee_cluster_attribute
  data:
    ieee: '{{ ieee }}'
    endpoint_id: 1
    cluster_id: 513
    cluster_type: in
    attribute: 16405
    value: '{{ (states(temp_sensor_id) | float * 100) | round(0) }}'
- service: timer.start
  target:
    entity_id: !input max_update_timer_id
mode: single

For Zigbee2MQTT you still have to create a separate timer helper for each automation, but you don’t have to set the intervals as they are decided automatically based on radiator_covered state. You can also change the covered/uncovered mode, and automation will use new timings on next run.

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

blueprint:
  domain: automation
  name: Danfoss Ally Ext Temp Z2M
  description: Update Danfoss Ally TRV external temperature with min/max refresh rate, via zigbee2mqtt
  input:
    ally_device:
      name: Ally TRV Device
      description: Temperature reading will be sent to this device
      selector:
        device:
          manufacturer: Danfoss
          entity:
            domain: climate
    temp_sensor_id:
      name: Temperature Sensor
      description: External sensor from which the temperature will be read. Expects data format 12.3
      selector:
        entity:
          domain: sensor
          device_class: temperature
    max_update_timer_id:
      name: Timer entity
      description: >
        Timer that will trigger at maximum update interval
        if source sensor has not changed.
        Sets automatically to 30min for covered, 3h for uncovered
        Use separate timer for each automation.
      selector:
        entity:
          domain: timer
variables:
  device: !input ally_device
  temp_sensor_id: !input temp_sensor_id
  radiator_covered_state: >
    {{ states(
    device_entities(device)
    |select('match', '.*radiator_covered$')|first) }}
  min_update_minutes: >
    {% if radiator_covered_state == 'off' %}
      30
    {% else %}
      5
    {% endif %}
  max_update_minutes: >
    {% if radiator_covered_state == 'off' %}
      180
    {% else %}
      30
    {% endif %}
trigger:
- platform: state
  entity_id: !input temp_sensor_id
- platform: event
  event_type: timer.finished
  event_data:
    entity_id: !input max_update_timer_id
condition:
- condition: template
  value_template: >
    {{ as_timestamp(now())
    - as_timestamp(state_attr(this.entity_id,'last_triggered'),0)
    > 60 * min_update_minutes }}
action:
- service: mqtt.publish
  data:
    topic: "zigbee2mqtt/{{ device_attr(device, 'name') }}/set/external_measured_room_sensor"
    payload_template: "{{ (states(temp_sensor_id) | float * 100) | round(0) }}"
- service: timer.start
  target:
    entity_id: !input max_update_timer_id
  data:
    duration: "{{ max_update_minutes * 60 }}"
mode: single

I hope these are helpful to someone.

9 Likes

Thank you for this Blueprint. May I ask how I can create this timer helper?

Also I guess if I change manufacturer: Danfoss to manufacturer: Popp I can use this automation with Popp 701721 control via MQTT | Zigbee2MQTT since it’s white-label Danfoss 014G2463.

You can create a timer easily by going to settings => devices => helpers => create helper, and then select “timer”.

And yes, as far as I have heard, the Popp thermostat is identical to Danfoss Ally, so you can use this by changing the “manufacturer” value, or just removing the manufacturer: Danfoss line altogether.

Thank you for the automation, nice, clean, and easy to understand.

I do though have one consideration. If I have an eTRV uncovered and last succesful external temperature update was at 20:00:00.Then another temperature update triggers the automation at 20:11:23 and then again at 20:29:45 but due to min_update_minutes is 30 minutes, then no action is taken. This means eTRV is not updated with external room temperature after 30 minutes, even though external temperature is changed. Next external room sensor update may be half an hour later, i.e., eTRV is not updated after 30 minutes even though external temperature is actually updated.

Have you considered this case? It can most likely be handled in numerous ways, if necessary, but will also add complexity.

Hi,

Thanks for the feedback. Ii is indeed as you say, and I did consider this scenario briefly, but decided the gain was not worth the extra complexity, and I think the behavior in such edge case is negligible, for several reasons.

Basically, if we miss a value or a few due to the minimum refresh time, there are 2 options:

  1. The temperature in the room is relatively stabilized and the missed change was a low amount, such as 20.0 => 20.1 => 20.2 °C. The impact is negligible, even if the eTRV were to have the wrong data, and it shouldn’t be a problem, even if the next change, i.e. 20.2 => 20.3, were to happen another hour from now.
  2. The missed change is more significant, for example the room was cold and is now actively heating up, and the automation missed several temperature updates totaling 0.5 °C, which would be significant change. However, since temperature changes follow a curve, it is very unlikely that room temperature could change by such a large amount in a short time, and then stay completely constant for hours. If there was a drastic change, the next change will also be relatively soon after.

Additionally, and most importantly, when eTRV is in uncovered mode, it still uses the internal temperature sensor to calculate the heating operation. The data from external sensor is used to calibrate the offset between the internal sensor and actual room temperature. So even if the data is relatively old and inaccurate, if the eTRV internal sensor changed by the same difference, the operation is still accurate. This is different from covered mode where the internal sensor is ignored completely for the purpose of determining room temperature, which is why in this mode, the refresh intervals should be shorter (5-30min), while in uncovered mode they can be longer.

Given these considerations, I doubt it is worthwile increasing the complexity of the automation for this, and also I was unable to come up with a simple way to do this for both ZHA and Z2M. I considered using a “restart” mode, and if the minimum time had not passed, simply wait until it has, but that will impact the last_triggered value for when the next run triggers. For Zigbee2MQTT variant, we could reset the remaining timer value so it triggers as soon as the reading can be updated, because we already set the timer from the automation, but this doesn’t work for the ZHA automation, because it depends on an externally set timer value, which we do not want to lose. But again, given the considerations above, I’m not sure it is worth doing this even for Z2M variant. Leaving the time check in the condition, and not triggering the automation at all before the time has passed, makes the whole process much more simple and easy to follow/trace.

1 Like

Hello,
My first post here after some time.
I’m trying to do first automaton using Your blueprint.


My thermometer is BLE_Temp1 Temperature.
Would it be ok if temp value is provided like 2216?

Can we add “open window” sensor support for this blueprint?
Or can You tell me how to modify this automation for open window sensor to stop heating when window is open? and to resume heating when window is closed?

Thanks for this Blueprint napalm. I’m using your Zigbee2MQTT version very successfully with Hive’s UK7004240 TVR’s. It is a re-badged Danfoss Ally (014G2461) with Hive’s modified version of the firmware. It’s been running very smoothly for two weeks without a hitch.

1 Like

Hi,

I hadn’t visited the forum for a while, so I’m not sure if your question is still relevant, but

Yes, this is how Danfoss Ally (and re-branded variants) receive the external temperature - as an integer of hundreds of a degree celsius. So for instance 25.13 should become 2513. My blueprint will convert this automatically.

“Open window” detection is out of scope of this blueprint. For myself, I do have an automation which sends the window open signal to the TRVs though, but I’m not sure if it works 100% as I would like - I think it just prevents the valve from opening more, but I would want the valve to close if window is opened. But maybe I’m missing some understanding how this TRV feature works. I don’t have a blueprint, but I will copy my automation for it here, but you will have to go through it and modify it for your own needs, as it won’t work for your setup as it is. I have a single automation for 3 rooms, which sends the signal to the TRVs in that room, depending on whether the window sensor was opened or closed.

alias: TRV Open Window Control
description: ""
trigger:
  - platform: state
    entity_id:
      - binary_sensor.bedroom_window_contact
    for:
      seconds: 10
    id: bedroom
  - platform: state
    entity_id:
      - binary_sensor.balcony_door_contact
    for:
      seconds: 10
    id: livingroom
  - platform: state
    entity_id:
      - binary_sensor.office_window_1_contact
      - binary_sensor.office_window_2_contact
    for:
      seconds: 10
    id: office
condition: []
action:
  - choose:
      - conditions:
          - condition: trigger
            id: bedroom
          - condition: template
            value_template: "{{ trigger.to_state.state == 'off' }}"
        sequence:
          - service: switch.turn_off
            target:
              entity_id: switch.bedroom_trv_window_open_external
            data: {}
      - conditions:
          - condition: trigger
            id: bedroom
          - condition: template
            value_template: "{{ trigger.to_state.state == 'on' }}"
        sequence:
          - service: switch.turn_on
            target:
              entity_id: switch.bedroom_trv_window_open_external
            data: {}
      - conditions:
          - condition: trigger
            id: livingroom
          - condition: template
            value_template: "{{ trigger.to_state.state == 'off' }}"
          - condition: state
            entity_id: binary_sensor.balcony_door_contact
            state: "off"
        sequence:
          - service: switch.turn_off
            data: {}
            target:
              entity_id:
                - switch.livingroom_trv_1_window_open_external
                - switch.livingroom_trv_2_window_open_external
      - conditions:
          - condition: trigger
            id: livingroom
          - condition: template
            value_template: "{{ trigger.to_state.state == 'on' }}"
        sequence:
          - service: switch.turn_on
            data: {}
            target:
              entity_id:
                - switch.livingroom_trv_1_window_open_external
                - switch.livingroom_trv_2_window_open_external
      - conditions:
          - condition: trigger
            id: office
          - condition: template
            value_template: "{{ trigger.to_state.state == 'off' }}"
          - condition: state
            entity_id: binary_sensor.office_window_1_contact
            state: "off"
          - condition: state
            entity_id: binary_sensor.office_window_2_contact
            state: "off"
        sequence:
          - service: switch.turn_off
            data: {}
            target:
              entity_id:
                - switch.office_trv_1_window_open_external
                - switch.office_trv_2_window_open_external
      - conditions:
          - condition: trigger
            id: office
          - condition: template
            value_template: "{{ trigger.to_state.state == 'on' }}"
        sequence:
          - service: switch.turn_on
            data: {}
            target:
              entity_id:
                - switch.office_trv_1_window_open_external
                - switch.office_trv_2_window_open_external
mode: single

again, this is just for reference. It’s not a blueprint and will not work without modifications for your specific setup.

Are you sure about this? I have the latest firmware and I set the thermostat to “covered”. I used your template and a timer-helper with 10 min interval. I checked if the external room sensor is changed correctly. But even with a 5-degree gap between internal and external sensor, the pi_heat_demand only changes when the internal sensor falls below the threshold.

Btw the script works perfectly fine and updates the thermostat correctly - thank you!

This is not entirely correct. Checking the Danfoss Ally page from zigbee2mqtt: Danfoss 014G2461 control via MQTT | Zigbee2MQTT

For uncovered radiators the minimum refresh rate is 30 minutes and the maximum refresh rate is 3 hours or if the value changes by 0.1K in temperature. For covered radiators the minimum is 5 minutes and the maximum is 30 minutes or if the value changes by 0.1K in temperature.

Which basically means the value can be updated whenever the increment since the last change is bigger or equal to 0.1K. So technically the automation could be used with a condition like this:

condition: or
conditions:
  - condition: trigger
    id: timer
  - condition: and
    conditions:
      - condition: trigger
        id: sensor
      - condition: template
        value_template: >-
          {{
          (((states('number.thermostat_external_measured_room_sensor')|float(0))
          - (((states('sensor.living_room_temperature')|float(0)) *
          100)|round|float)) | abs) > 10 }}
    alias: When triggered by sensor and value changed above or below 0.1K
alias: Either triggered by timer or sensor

This is just an example from my personal automation, for the blueprint it’d need to be updated with the variables of course.

The 10 could also be adjusted to only recognize increments of 0.2 or 0.5 by changing it to 20 or 50.

So if the room temperature drops pretty frequently, the thermostats could be updated way faster by setting a timer of 30 minutes for uncovered radiators and 5 minutes for covered radiators and using this condition and a trigger for the temp sensor to update the externally measured temp way more often so the thermostat can adjust its offset.

1 Like

Hello i use your blueprint also with z2m and it works okay. Thank you for the work! I would like to add the temperature change above 0.1 K also and tried to modify the blueprint, but it didnt work how it should. May someone take a look, why it doesnt do what it should? I added an "or-function and state change from the room sensor, but the automatation did not work.

condition:

  • condition: or
    conditions:
  • condition: template
    value_template: >
    {{ as_timestamp(now())
    • as_timestamp(state_attr(this.entity_id,‘last_triggered’),0)

    60 * min_update_minutes }}

  • condition: template
    value_template: >
    {{ (trigger.to_state.state|int - trigger.from_state.state|int)

    = 10 }}

Hi, sorry for the late response.
Not sure if I am understanding you correctly, but the way I understand it, the refresh period should in either case be between the two values (30min/3h or 5min/30min), even if the thermostat changes more often. This is based on what’s written in Danfoss programming guide. The wording is a bit more clear there:

The temperature measured at the GW device
should be sent “as measured” to all the 3
eTRVs at least every 3 hours but not more
often than every 30 minutes @ every 0,1K
change.

Note the “not more often” part. Similar wording is used when describing “covered” mode.

The temperature measured at the room
sensors device should be sent “as measured”
to all the devices that are in the same room
at least every 30 minutes but not more often
than every 5 minutes @ every 0,1K change.

source

This makes sense, as the temperature sensors often have 0.1K as the minimum measured change, so sending it every 0.1K change could result in need for rapid updates if sensor starts flapping between threshold of 2 neighboring values (i.e. 22.1C and 22.2C). The limit 5/30min prevents this.

hello @napalm, thank you for your work. I also use your blueprints (these one here and room load balancing) and both work good. But i have to agree @shawly, the trv´s really need the 0.1k change or defined time. I checked it with an automation at my office now for 2 days and the room temperature is more pricise in uncovered mode. And it makes sense because, the PID algorithm needs to know, how fast a room “loses” or “gains” heat. If u only actualize in a defined time, p, i, and d parts may not be good calculated. I think this also depends on your house, and how fast a room gets cold or warm, when outer temperature changes.

Thank you for your great work!

May you maybe add those 0.1k change to your blueprint and make a second one of it? Then we would have your default blueprint with

→ min(when temp changed)/max timer based only (yours now)

and a second blueprint
→ min(when temp changed)/max timer based or change by 0.1k(timmer ignored, because or).

Then when could try these both versions and gain experience. Actually, we have cold night and more or less warm days, so it is good to see how the trv´s react.

I have made an automation as suggested, but since 2 days, cant get it templated.

alias: Danfoss Ally Ext Temp Z2M - test bĂźro automation
description: “”
trigger:

  • platform: state
    entity_id: sensor.average_temperature_buro
    id: sensor_event
  • platform: event
    event_type: timer.finished
    event_data:
    entity_id: timer.timer_buero
    condition:
  • condition: or
    conditions:
    • condition: template
      value_template: |-
      {{ as_timestamp(now()) -
      as_timestamp(state_attr(this.entity_id,‘last_triggered’),0) > 60 *
      min_update_minutes }}
    • condition: and
      conditions:
      • condition: trigger
        id: sensor_event
      • condition: template
        value_template: |-
        {{
        (((states(‘number.tcv_buero_external_measured_room_sensor’)|float(0))
        - (((states(‘sensor.average_temperature_buro’)|float(0)) *
        100)|round|float)) | abs) > 10 }}
        action:
  • service: mqtt.publish
    data:
    topic: >-
    zigbee2mqtt/{{ device_attr(device, ‘name’)
    }}/set/external_measured_room_sensor
    payload_template: “{{ (states(temp_sensor_id) | float * 100) | round(0) }}”
  • service: timer.start
    target:
    entity_id: timer.timer_buero
    data:
    duration: “{{ max_update_minutes * 60 }}”
    variables:
    device: 9e5bf48f941eb762b54eba4cc2aaad88
    temp_sensor_id: sensor.average_temperature_buro
    radiator_covered_state: >
    {{ states( device_entities(device) |select(‘match’,
    ‘.*radiator_covered$’)|first) }}
    min_update_minutes: |
    {% if radiator_covered_state == ‘off’ %}
    30
    {% else %}
    5
    {% endif %}
    max_update_minutes: |
    {% if radiator_covered_state == ‘off’ %}
    180
    {% else %}
    30
    {% endif %}
    mode: single

I have the same issue. The external temp is correctly updated in external_measured_room_sensor via the blueprint (thank you for that Napalm!), but the eTRV does not react.

In covered mode, nothing happens. Only a change in local temperature triggers a change on pi_heating_demand. local_temperature and current_temperature both exist, I wonder what is the difference?

In uncovered mode, no offset is applied and no action is triggered. Only a genuine change on the local temp set the heating demand.

The blueprint does what it has to do but there is absolutely no reaction from the eTRV.

Is it the same for you?

@gwbrck did you find a solution since january?

I have the exact same issue in z2mqtt… The TRV is set to covered, and it receives the external_measured_room_sensor and I have multiplied by 100… But it only reacts to it’s internal temp. sensor… There must be a bug somewhere in the driver?

Hi,

thank you @napalm for all the work.

Meanwhile the blueprint seems to be a bit outdated regarding the current home assistant syntax. As I anyhow wanted to be able to react a bit faster on temperature changes like in the example from @shawly in post from Jan 23, I played a bit around and wrote me an own automation for zigbee2mqtt (a blueprint was too much effort in a first step).

It looks like this:

- id: '1730914678777'
  alias: Danfoss external temperature
  description: ''
  triggers:
  - entity_id:
    - sensor.<MY_EXTERNAL_THERMOMETER_TEMPERATURE>
    trigger: state
  - event_type: timer.finished
    event_data:
      entity_id: timer.<MY_MAX_INTERVAL_TIMER>
    trigger: event
  conditions:
  - alias: "Trigger or Temperature"
    condition: or
    conditions:
      - condition: trigger
        id: 1
      - condition: template
        value_template: >-
          {% set externalTemperatureInDanfoss = states('number.<MY_CURRENT_DANFOSS_EXTERNAL_MEASURED_ROMM_SENSOR_VALUE>') | float(0) %}
          {% set externalTemperatureExternal = ((states('sensor.<MY_EXTERNAL_THERMOMETER_TEMPERATURE>') | float(0)) * 100) | round | float %}
          {% set difference = (externalTemperatureInDanfoss - externalTemperatureExternal) | abs %}
          {{ difference > 10 }}
  actions:
  - data:
      topic: "zigbee2mqtt/{{ state_attr('climate.<MY_DANFOSS_TRV_TO_BE_CHANGED>', 'friendly_name')}}/set/external_measured_room_sensor"
      payload: >-
        {{ (states('sensor.<MY_EXTERNAL_THERMOMETER_TEMPERATURE>') | float * 100) | round(0) }}
    action: mqtt.publish
  - target:
      entity_id: timer.<MY_MAX_INTERVAL_TIMER>
    data:
      duration: '{{ 28 * 60 }}'
    action: timer.start
  mode: single

It uses the same approach with the timer for max value (I set it to 28 minutes) but reacts on all changes of the external thermometer which differ more than 0.1°C from the current external_measured_room_sensor of the respective TRV (code: {{ difference > 10 }}).

I just created the automation today. A quick test with changing the set-point to slightly above the external temperature (while the internal one was 2 degrees higher) worked fine. I will observe it for a while before bringing it to my other TRVs.

EDIT:
The code for the temperature changes works well. But the timer code is not doing yet what it should do. The timer gets started and finished correctly and also triggers the automation. But my condition to always update the external_measured_room_sensor in case the timer is the trigger of the automation seems not to work. The condition returns false and so the external_measured_room_sensor is not updated after the max wait time.

This piece of code seems not to work:

      - condition: trigger
        id: timer.<MY_MAX_INTERVAL_TIMER>

I hope I find time tomorrow to investigate it. Should not be a big thing.

EDIT 2:
I fixed it by using the index of the trigger, so now it looks like:

      - condition: trigger
        id: 1

I also changed it here in the post in the original code.