Eurotronic Spirit radiator valves with external temperature sensors in ZwaveJS

Updated on 13-09-2021, as it is no longer needed to work with OpenZwave or MQTT. Comments on the orignal versions are at the bottom of this post
Updated on 14-03-2023: I no longer use the zwave_js.multicast_set_value is it proved unreliable in some cases. I also added the usage of input_boolean helpers, to dynamically define which virtual thermostat can activate the boiler and which can not.

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, which is not always the most ideal way of working (the devices need to support it, and maybe you want to use a Zigbee or other type of thermometer)

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.

Step 1: Switch the TRVs to 'Manufacturer Specific’
ZwaveJS doesn’t support the ‘manufacturer specific’ mode of the Spirit valves, due to a firmware-issue in the valve. To be able to work around this, ZwaveJS either needs to make a code-change specific for this device (which is not a very elegant way of working) or a more complex generic solution needs to be made. (which is on their board, but is complex and invasive to implement, so it will take a lot of time and effort on their side)

ZwaveJS however, does allow us to directly send Zwave-commands (by lack of a better description from my side) to our devices, regardless of what our devices report what they are capable of. In ZwaveJS you can use the zwave_js.set_value command with a number of parameters. (Note: I found that the zwave_js.multicast_set_value command is not entirely reliable in my case)

For each of the valve you apply the following to set them to ‘Manufacturer specific’. Note that Home Assistant translates this to “heat/cool” in the GUI. You should not pay any attention to this behavior.

service: zwave_js.set_value
data:
  command_class: '64'
  property: mode
  value: 31
target:
  entity_id: climate.<valvename>
Background info for those who care :)

Background: Zwave Command class 64, is the command class used by the valve to change the operating mode
Property: “mode” is the name of the property for the operating mode
Value: 31 is the ‘Manufacturer specific’ value for the valve
For completeness:

  • Value 0 is ‘Off’
  • Value 1 is ‘Heating/on’
  • Value 11 is ‘Energy heat’ (which defaults to a preset temperature value)
  • Value 15 is ‘Full power’ (which opens the value to 100% for 30 minutes before returning to 1 automatically)
  • Value 31 is ‘Manufacturer specific’, which is ‘manual operation’. This allows the value to be set to a specific percentage.

Step 2: Set up the Virtual Thermostats:
You will need to create a virtual thermostat (the virtual version of what you probably have mounted on a wall in your living room) called a ‘generic_thermostat’ in HA, which needs a heater (A virtual switch in our case) and a target_sensor (thermometer):

configuration.yaml:

climate: !include climate.yaml

climate.yaml:

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

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 should be set to ‘off’ or just a bit open.
Notes. I am not a specialist, but keep in mind of the following:

  • Never set all the valves to 0%!. (if all the valves are closed they can build up pressure somewhere in the pipes, which can apparently cause physical damage somewhere)
  • You can omit the valve-part (service number.set_value) if you prefer as it will also be done by the part in Step 4. However, that only triggers when a new temperature is measured, so it may take longer before you get your valve in it’s desired state.

configuration.yaml:

switch: !include switch.yaml

switch.yaml:

## Virtuele schakelaars voor thermostaten
platform: template
switches:
  schakelaar_thermostaat_woonkamer:
    turn_on:
      - service: zwave_js.set_value
        data:
          value: 31
          command_class: '64'
          property: mode
        target:
          entity_id: climate.radiatorkraan_woonkamer
      - 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
      - service: zwave_js.set_value
        data:
          value: 0
          command_class: '64'
          property: mode
        target:
          entity_id: climate.radiatorkraan_woonkamer

  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 %}
      - service: zwave_js.set_value
        data:
          value: 31
          command_class: '64'
          property: mode
        target:
          entity_id: climate.radiatorkraan_gang
    turn_off:
      - service: number.set_value
        data:
          entity_id: number.radiatorkraan_gang_valve_control
          value: 5
      - service: zwave_js.set_value
        data:
          value: 0
          command_class: '64'
          property: mode
        target:
          entity_id: climate.radiatorkraan_gang

  schakelaar_thermostaat_slaapkamer:
    turn_on:
      - service: zwave_js.set_value
        data:
          value: 31
          command_class: '64'
          property: mode
        target:
          entity_id: climate.radiatorkraan_slaapkamer
      - 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
      - service: zwave_js.set_value
        data:
          value: 0
          command_class: '64'
          property: mode
        target:
          entity_id: climate.radiatorkraan_slaapkamer

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 changes:

