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! 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 )
- 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.