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

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)

ZwaveJS however, does allow us to directly send Zwave-commands (by lack of a better description from my side) to our devices, gegardless of what our deviced report what they are capable of. In ZwaveJS you can use the zwave_js.set_value command with a number of parameters. (or the zwave_js.multicast_set_value command to send a command to multiple devices at the same time)

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

service: zwave_js.set_value
data:
  command_class: '64'
  property: mode
  value: 31
target:
  entity_id: climate.<valvename>

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, 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 should be set to ‘off’ or just a bit open. Never set all the valves to 0%!. (if all the valves are closed they can build up pressure somewhere in the pipes. 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: 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 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 %}

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.multicast_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.multicast_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. 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

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.

3 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!