Eurotronic Spirit radiator valves with external temperature sensors in ZwaveJS

Tags: #<Tag:0x00007fc409cb78c8> #<Tag:0x00007fc409cb7800>

I have set up my home heating configuration based upon several items that I found across the forum. I have amended them all to my own need. It consists of a few parts that work together fine.

Background
The Eurotronic Spirit Thermostatic Radiator Valves (TRV from now on) have the ability to use an external thermometer (which can make sense, since reading the temperature right next to the radiator is not the most ideal solution on some cases). This however has some limits. One of which requires the thermometer to also be a zwave-device in the same zwave-group. (disclaimer. I don’t really understand how it would work, but apparently the required functionality isn’t supported in ZwaveJS and OpenZwave yet)

Since the external thermometer wasn’t a viable solution for me, I decided to make something that I can use to directly control the TRVs

I used to work with OpenZwave and pushed MQTT messages to change the valve position which worked fine. I however wanted to migrate to ZwaveJS and have amended my solution to the following. There are probably easier ways to do it, but this is how I got everything working for me :slight_smile:

Step 1:Use OpenZwave to set the TRVs to 'Manufacturer Specific’
ZwaveJS does currently not support setting the TRVs to that mode.. A workaround for that is in the pipeline, but no indicative date for it.
I’m not going in details for that. Just use the GUI of OpenZwave. (using ozw-admin). If people really want, I’ll add some screenshots, but it should be fairly self-explanotory. But for completeness:

  • Disable the ZwaveJs integration (which should also shut down the ZwaveJS addon)
  • Start up an MQTT broker (if it isn’t running already)
  • Start up OpenZwave
  • Change the TRVs to be set on the Manufacturer Specific mode via ozw-admin or mqtt-commands
  • Shut down OpenZwave
  • Shut down MQTT broker (unless you use it for something else of course :slight_smile: )
  • Enable the ZwaveJS integration (which should also start the ZwaveJS addon)

Step 2: Set up the Virtual Thermostats:
You will need to create a virtual thermostat, which needs a heater (switch) and a target_sensor (thermometer):

configuration.yaml:

climate: !include climate.yaml

climate.yaml:

## Virtuele thermostaten voor aansturing
- platform: generic_thermostat
  name: Generieke thermostaat woonkamer
  heater: switch.schakelaar_thermostaat_woonkamer
  target_sensor: sensor.beweging_woonkamer_temperature
- platform: generic_thermostat
  name: Generieke thermostaat gang
  heater: switch.schakelaar_thermostaat_gang
  target_sensor: sensor.beweging_gang_air_temperature
- platform: generic_thermostat
  name: Generieke thermostaat slaapkamer
  heater: switch.schakelaar_thermostaat_slaapkamer
  target_sensor: sensor.beweging_slaapkamer_temperature

Step 3: Set up virtual heaters (switches)
The switches ‘turn on’ (which is what the virtual thermostat wants), by changing the position of the TRV. Effectively by calling a number.set_value with to the entity_id of the TRV’s valve-position-entity with a value of how far the valve should open. It opens to a certain level depending on how much the ‘desired’ temperature deviates from the actual temperature’ to prevent ‘overheating’. So the closer the temperature gets to the
desired temperature, the lower the value of the radiator-valve. These values should be tweaked ‘per room’. In my example, they are all the same. If the desired temperature is 5 degrees lower than the actual temperature, the valve opens 99%. If it is 3 degrees lower, the valve open 85%, etc. etc. If you ‘switch off’ the virtual thermostat, the value shouldn’t be closed completely as appearently this can damage some boilers. (if all the valves are closed that can build up pressure somewhere. I am no specialist, but this is what I heard somewhere. Of course: Do as you will :slight_smile: - I take no responsibility)
configuration.yaml:

switch: !include switch.yaml

switch.yaml:

## Virtuele schakelaars voor thermostaten
platform: template
switches:
  schakelaar_thermostaat_woonkamer:
    turn_on:
      service: number.set_value
      data_template:
        entity_id: number.radiatorkraan_woonkamer_valve_control
        value: >
          {% if states('sensor.beweging_woonkamer_temperature') | float - state_attr("climate.generieke_thermostaat_woonkamer", "temperature") | float <= -5 %} 99
          {% elif states('sensor.beweging_woonkamer_temperature') | float - state_attr("climate.generieke_thermostaat_woonkamer", "temperature") | float <= -3 %} 85
          {% elif states('sensor.beweging_woonkamer_temperature') | float - state_attr("climate.generieke_thermostaat_woonkamer", "temperature") | float <= -2 %} 60
          {% elif states('sensor.beweging_woonkamer_temperature') | float - state_attr("climate.generieke_thermostaat_woonkamer", "temperature") | float <= -1 %} 50
          {% elif states('sensor.beweging_woonkamer_temperature') | float - state_attr("climate.generieke_thermostaat_woonkamer", "temperature") | float <= 0.5 %} 35
          {% elif states('sensor.beweging_woonkamer_temperature') | float - state_attr("climate.generieke_thermostaat_woonkamer", "temperature") | float >= 0.5 %} 0
          {% else %} 10
          {% endif %}
    turn_off:
      service: number.set_value
      data:
        entity_id: number.radiatorkraan_woonkamer_valve_control
        value: 5
  schakelaar_thermostaat_gang:
    turn_on:
      service: number.set_value
      data_template:
        entity_id: number.radiatorkraan_gang_valve_control
        value: >
          {% if states('sensor.beweging_gang_air_temperature') | float - state_attr("climate.generieke_thermostaat_gang", "temperature") | float <= -5 %} 99
          {% elif states('sensor.beweging_gang_air_temperature') | float - state_attr("climate.generieke_thermostaat_gang", "temperature") | float <= -3 %} 85
          {% elif states('sensor.beweging_gang_air_temperature') | float - state_attr("climate.generieke_thermostaat_gang", "temperature") | float <= -2 %} 60
          {% elif states('sensor.beweging_gang_air_temperature') | float - state_attr("climate.generieke_thermostaat_gang", "temperature") | float <= -1 %} 50
          {% elif states('sensor.beweging_gang_air_temperature') | float - state_attr("climate.generieke_thermostaat_gang", "temperature") | float <= -0.5 %} 35
          {% elif states('sensor.beweging_gang_air_temperature') | float - state_attr("climate.generieke_thermostaat_gang", "temperature") | float >= 0.5 %} 0
          {% else %} 10
          {% endif %}
    turn_off:
      service: number.set_value
      data:
        entity_id: number.radiatorkraan_gang_valve_control
        value: 5

  schakelaar_thermostaat_slaapkamer:
    turn_on:
      service: number.set_value
      data_template:
        entity_id: number.radiatorkraan_slaapkamer_valve_control
        value: >
          {% if states('sensor.beweging_slaapkamer_temperature') | float - state_attr("climate.generieke_thermostaat_slaapkamer", "temperature") | float <= -5 %} 99
          {% elif states('sensor.beweging_slaapkamer_temperature') | float - state_attr("climate.generieke_thermostaat_slaapkamer", "temperature") | float <= -3 %} 85
          {% elif states('sensor.beweging_slaapkamer_temperature') | float - state_attr("climate.generieke_thermostaat_slaapkamer", "temperature") | float <= -2 %} 60
          {% elif states('sensor.beweging_slaapkamer_temperature') | float - state_attr("climate.generieke_thermostaat_slaapkamer", "temperature") | float <= -1 %} 50
          {% elif states('sensor.beweging_slaapkamer_temperature') | float - state_attr("climate.generieke_thermostaat_slaapkamer", "temperature") | float <= 0.5 %} 35
          {% elif states('sensor.beweging_slaapkamer_temperature') | float - state_attr("climate.generieke_thermostaat_slaapkamer", "temperature") | float >= 0.5 %} 0
          {% else %} 10
          {% endif %}
    turn_off:
      service: number.set_value
      data:
        entity_id: number.radiatorkraan_slaapkamer_valve_control
        value: 5

