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

Tag: tado

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

    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