Best way to signal Aqara TRVs are "calling for heat" to a Heat Pump (ASHP)

My heating system is based on a Mitsubishi Ecodan Heat Pump (ASHP). The ground floor is UFH running a Heatmiser system. The other 2 floors have radiators with Aqara Smart TRVs.

The UFH part runs well as it has a relay to signal “call for heat” to the ASHP.

For the TRVs, I was initially hoping these would have an obvious attribute like “valve is open”, which would directly translate to “calling for heat”, but apparently I was wrong. I even made a feature request to Aqara, but I was ignored apparently. The only way I managed to integrate it with the rest of the system was by creating a Home Assistant binary sensor that directly controls a relay, which is:

  • ON if any TRV’s current_temperature < temperature (set point)
  • OFF otherwise.

This is a bit sub optimal as I have the impression this sensor is ON most of the time. This is bad for my pocket, so I’m trying to improve it. Anybody has suggestions on how to make it better?

For now my idea is creating a binary sensor for each TRV, that would be:

  • ON if current_temperature < temperature - 0.5ºC.
  • OFF if current_temperature >= temperature + 0.5ºC.
  • STAY UNCHANGED otherwise.

I would update the original binary sensor to be an OR of all individual sensors above.

I’m not sure how to implement the 3rd part (Stay Unchanged). I’ve seen post #342663 (Force Template Sensor to retain value) but I’m not sure it will work well. I’m currently trying to make it work.

I’m also a bit worried the TRV might not exactly work with the -0.5ºC ~ +0.5ºC grace window (hysteresis?). I’m considering verifying it experimentally.

I also considered using a Threshold but apparently I can not set the threshold dynamically (for when someone changes the set temperature).
I also considered using a Derivative sensor, but it seems to be useful only to reduce the frequency of change (which might be useful as an extended goal).

I’m digging into Generic Thermostat as well. Sounds reasonable?

Any suggestions are welcome! Thanks a lot!

Most (perhaps all) climate entities have an attribute hvac_mode or hvac_something that tells if it’s on or not.
I’m on the phone currently so I can’t see the attributes of the climate entities

EDIT; hvac_action

The attributes available for one of my Aqara E1 TRV are:

  • hvac_modes: off, heat
  • min_temp: 5
  • max_temp: 30
  • target_temp_step: 0.5
  • preset_modes: none, manual, away, auto
  • current_temperature: 16.2
  • temperature: 16
  • preset_mode: manual
  • friendly_name: TRV - Office
  • supported_features: 401

So unfortunately I don’t think it exposes the hvac_action.

I think I managed to get it working this way:

1 - Created one custom attribute to represent the initial value of the Hysteresis State for each TRV. In configuration.yaml:

homeassistant:
  customize:
    climate.trv_office:
      hysteresis_state: "off"

Custom attributes do not support template. Hence the following steps.

2 - Create a python script set_attribute.py to update the attribute when needed:

inputEntity = data.get('entity_id')
inputAttribute = data.get('attribute')
inputValue = data.get('value')

inputStateObject = hass.states.get(inputEntity)
inputState = inputStateObject.state
inputAttributesObject = inputStateObject.attributes.copy()

inputAttributesObject[inputAttribute] = inputValue

hass.states.set(inputEntity, inputState, inputAttributesObject)

3 - Create an automation for each TRV so it is updated whenever current_temperature or temperature attributes change:

alias: TRV Hysteresis State - Office
description: ""
triggers:
  - trigger: state
    entity_id:
      - climate.trv_office
    attribute: current_temperature
  - trigger: state
    entity_id:
      - climate.trv_office
    attribute: temperature
conditions: []
actions:
  - action: python_script.set_attribute
    data_template:
      entity_id: climate.trv_office
      attribute: hysteresis_state
      value: >
        {% from 'macros.jinja' import hysteresis_state_for_trv %} {{
        hysteresis_state_for_trv('climate.trv_office', 0.5) }}
mode: single

It uses a macro defined in custom_templates/macros.jinja:

{%- macro hysteresis_state_for_trv(entity, hysteresis) %}
{%- set ent_state = states(entity) %}
{%- set cur_attr_value = state_attr(entity, 'hysteresis_state') %}
{%- set cur_temp = state_attr(entity, 'current_temperature') %}
{%- set set_temp = state_attr(entity, 'temperature') %}
{%- if ent_state == 'unavailable' %}
on
{%- elif ent_state == 'off' %}
off
{%- elif cur_temp | float(100) < set_temp | float(1) - hysteresis %}
on
{%- elif cur_temp | float(0) >= set_temp | float(99) + hysteresis %}
off
{%- elif cur_attr_value == 'unknown' %}
on
{%- else %}
{{ cur_attr_value }}
{%- endif %}
{%- endmacro %}

I was very careful here with default values and conditions to cover HA restart and similar situations. The sensor would always start up calling for heat, until the first run of the automation takes place.

Now everything reacts to this new attribute.