Step 4: Change the valves as the temperature changes
In the step above, only the initial valve position is set. This will remain static as long as the virtual thermostat is ‘on’ so I created an automation that effective does the same as above, but is triggered when the reported temperatures change:

- id: radiatorknop_woonkamer_zet_goed
  alias: Radiatorknop goedzetten woonkamer
  description: Zet de thermostaat op een goede stand in woonkamer
  initial_state: on
  mode: single
  trigger:
    - platform: state
      entity_id: sensor.beweging_woonkamer_temperature
  condition:
    - condition: state
      entity_id: climate.generieke_thermostaat_woonkamer
      state: heat
  action:
      service: number.set_value
      data_template:
        entity_id: number.radiatorkraan_woonkamer_valve_control
        value: >
          {% if states('sensor.beweging_woonkamer_temperature') | float - state_attr("climate.generieke_thermostaat_woonkamer", "temperature") | float <= -5 %} 99
          {% elif states('sensor.beweging_woonkamer_temperature') | float - state_attr("climate.generieke_thermostaat_woonkamer", "temperature") | float <= -3 %} 80
          {% elif states('sensor.beweging_woonkamer_temperature') | float - state_attr("climate.generieke_thermostaat_woonkamer", "temperature") | float <= -2 %} 60
          {% elif states('sensor.beweging_woonkamer_temperature') | float - state_attr("climate.generieke_thermostaat_woonkamer", "temperature") | float <= -1 %} 50
          {% elif states('sensor.beweging_woonkamer_temperature') | float - state_attr("climate.generieke_thermostaat_woonkamer", "temperature") | float <= 0.5 %} 35
          {% elif states('sensor.beweging_woonkamer_temperature') | float - state_attr("climate.generieke_thermostaat_woonkamer", "temperature") | float >= 0.5 %} 0
          {% else %} 10
          {% endif %}

- id: radiatorknop_gang_zet_goed
  alias: Radiatorknop goedzetten gang
  description: Zet de thermostaat op een goede stand in gang
  initial_state: on
  mode: single
  trigger:
    - platform: state
      entity_id: sensor.beweging_gang_air_temperature
  condition:
    - condition: state
      entity_id: climate.generieke_thermostaat_gang
      state: heat
  action:
      service: number.set_value
      data_template:
        entity_id: number.radiatorkraan_gang_valve_control
        value: >
          {% if states('sensor.beweging_gang_air_temperature') | float - state_attr("climate.generieke_thermostaat_gang", "temperature") | float <= -5 %} 99
          {% elif states('sensor.beweging_gang_air_temperature') | float - state_attr("climate.generieke_thermostaat_gang", "temperature") | float <= -3 %} 85
          {% elif states('sensor.beweging_gang_air_temperature') | float - state_attr("climate.generieke_thermostaat_gang", "temperature") | float <= -2 %} 60
          {% elif states('sensor.beweging_gang_air_temperature') | float - state_attr("climate.generieke_thermostaat_gang", "temperature") | float <= -1 %} 50
          {% elif states('sensor.beweging_gang_air_temperature') | float - state_attr("climate.generieke_thermostaat_gang", "temperature") | float <= 0.5 %} 35
          {% elif states('sensor.beweging_gang_air_temperature') | float - state_attr("climate.generieke_thermostaat_gang", "temperature") | float >= 0.5 %} 0
          {% else %} 10
          {% endif %}

