A low carbon energy consultant mostly posting about home automation, with the occasional energy post thrown in. All views expressed are my own.

Using Home Assistant to integrate 3rd party sensors with your Tado valves

Written by

in

Anyone who’s owned a Tado raditor valve will probably know that its thermometer is, well, a bit optimistic to say the least. By designing them as an all-in-one product, as soon as hot water starts flowing through your pipes the reported temperature of the valves skyrockets. It thinks that the air an inch from your boiling rad is representative of the temperature in your room. Hey genius, it isn’t. This is what mine was doing with a target temp of 23°C:

What happens is your room stays well below the target temperature, in my test case not going above 20°C. Maybe this is how they say their valves save you money? I don’t know. Your valves also constantly open and close as they warm and cool, draining precious battery and generally being a noise nuisance.

Tado have clearly realised this setup isn’t ideal and have since started offering a dedicated Wireless Temperature Sensor. Brilliant, finally, I can… wait, £70 per sensor?! On top of a £70 per room investment with valves already? No thanks guys. So I’m going to show you how to integrate any smart temperature sensor, in this case the fantastic £9 Aqara zigbee one, to your Tado system.

First, you’re going to want to set up Home Assistant. I’m not going to show you how to do this because quite frankly there are a lot of more seasoned, better explained guides out there. Essentially you’ll be running it on a standalone Raspberry Pi, a docker container on an existing machine or one of many other ways. Once set up, Home Assistant should straight away see your Tado devices on your network. Then for this example you’re going to want to give your Home Assistant server zigbee hub functionality which opens up a whole heap of compatible low power local smart sensors. Again, many ways to do this – my preference being a cc2531 USB stick paired to Zigbee2mqtt. This will allow you to connect and pair any zigbee sensor.

Xiaomi’s range of Aqara sensors cover a huge variety of use cases. There’s buttons, motion sensors, leak detectors, door sensors – you name it. For this we’ll be using their Temperature & Humidity sensor which can be picked up for around £9 from Aliexpress, or £12 from ebay for the less adventurous.

Once paired, this means Home Assistant can see both my Tado valves and Aqara sensor(s). Now let’s get them talking to each other! To do this, I’ll add a Generic Thermostat item for each room to my configuration.yaml file:

climate:
  - platform: generic_thermostat
    name: Bathroom
    heater: switch.tado_bathroom
    target_sensor: sensor.bathroom_sensor_temperature
    keep_alive:
      minutes: 30

For ‘target_sensor’ the entity is my Aqara sensor. For ‘heater’, a bit more code in configuration.yaml is needed. I need to create a fake switch:

switch:
  - platform: template
    switches:
      tado_bathroom:
        friendly_name: Tado Bathroom
        turn_on:
          service: tado.set_climate_timer
          data:
            entity_id: climate.bathroom
            temperature: 25
        turn_off:
          service: tado.set_climate_timer
          data:
            entity_id: climate.bathroom
            temperature: 5

This means that my new generic thermostat, when it reaches temperature, will send Tado a target temperature of a chilly 5°C – essentially, turning off. And when the generic thermostat calls for heat it sends the maximum value Tado will allow – this is 25°C.

Already I’m seeing much more consistent and toastier temperatures in my bathroom. The valve and boiler now have more than 75% fewer heating events – not to mention the plain fact that the room is now actually getting to the temperature that it should!

Since only Home Assistant can see the correct temperature, this means all control and more importantly scheduling will be have to done through here rather than through Tado. While this is a little daunting to begin with, the benefits are plenty in that you can have properly customised heating events now. Want the living room to get warm when the TV comes on? No problem! Want the bathroom radiator to switch off when humidity spikes to signal a running shower? Go right ahead!

First, you’ll have to remove all your scheduling you might have already set up with Tado. While you’re at it, add a negative offset to your valves. Tado’s maximum allowed value is 25°C, however the valve measurement is so inaccurate that I’ve seen some rooms reported as above 30°C when in reality they weren’t even 19°C. A negative offset stops Tado from turning the valve off too early.

For scheduling you have plenty of options such as a time-based simple Automation, however for my example I will show my NodeRed flow. With this, I have been able to simulate pre-heat (turns on earlier when room is cooler), weekend/weekday split, frost stat and plenty of other features.

