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.

@ChrisRey Any insights about this?

@marcusn Sorry, I was out of business for a while.
I use the system_mode only for reporting whether the TRV is open or not and as a condition for some automations. I never looked at it in relation to the valve.
IN the screenshot I provided, it is indeed strange that it shows ‘open’ if the temperature is above the target one. Maybe this has something to do with the fact that the ‘sensor’ is set to external.
I never looked at it…

The main point I had when starting this discussion was that when moving to version 2, the ‘system_mode’ attribute disappeared.

1 Like

Hi @marcusn,
I’m really new to ZHA and not much programming background. I tried to implement your logic with the help of chatGPT, but it looks like the I’m not getting this new attribute in the climate.termostat_room entity.
What could I have done wrong…

Sorry for the delay answering. Life happened.

You need to enable python scripting for this to work.
So add this to your configuration.yaml file:

python_script:

Then you add the python script set_attribute.py to the python_scripts directory.

Then you need to add the custom attribute to your thermostat entity.
To do so, add this to your configuration.yaml file:

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

Then you’ll need your automation to call the python script to update the attribute.
I suggest you calling the script manually from the developer tools before adding to automations:

  - action: python_script.set_attribute
    data_template:
      entity_id: climate.trv_office
      attribute: hysteresis_state
      value: <on or off>

This should do it.

Sorry for resurrecting this thread.

@ChrisRey, as far as I understand there is no state that clearly translates the concept of “this TRV is calling for heat”. Am I missing something?

These are the configurations available:

system_mode can be:

  • off = No heating at all
  • heat = Device will heat the room as needed

Then preset takes place:

  • manual = Do not change the mode. Keep trying to heat/cool to reach the set temperature.
  • away = do not heat (only avoid freezing)
  • auto = follow a schedule

The raw state for a device with local_temperature < occupied_heating_setpoint is (a.k.a device needs to heat up the room):

{
    "away_preset_temperature": "5.0",
    "device_temperature": 21,
    "internal_heating_setpoint": 19,
    "local_temperature": 18.6,
    "occupied_heating_setpoint": 22,
    "preset": "manual",
    "schedule": false,
    "sensor": "external",
    "system_mode": "heat",
   ...
}

which translates to Home Assistant entity attributes:

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

On the other hand local_temperature > occupied_heating_setpoint is (a.k.a room is warm enough):

{
    "away_preset_temperature": "5.0",
    "device_temperature": 20,
    "internal_heating_setpoint": 19,
    "local_temperature": 18.8,
    "occupied_heating_setpoint": 15,
    "preset": "manual",
    "schedule": false,
    "sensor": "external",
    "system_mode": "heat",
}

which translates to HA entity attributes:

hvac_modes: off, heat
min_temp: 5
max_temp: 30
target_temp_step: 0.5
preset_modes: none, manual, away, auto
current_temperature: 18.8
temperature: 15
preset_mode: manual
friendly_name: TRV - Back Bedroom
supported_features: 401

My understanding of the properties is:

  • local_temperature: The current temperature of the room for all purposes
  • occupied_heating_setpoint: The desired temperature in the room
  • internal_heating_setpoint: I’m not sure. Do you know?
  • device_temperature: How hot the device itself is. Not relevant here.

The Heat pump probably have temp sensor on the return pipe. Opening the radiator s will result in a temperature drop.

This is an interesting possibility. I’ll explore it.
Thanks

Actually, it doesn’t help because the temperature drop will only happen if the heat pump is running.
That’s exactly what I’m trying to do, make it run when valves open.

The radiators are also in a closer loop with a great exchanger block. The heat pump loop is another closed loop connected to the other side of the heat exchanger block.

(schematic generated with chatgpt. Don’t focus too much in the details)