- 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 %}

Step 5 for completeness
I use an input_select to change the behavior of my thermostats (scheduled for daily usage, someone in the house all day (public hollidays etc), or completely off (in the summer/hollidays). Depending on which input_select is active, the valves are either switched to ‘Manufacturer specific’ or ‘Off’

- id: zet_radiatorkranen_uit
  alias: Zet radiatorkranen op 'uit'
  description: ''
  mode: single
  trigger:
   - platform: state
     entity_id: input_select.thermostaat_stand
     to: "Thermostaten uit"
  condition: []
  action:
    service: zwave_js.set_value
    data:
      value: 0
      command_class: '64'
      property: mode
    target:
      entity_id:
      - climate.radiatorkraan_woonkamer
      - climate.radiatorkraan_gang
      - climate.radiatorkraan_slaapkamer

- id: zet_radiatorkranen_aan
  alias: Zet radiatorkranen op 'aan'
  description: ''
  mode: single
  trigger:
    - platform: state
      entity_id: input_select.thermostaat_stand
      from: "Thermostaten uit"
      to:
        - "Standaard"
        - "Hele dag thuis"
  condition: []
  action:
    service: zwave_js.set_value
    data:
      value: 31
      command_class: '64'
      property: mode
    target:
      entity_id:
      - climate.radiatorkraan_woonkamer
      - climate.radiatorkraan_gang
      - climate.radiatorkraan_slaapkamer

I switch my boiler off and on using a relais. I use input_boolean helpers to define which virtual thermostat can actually trigger the boiler. (for example. Only the Woonkamer can activate the boiler, and the Slaapkamer and Gang only tag along, but by switching the input_boolean of the Slaapkamer, you can (temporarily) also maak that room able to trigger the boiler)

- 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: and
    conditions:
    - condition: time
      alias: Alleen overdag CV aan
      after: 06:30
      before: '23:00'
    - condition: not
      alias: Thermostaten staan niet centraal uit
      conditions:
      - condition: state
        entity_id: input_select.thermostaat_stand
        state: Thermostaten uit
    - condition: or
      alias: Een van de thermostaten staat op heating en mag CV inschakelen
      conditions:
      - condition: and
        conditions:
        - condition: state
          entity_id: input_boolean.cv_schakelaar_gang
          state: 'on'
        - condition: state
          entity_id: climate.generieke_thermostaat_gang
          attribute: hvac_action
          state: heating
        alias: Test op gang
      - condition: and
        conditions:
        - condition: state
          entity_id: input_boolean.cv_schakelaar_woonkamer
          state: 'on'
        - condition: state
          entity_id: climate.generieke_thermostaat_woonkamer
          attribute: hvac_action
          state: heating
        alias: Test op woonkamer
      - condition: and
        conditions:
        - condition: state
          entity_id: input_boolean.cv_schakelaar_slaapkamer
          state: 'on'
        - condition: state
          entity_id: climate.generieke_thermostaat_slaapkamer
          attribute: hvac_action
          state: heating
        alias: Test op slaapkamer
  action:
  - service: switch.turn_on
    entity_id: switch.cv_schakelaar
  mode: single
  max_exceeded: silent

- 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_kyra
    platform: state
  - entity_id: climate.generieke_thermostaat_julia
    platform: state
  - entity_id: climate.generieke_thermostaat_kantoor
    platform: state
  - entity_id: climate.generieke_thermostaat_slaapkamer
    platform: state
  condition:
  - condition: and
    conditions:
    - condition: or
      conditions:
      - condition: state
        entity_id: input_boolean.cv_schakelaar_woonkamer
        state: 'off'
      - condition: and
        conditions:
        - condition: state
          entity_id: input_boolean.cv_schakelaar_woonkamer
          state: 'on'
        - condition: state
          entity_id: climate.generieke_thermostaat_woonkamer
          attribute: hvac_action
          state:
          - idle
          - false
      alias: Woonkamer controleren
    - condition: or
      conditions:
      - condition: state
        entity_id: input_boolean.cv_schakelaar_gang
        state: 'off'
      - condition: and
        conditions:
        - condition: state
          entity_id: input_boolean.cv_schakelaar_gang
          state: 'on'
        - condition: state
          entity_id: climate.generieke_thermostaat_gang
          attribute: hvac_action
          state:
          - idle
          - false
      alias: Gang controleren
    - condition: or
      conditions:
      - condition: state
        entity_id: input_boolean.cv_schakelaar_slaapkamer
        state: 'off'
      - condition: and
        conditions:
        - condition: state
          entity_id: input_boolean.cv_schakelaar_slaapkamer
          state: 'on'
        - condition: state
          entity_id: climate.generieke_thermostaat_slaapkamer
          attribute: hvac_action
          state:
          - idle
          - false
      alias: Slaapkamer controleren
    alias: Test of kamers klaar zijn om CV uit te zetten.
  action:
  - service: switch.turn_off
    entity_id: switch.cv_schakelaar
  mode: single
  max_exceeded: silent

If you have any comments or ideas how to make the above simpler/more efficient/cleaner, please let me know! :slight_smile: If you have any questions, just shoot.

The old way of working:

In the original version of this post I used OpenZwave and MQ to change the mode of the valves:

  • 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)