You can import the above nodes by copy and pasting the code below to NodeRed:

[{"id":"892dc05c.97374","type":"time-range-switch","z":"ce507987.cb9dc","name":"","lat":"","lon":"","startTime":"07:00","endTime":"08:00","startOffset":0,"endOffset":"-0.01","x":790,"y":160,"wires":[["119e8c13.034bc4"],["2e137bcb.7c7214"]]},{"id":"119e8c13.034bc4","type":"api-call-service","z":"ce507987.cb9dc","name":"Bathroom 19","server":"24cd78af.855ca8","version":3,"debugenabled":false,"service_domain":"climate","service":"set_temperature","entityId":"climate.bathroom_2","data":"{\"temperature\":\"19\"}","dataType":"json","mergecontext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":970,"y":160,"wires":[[]]},{"id":"4ef037c0.e38748","type":"api-call-service","z":"ce507987.cb9dc","name":"Bathroom 17","server":"24cd78af.855ca8","version":3,"debugenabled":false,"service_domain":"climate","service":"set_temperature","entityId":"climate.bathroom_2","data":"{\"temperature\":\"17\"}","dataType":"json","mergecontext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":990,"y":200,"wires":[[]]},{"id":"2e137bcb.7c7214","type":"time-range-switch","z":"ce507987.cb9dc","name":"","lat":"","lon":"","startTime":"08:00","endTime":"22:00","startOffset":0,"endOffset":"-0.01","x":810,"y":200,"wires":[["4ef037c0.e38748"],["cd240f8d.c0cb38"]]},{"id":"b07046ba.9f796","type":"time-range-switch","z":"ce507987.cb9dc","name":"","lat":"","lon":"","startTime":"09:00","endTime":"10:00","startOffset":0,"endOffset":"-0.01","x":790,"y":320,"wires":[["13cd4f23.c65311"],["c4c02cdd.c2e108"]]},{"id":"13cd4f23.c65311","type":"api-call-service","z":"ce507987.cb9dc","name":"Bathroom 19","server":"24cd78af.855ca8","version":3,"debugenabled":false,"service_domain":"climate","service":"set_temperature","entityId":"climate.bathroom_2","data":"{\"temperature\":\"20\"}","dataType":"json","mergecontext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":970,"y":320,"wires":[[]]},{"id":"44c11579.2c3f1c","type":"api-call-service","z":"ce507987.cb9dc","name":"Bathroom 17","server":"24cd78af.855ca8","version":3,"debugenabled":false,"service_domain":"climate","service":"set_temperature","entityId":"climate.bathroom_2","data":"{\"temperature\":\"17\"}","dataType":"json","mergecontext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":990,"y":360,"wires":[[]]},{"id":"c4c02cdd.c2e108","type":"time-range-switch","z":"ce507987.cb9dc","name":"","lat":"","lon":"","startTime":"10:00","endTime":"22:00","startOffset":0,"endOffset":"-0.01","x":810,"y":360,"wires":[["44c11579.2c3f1c"],["29064d49.bd7a12"]]},{"id":"58009cfb.269024","type":"inject","z":"ce507987.cb9dc","name":"08:00","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"00 08 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":200,"wires":[["45702381.b80edc"]]},{"id":"e0fa1a51.27d268","type":"inject","z":"ce507987.cb9dc","name":"22:00","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"00 22 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":240,"wires":[["45702381.b80edc"]]},{"id":"a42dd191.b20e1","type":"inject","z":"ce507987.cb9dc","name":"22:00","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"00 22 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":360,"wires":[["45702381.b80edc"]]},{"id":"760727c2.bf56a8","type":"comment","z":"ce507987.cb9dc","name":"Bathroom","info":"","x":100,"y":80,"wires":[]},{"id":"ad143f94.088ec","type":"inject","z":"ce507987.cb9dc","name":"10:00","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"00 10 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":320,"wires":[["45702381.b80edc"]]},{"id":"29064d49.bd7a12","type":"api-call-service","z":"ce507987.cb9dc","name":"Bathroom 12","server":"24cd78af.855ca8","version":3,"debugenabled":false,"service_domain":"climate","service":"set_temperature","entityId":"climate.bathroom_2","data":"{\"temperature\": \"12\"}","dataType":"json","mergecontext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1010,"y":400,"wires":[[]]},{"id":"cd240f8d.c0cb38","type":"api-call-service","z":"ce507987.cb9dc","name":"Bathroom 12","server":"24cd78af.855ca8","version":3,"debugenabled":false,"service_domain":"climate","service":"set_temperature","entityId":"climate.bathroom_2","data":"{\"temperature\": \"12\"}","dataType":"json","mergecontext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1010,"y":240,"wires":[[]]},{"id":"fe9a2428.fa4308","type":"comment","z":"ce507987.cb9dc","name":"Bathroom","info":"","x":760,"y":80,"wires":[]},{"id":"977471e5.6c9808","type":"inject","z":"ce507987.cb9dc","name":"04:00 - 07:00 2m","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"*/2 4-6 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":130,"y":160,"wires":[["45702381.b80edc"]]},{"id":"fe605d1a.2eb19","type":"inject","z":"ce507987.cb9dc","name":"06:00 - 09:00 2m","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"*/2 6-8 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":130,"y":280,"wires":[["45702381.b80edc"]]},{"id":"2adbba2f.44f3ce","type":"time-range-switch","z":"ce507987.cb9dc","name":"","lat":"","lon":"","startTime":"04:00","endTime":"07:00","startOffset":0,"endOffset":"-0.01","x":770,"y":120,"wires":[["8d666195.6ae41"],["892dc05c.97374"]]},{"id":"432c7002.7e711","type":"time-range-switch","z":"ce507987.cb9dc","name":"","lat":"","lon":"","startTime":"06:00","endTime":"09:00","startOffset":0,"endOffset":"-0.01","x":770,"y":280,"wires":[["731fe2bb.07df24"],["b07046ba.9f796"]]},{"id":"96bb4703.1c9a58","type":"api-call-service","z":"ce507987.cb9dc","name":"Bathroom PL","server":"24cd78af.855ca8","version":3,"debugenabled":false,"service_domain":"climate","service":"set_temperature","entityId":"climate.bathroom_2","data":"{\"temperature\": {{payload}}}","dataType":"json","mergecontext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1210,"y":280,"wires":[[]]},{"id":"e19c4234.a19268","type":"api-call-service","z":"ce507987.cb9dc","name":"Bathroom PL","server":"24cd78af.855ca8","version":3,"debugenabled":false,"service_domain":"climate","service":"set_temperature","entityId":"climate.bathroom_2","data":"{\"temperature\": {{payload}}}","dataType":"json","mergecontext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1210,"y":120,"wires":[[]]},{"id":"731fe2bb.07df24","type":"function","z":"ce507987.cb9dc","name":"12-19","func":"const globalHomeAssistant = global.get('homeassistant');\nvar today = new Date();\nvar time = today.getHours() + today.getMinutes() / 60;\n\nvar room = \"climate.bathroom_2\"\nvar u1 = 12;\nvar u2 = 19;\nvar t1 = 6;\nvar t2 = 9;\n\nvar temp = u1 + ((time - t1)*(u2-u1))/(t2 - t1) \nvar heat = globalHomeAssistant.homeAssistant.states[room].attributes.hvac_action;\nvar newMsg = { payload: {} };\nif (heat == 'heating') {\n    newMsg.payload = u2\n}\nelse {\n    newMsg.payload = Math.round(Math.min(temp, u2)*2)/2\n}\nreturn newMsg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":930,"y":280,"wires":[["97d5acd1.8a6d4"]]},{"id":"8d666195.6ae41","type":"function","z":"ce507987.cb9dc","name":"12-19","func":"const globalHomeAssistant = global.get('homeassistant');\nvar today = new Date();\nvar time = today.getHours() + today.getMinutes() / 60;\n\nvar room = \"climate.bathroom_2\"\nvar u1 = 12;\nvar u2 = 19;\nvar t1 = 4;\nvar t2 = 7;\n\nvar temp = u1 + ((time - t1)*(u2-u1))/(t2 - t1) \nvar heat = globalHomeAssistant.homeAssistant.states[room].attributes.hvac_action;\nvar newMsg = { payload: {} };\nif (heat == 'heating') {\n    newMsg.payload = u2\n}\nelse {\n    newMsg.payload = Math.round(Math.min(temp, u2)*2)/2\n}\nreturn newMsg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":930,"y":120,"wires":[["d805e2ea.9d826"]]},{"id":"45702381.b80edc","type":"api-current-state","z":"ce507987.cb9dc","name":"If Off","server":"24cd78af.855ca8","version":2,"outputs":2,"halt_if":"Off","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_select.heating_mode","state_type":"str","blockInputOverrides":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"}],"x":390,"y":300,"wires":[["a64cdc6c.9cf038"],["67a7f449.27e95c"]]},{"id":"a64cdc6c.9cf038","type":"api-call-service","z":"ce507987.cb9dc","name":"Bathroom FS","server":"24cd78af.855ca8","version":3,"debugenabled":false,"service_domain":"climate","service":"set_temperature","entityId":"climate.bathroom_2","data":"{\"temperature\": \"12\"}","dataType":"json","mergecontext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":550,"y":200,"wires":[[]]},{"id":"67a7f449.27e95c","type":"weekday","z":"ce507987.cb9dc","name":"","sun":false,"mon":true,"tue":true,"wed":true,"thu":true,"fri":true,"sat":false,"x":580,"y":260,"wires":[["2adbba2f.44f3ce"],["432c7002.7e711"]]},{"id":"b916e79.e77fc18","type":"server-state-changed","z":"ce507987.cb9dc","name":"Heating mode changes","server":"24cd78af.855ca8","version":3,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"input_select.heating_mode","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"","halt_if_type":"str","halt_if_compare":"is","outputs":1,"output_only_on_state_change":true,"for":"","forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":140,"y":120,"wires":[["45702381.b80edc"]]},{"id":"d805e2ea.9d826","type":"switch","z":"ce507987.cb9dc","name":"If > target","property":"payload","propertyType":"msg","rules":[{"t":"gt","v":"$globalContext(\"homeassistant.homeAssistant.states['climate.bathroom_2'].attributes.temperature\")","vt":"jsonata"}],"checkall":"true","repair":false,"outputs":1,"x":1060,"y":120,"wires":[["e19c4234.a19268"]]},{"id":"97d5acd1.8a6d4","type":"switch","z":"ce507987.cb9dc","name":"If > target","property":"payload","propertyType":"msg","rules":[{"t":"gt","v":"$globalContext(\"homeassistant.homeAssistant.states['climate.bathroom_2'].attributes.temperature\")","vt":"jsonata"}],"checkall":"true","repair":false,"outputs":1,"x":1060,"y":280,"wires":[["96bb4703.1c9a58"]]},{"id":"24cd78af.855ca8","type":"server","name":"Home Assistant","version":1,"legacy":false,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true}]

The flows work primarily with a number of timers seen on the left. At each specified time, a flow begins which first asks whether the global heating setting is off. This is optional but recommended, and is a custom input boolean added as follows to the configuration.yaml:

input_boolean:
  heating_main:
    name: Heating
    icon: mdi:thermostat

If this is off, then the pseudo ‘frost stat’ (or FS) is called. This is a call service node with a suitably low temperature set within data:

If this is set to on, a weekday node sends it to either set of timed ranges. For most of these they simply follow the above frost stat example of calling a specific temperature. This differs however with the top node, which calls a custom function. This function is designed to provide a gradual increase in target temperature at the start of a heating event, meaning the heating will switch on earlier when needed. Then, as soon as heating switches on, it changes the target temperature to its maximum. This is to avoid the room temperature being able to catch up to the target temperature, leading to unecessary on/off events.

The code behind this node is as follows. Note the 5 variables hardcoded at the beginning – this provides the function with the room, start temp, end temp, start time and end time.

const globalHomeAssistant = global.get('homeassistant');
var today = new Date();
var time = today.getHours() + today.getMinutes() / 60;

var room = "climate.bathroom_2"
var u1 = 12;
var u2 = 19;
var t1 = 4;
var t2 = 7;

var temp = u1 + ((time - t1)*(u2-u1))/(t2 - t1) 
var heat = globalHomeAssistant.homeAssistant.states[room].attributes.hvac_action;
var newMsg = { payload: {} };
if (heat == 'heating') {
    newMsg.payload = u2
}
else {
    newMsg.payload = Math.round(Math.min(temp, u2)*2)/2
}
return newMsg;

Because this replaces the message’s payload, the next node can then set the temperature to the value of payload:

By now, I have a rich powerful customisable template for one room than can easily be copy and pasted for another. The test case of my bathroom was so successful that I’ve used this setup in another 6 rooms in my home, which have been running successfully for over a year giving it a true all-season test.

Finally, to allow a bit of quick overriding and monitoring for my rooms I have utilised the amazing Button Card addon to easily show all of my rooms on my Home Assistant dashboard. This shows current and target temperatures, lights up red when a room is heating, and utilises the adorable Material Design Icons (code for bathroom given below).

color: auto
color_type: card
entity: climate.bathroom_2
icon: mdi:shower
label: |
  [[[
    return states['climate.bathroom_2'].attributes.temperature + '°C';;
  ]]]
name: |
  [[[
    return Math.round(states['sensor.bathroom_sensor_temperature'].state * 10)/10
  ]]]
show_label: true
show_name: true
show_state: false
state:
  - color: rgb(200, 60, 0)
    operator: template
    value: |
      [[[
        return states['climate.bathroom_2'].attributes
        && (states['climate.bathroom_2'].attributes.hvac_action == 'heating')
      ]]]
styles:
  card:
    - height: 60px
  label:
    - filter: opacity(75%)
      font-size: 10px
  name:
    - font-size: 16px
type: custom:button-card

I really hope even some of these steps come in useful to those of you that might already have Tado and feel frustrated by its limitations. As with all cloud-based services, it worries me that one day Tado will no longer support my devices or will turn off the API, since every time a Tado heating event is sent this has to travel through Tado’s cloud service. However, when this day comes the migration from Tado will be much easier now that the majority of heating events are controlled by Home Assistant – simply find one of the many other radiator valves supported by Home Assistant and update your switches to send values to this instead of Tado.

I’d love to hear your comments if you’ve found this useful, or if there are any items I have overlooked that would make the process simpler or better (for which I expect there will be many!).

Ben

Comments

2 responses to “Using Home Assistant to integrate 3rd party sensors with your Tado valves”

  1. timdonovanuk avatar
    timdonovanuk

    Interestingly, we both arrived at the same solution independently 🙂

    https://community.home-assistant.io/t/tado-set-offset-using-separate-temperature-sensor/288515/22

    I’ve been looking at the Tado core component, as I feel the ability to select an external source for the temp sensor would make sense there. The code is spaghetti though.

    My only concern about this approach is Tado actually has 3 settings when warming your room, low, med, high – i.e. if the room only needs a bit of heat, it’ll only open the valve a bit. With our solution I think you end up with high applied at all times. Not sure what this means regarding wearing out the boiler quicker.

    1. timdonovanuk avatar
      timdonovanuk

      Found a solution to this.

      – platform: generic_thermostat
      name: Study Climate Tado Proxy
      unique_id: study_climate_tado_proxy
      heater: switch.study_heater_tado_proxy
      target_sensor: sensor.study_climate_sensor_temperature
      target_temp_step: 0.5
      keep_alive:
      seconds: 10

      The keep alive here is vital to make sure if you change the target temp on the proxy thermostat, that it is sent to Tado.

      study_heater_tado_proxy:
      turn_on:
      service: tado.set_climate_timer
      target:
      entity_id: climate.study
      data:
      temperature: “{{ state_attr(‘climate.study_climate_tado_proxy’, ‘temperature’) + ( state_attr(‘climate.study’, ‘current_temperature’) – state_attr(‘sensor.study_climate_sensor_temperature’, ‘temperature’) ) | round(1) }}”
      turn_off:
      service: tado.set_climate_timer
      target:
      entity_id: climate.study
      data:
      temperature: 5

      The important part here is the sending of a target temperature to the tado.set_climate_timer service. What I’m doing here is calculating the target temp based on the difference between what Tado thinks it is, and what the external sensor thinks it is. I.e. if Tado thinks it’s 22 degrees, and the external sensor thinks its 20 degrees, and the target temp is 21 degrees, it’ll tell Tado to raise the temp to 23 degrees. keep_alive is needed to continually reassess this maths.

      I think this works well, and will ensure Tado only raises the valve to low/med/high when needed, rather than setting it to 25+ every time on full blast.

      Cheers!

Leave a Reply

Your email address will not be published. Required fields are marked *