I know I could have done it using a separate entity or helper instead of a attribute. However, having it in the same entity allows me to use it on the UI to customise the colour of the card. I heavily reuse code using decluterring-card, so I can abstract which entity the card is about and everything works as expected. Besides, there is no way to create a custom Climate entity, and this way the entity is a Climate.

One could also implement this with a specific script to make things easier. Something like update_trv_hysteresis_state.py (untested):

inputEntity = data.get('entity_id')
inputHysteresis = data.get('hysteresis')

inputStateObject = hass.states.get(inputEntity)
inputState = inputStateObject.state
inputAttributesObject = inputStateObject.attributes.copy()

curTemp = float(inputAttributesObject['current_temperature'])
setTemp = float(inputAttributesObject['temperature'])

if inputState == None:
  inputAttributesObject'hysteresis_state'] = 'on'
elif cur_temp < set_temp - inputHysteresis:
  inputAttributesObject'hysteresis_state'] = 'on'
elif cur_temp >= set_temp + inputHysteresis:
  inputAttributesObject'hysteresis_state'] = 'off'
elif inputAttributesObject'hysteresis_state']  == 'unknown':
  inputAttributesObject'hysteresis_state'] = 'on'

hass.states.set(inputEntity, inputState, inputAttributesObject)

This is not as careful with start up and unset attributes. So be careful if using it.

What do you think?

Still meaningful:

I’m reasonably sure the device implements a similar hysteresis logic internally. However it’d have to be confirmed experimentally.

The correct value for hysteresis also needs to be confirmed. Some strange scenarios might rise in case it is wrong.

Suppose internally it is 0.5 and we use 1, the device will never reach the upper bound (1 degree above set point), and the sensor will be kinda stuck on heat pretty much indefinitely while internally it transitions to idle midway through. Then temperature will lower gradually until it is 0.5 bellow set temperature. Then the device will switch to heat again. This will repeat indefinitely causing a resource wasteful behaviour ($$).

Ultimately, any parallel implementation of hysteresis - or should I dare to say any implementation for hvac_action - will always conflict/conpete. Aqara should expose the status directly from the device, but unfortunately they don’t. I reached out to them asking for this about an year ago. Maybe I should do it again…

The only way to mitigate this IMHO, is by introducing a delayed response to changes. In my setup I turn my ASHP on/off 10 minutes after changes, just in case. Hopefully it will allow externalinternal behaviours to converge.

I started a similar discussion within Z2M here if anyone is interested.

Check results:

On the top, the original implementation without hysteresis.
On the center, the hysteresis implementation.
On the bottom the switch with 10 minutes delay.

Results are only relevant from around 23:30 forward, as results before were not the final solution.
Notice how much energy is saved. And the warmth feeling is pretty much the same or slightly better with the hysteresis solution (temperature overshoots a bit)…

Which version of zigbee2mqtt are you using?
The doc for the E1 TRV of aqara shows that the system_mode is exposed:

I’m still running 2024.12.5; With version 1.42.0-2 of zigbee2mqtt, here are the full list of attributes in the developer tools, which include the

system_mode: heat

If you upgrade zigbee2mqtt to version 2.0.0-1, it is reduced to:

The funny thing is that when you go to the web UI of zigbee2mqtt and look at the state for your TRV, the system_mode attribute is available in both versions.

It seems that the version 2.0.0 has broken something!

Keep in mind that your method does not account for a TRV to be off.
If you completely turn off one and the room temperature drops below your threshold then the pump will be turned on

I’m running 1.41.0-1 because I’m away from home since end of November.

Indeed Z2M expose the system_mode attribute:

{
    "away_preset_temperature": "5.0",
    "battery": 39,
    "calibrated": true,
    "child_lock": false,
    "device_temperature": 19,
    "internal_heating_setpoint": 17,
    "last_seen": "2025-01-06T14:10:11.821Z",
    "linkquality": 160,
    "local_temperature": 19.6,
    "occupied_heating_setpoint": 17,
    "power_outage_count": 17,
    "preset": "manual",
    "schedule": false,
    "schedule_settings": "mon,sun|0:00,21.0|12:00,17.0|18:00,21.0|23:59,21.0",
    "sensor": "external",
    "setup": false,
    "system_mode": "heat",
    "update": {
        "installed_version": 2590,
        "latest_version": 2590,
        "state": "idle"
    },
    "valve_alarm": true,
    "valve_detection": true,
    "voltage": 2800,
    "window_detection": true,
    "window_open": false
}

However it is not available as a home assistant entity attribute for me. This is all I have in the developer tools:

Which matches your reduced set of attributes as well.
Hence I don’t think the issue started with 2.0.0.

I’m not sure I understand the system_mode meaning.
From the above, we have:

    "device_temperature": 19,
    "local_temperature": 19.6,
    "sensor": "external"
    "system_mode": "heat",

Meaning the setpoint temperatur (internal_heating_setpoint) is below the current temperature. I suppose the latter is device_temperature for external temperature sensor, which is my case as sensor is external, or local_temperature if using local.

If system_mode = heat means the valve is open, something is off as the device should not be calling for heat.

Good catch!
I fixed the macro to cover it.