This whole setup had 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 steps above.

6 Likes

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

I updated the how-to so that you can use it without OpenZwave.

I’m about ready to throw my spirit zwave valves in the bin… don’t know what’s up lately but this last few weeks they’ve been like a dodgy set of christmas lights blinking on and off being online then offline… totally unreliable

Sorry to hear that, but thank you for that useful addition to the topic :crazy_face:

Hi Lennard,

This is great. I just implemented it on one of my Spirit valves and so far seems to be doing its job :slight_smile: Still need to add another 5 and the boiler control. But it all looks really promising.

Thanks!

Just made the switch to ZwaveJS, with the only reason to implement this :slight_smile:
Finally control over the valves. Seems to work like a charm. Awesome work! Thanks.

Do i understatnd correctly that setting command class 64 to

  • Value 11 is ‘Energy heat’ (which defaults to a preset temperature value)

Will return the TRV to default behavior and setpoint can be set via HA?

Hi,

Sorry for the late reply. Only just noticed your question.
No: Setting “1” is the ‘default’, which you can use to have HA to a specific temperature.

Energy heat is like a hard preset which you define up front. You can, for example set ‘Energy Heat’ to ‘16’ degrees C.

The idea, is that you don’t have to set the thermostat to 16 degrees, but you can tell it to go to ‘Energy heat’. To me personally, I don’t see the added value of the function, when using it with an automation platform like Home Assistant, but maybe someone else has another view on it.

Hi,

sorry, some very basic question: Where do I input the code to switch to switch the device to manufacturer specific? I’m talking about the part

Blockquote
For each of the valve you apply the following to set them to ‘Manufacturer specific’:

I’m using the ZwaveJS2MQTT integration

Apologies, I only just noticed your reply.

You can send it using the zwave_js.set_value as I wrote in the blockquote below the lines you cited or you can use a standard mqtt client or ozw-admin.

You need the input derived from the mqtt-location, I put in the same blockquote.

Hi, I just wanted to ask if this method is still working, before I attempt to implement it

Apologies. Just only noticed your reply. I did do some stuff on my heating-system recently, but I don’t think I have changed anything on this part. I’ll get back to you later today.