- id: radiatorknop_slaapkamer_zet_goed
  alias: Radiatorknop goedzetten Slaapkamer
  description: Zet de thermostaat op een goede stand in slaapkamer
  initial_state: on
  mode: single
  trigger:
    - platform: state
      entity_id: sensor.beweging_slaapkamer_temperature
  condition:
    - condition: state
      entity_id: climate.generieke_thermostaat_slaapkamer
      state: heat
  action:
      service: number.set_value
      data_template:
        entity_id: number.radiatorkraan_slaapkamer_valve_control
        value: >
          {% if states('sensor.beweging_slaapkamer_temperature') | float - state_attr("climate.generieke_thermostaat_slaapkamer", "temperature") | float <= -5 %} 99
          {% elif states('sensor.beweging_slaapkamer_temperature') | float - state_attr("climate.generieke_thermostaat_slaapkamer", "temperature") | float <= -3 %} 85
          {% elif states('sensor.beweging_slaapkamer_temperature') | float - state_attr("climate.generieke_thermostaat_slaapkamer", "temperature") | float <= -2 %} 60
          {% elif states('sensor.beweging_slaapkamer_temperature') | float - state_attr("climate.generieke_thermostaat_slaapkamer", "temperature") | float <= -1 %} 50
          {% elif states('sensor.beweging_slaapkamer_temperature') | float - state_attr("climate.generieke_thermostaat_slaapkamer", "temperature") | float <= 0.5 %} 35
          {% elif states('sensor.beweging_slaapkamer_temperature') | float - state_attr("climate.generieke_thermostaat_slaapkamer", "temperature") | float >= 0.5 %} 0
          {% else %} 10
          {% endif %}

For completeness. I switch my boiler off and on using a relais. The hallway (gang) is hashed out as I don’t the hallway to actually trigger the boiler. It only heats along if on of the other rooms requires heat.

- id: cv_aan_voor_warmte
  alias: CV aan voor warmte
  description: Schakelt de CV aan als er wamte nodig is.
  trigger:
  - entity_id: climate.generieke_thermostaat_woonkamer
    platform: state
#  - entity_id: climate.generieke_thermostaat_gang
#    platform: state
  - entity_id: climate.generieke_thermostaat_slaapkamer
    platform: state
  condition:
    condition: or
    conditions:
      - condition: state
        entity_id: climate.generieke_thermostaat_woonkamer
        attribute: hvac_action
        state: heating
#      - condition: state
#        entity_id: climate.generieke_thermostaat_gang
#        attribute: hvac_action
#        state: heating
      - condition: state
        entity_id: climate.generieke_thermostaat_slaapkamer
        attribute: hvac_action
        state: heating
  action:
  - service: switch.turn_on
    entity_id: switch.cv_schakelaar

# CV uit voor verwarming (alle gemeten thermostaten moeten uit warmer zijn dan de ingestelde temperatuur)
- id: cv_uit_voor_warmte
  alias: CV uit voor warmte
  description: Schakelt de CV uit als er wamte nodig is.
  trigger:
  - entity_id: climate.generieke_thermostaat_woonkamer
    platform: state
#  - entity_id: climate.generieke_thermostaat_gang
#    platform: state
  - entity_id: climate.generieke_thermostaat_slaapkamer
    platform: state
  condition:
    condition: and
    conditions:
#      - condition: state
#        entity_id: climate.generieke_thermostaat_gang
#        attribute: hvac_action
#        state:
#        - idle
#        - off
      - condition: state
        entity_id: climate.generieke_thermostaat_woonkamer
        attribute: hvac_action
        state:
        - idle
        - off
      - condition: state
        entity_id: climate.generieke_thermostaat_slaapkamer
        attribute: hvac_action
        state:
        - idle
        - off
  action:
  - service: switch.turn_off
    entity_id: switch.cv_schakelaar

Small caveat
This whole setup has one downside. And that is that you should NOT touch the ‘mode’ of your TRVs. If change the mode, then you will need to redo Step 1. (for as long as ZwaveJS is not able to change the mode to ‘Manufacturer Specific’.

Please let me know if you miss something or have suggestions to make this whole setup better/more efficient.

Edit: Missed a few typos during copy-pasting :slight_smile:

1 Like

so if they enable the pipeline solution this should be pretty easy?

Yes, when they add that functionality, the NodeJS-module can also change settings that the devices don’t advertise they support. Ideally Eurotronic would provide a firmware-update to ‘fix’ this, but that is very unlikely.

1 Like