universal thermostat for (underfloor/fan) heater
Overview
You can use this flow to create your own thermostat by using a temperature sensor plus a switch, which turn your heating device on or off (e.g Shelly plug S to control a infrared heating panel or Shelly RGBW2 to control up to four 24V floor heating valves)
There is enough room for improvement of the flow, but a goal was to keep it simple and adaptable. Feel free to test it and make your own changes!
Please buy me a coffee if this flow saves time (to create your own flow) and/or energy costs ! :-)
Features
- it's optimised to control it by json based web thermostat plugins for homebridge
{
"currentTemperature":25,
"targetHeatingCoolingState":1,
"currentHeatingCoolingState":1,
"targetTemperature":30
}
- you can add many window sensors as you like or have :-)
- Calendar (CalDav) based heating control is also possible
- heat logic script includes hysteresis parameter ( currently set to 0.2)
- input/outpus via HTML (optional mqtt)
Example
Here is an example of a underfloor heating flow. It shows some important steps, but not all. So basic node-red understanding is required.
1) Parts
- Shelly RGBW2 in white mode (2nd one to control 8 Channels)
- 24V underfloor heating valve (e.g. Möhlenhoff Alpha 5 24V NC)
- 24V Poweradapter (+ some wires)
- Temperature Sensor
- Window Sensor (optional)
- instance, where you can run Node-Red (and homebridge)
- needed nodes in Node-Red: node-red-contrib-boolean-logic-ultimate, node-red-contrib-ical-events)
- CalDav Calendar (I'm using one from a NextCloud instance)
2) Configuration
- Add required nodes to Node-Red
- Add the URLs off your Shelly RGBW2
- Ch1 on: http://192.168.xxx.xxx/white/0?turn=on&brightness=100
- Ch1 off: http://192.168.xxx.xxx/white/0?turn=off
- Ch2 on: http://192.168.xxx.xxx/white/1?turn=on&brightness=100
- Ch2 off: http://192.168.xxx.xxx/white/1?turn=off
...
set up a temp sensor (currently no example available)
Configure Calendar (optional)
- set-up a calDav Calendar to the ical node
If "targetHeatingCoolingState" is set to "3" the flow checks the linked calendar for entries. In the location field of a calendar entry you can add the target temperature. See the example in the flow for better understanding.
- set up Window Sensor (optional, example: Shelly Door/Window)
- insert reporting URL to the Node-Red Http End-Point
- For testing I added example inject-nodes. So it is easier to understand how it works and you can add your own input/output nodes.
[{"id":"b496e091.c65da8","type":"tab","label":"Thermostat_livingroom","disabled":false,"info":""},{"id":"a13df985.2bb858","type":"http request","z":"b496e091.c65da8","name":"Shelly A02 on","method":"GET","ret":"txt","paytoqs":"ignore","url":"http://192.168.xxx.xxx/white/0?turn=on&brightness=100","tls":"","persist":false,"proxy":"","authType":"","x":1100,"y":240,"wires":[[]]},{"id":"2df37a2f.4a992e","type":"http request","z":"b496e091.c65da8","name":"Shelly A02 off","method":"GET","ret":"txt","paytoqs":"ignore","url":"http://192.168.xxx.xxx/white/0?turn=off","tls":"","persist":false,"proxy":"","authType":"","x":1100,"y":280,"wires":[[]]},{"id":"9764a8fb.5e615","type":"ical-sensor","z":"b496e091.c65da8","d":true,"confignode":"","timeout":"60","timeoutUnits":"seconds","name":"Heater","trigger":"always","timezone":"","filterProperty":"summary","filterOperator":"between","combineResponse":false,"filter2":"","filter":"","x":210,"y":360,"wires":[["b608e9f1.2a5e3"],[]]},{"id":"ef172a20.80fff8","type":"http in","z":"b496e091.c65da8","name":"App: status","url":"/temp-dev/status","method":"get","upload":false,"swaggerDoc":"","x":730,"y":640,"wires":[["21fd74af.c472c4"]]},{"id":"1cc11aa.65d3ce5","type":"http in","z":"b496e091.c65da8","name":"App: targetHeatingCoolingState","url":"/temp-dev/targetHeatingCoolingState","method":"get","upload":false,"swaggerDoc":"","x":190,"y":500,"wires":[["c64971a9.51982"]]},{"id":"21c8c5a.f1c1c3a","type":"http in","z":"b496e091.c65da8","name":"App: targetTemperature","url":"/temp-dev/targetTemperature","method":"get","upload":false,"swaggerDoc":"","x":220,"y":740,"wires":[["80b58cba.3eef7"]]},{"id":"cab3896e.a53458","type":"delay","z":"b496e091.c65da8","name":"","pauseType":"delay","timeout":"2","timeoutUnits":"seconds","rate":"1","nbRateUnits":"","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":940,"y":280,"wires":[["2df37a2f.4a992e"]]},{"id":"c64971a9.51982","type":"template","z":"b496e091.c65da8","name":"targetHeatingCoolingState","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{ \"targetHeatingCoolingState\": {{req.query.value}} }","output":"json","x":475,"y":720,"wires":[["567787a2.ac8028","dfd5743.e1abc08"]],"l":false},{"id":"dfd5743.e1abc08","type":"http response","z":"b496e091.c65da8","name":"","statusCode":"","headers":{},"x":610,"y":760,"wires":[]},{"id":"567787a2.ac8028","type":"function","z":"b496e091.c65da8","name":"Heat Logic","func":"var T1 = context.get(\"T1\")|| msg.payload\n\n\nif (msg.topic === 'setState_C') {\n context.set('state_C', msg.payload);\n }\nif (msg.topic === 'setTarget_C') {\n context.set('target_C', msg.payload);\n }\nif (msg.payload.targetHeatingCoolingState !== undefined){\n\tcontext.set('T1.targetHeatingCoolingState', msg.payload.targetHeatingCoolingState);\n\t}\nif (msg.payload.currentTemperature !== undefined) {\n\tcontext.set('T1.currentTemperature', msg.payload.currentTemperature);\n\t}\nif (msg.payload.currentHeatingCoolingState !== undefined) {\n\tcontext.set('T1.currentHeatingCoolingState', msg.payload.currentHeatingCoolingState);\n\t}\nif (msg.payload.targetTemperature !== undefined) {\n context.set('T1.targetTemperature', msg.payload.targetTemperature);\n\t}\nif (msg.topic === 'WindowState') {\n context.set('WindowState', msg.payload);\n }\n\nconst state_C = context.get('state_C') === true;\nconst target_C = context.get('target_C') || T1.targetTemperature;\nconst WindowState = context.get('WindowState') !== true;\nconst hysteresis = 0.2;\nvar heat;\n\n\n\n\nif (!WindowState || T1.targetHeatingCoolingState === 0) {\n T1.currentHeatingCoolingState = 0;\n heat = false;\n} else { \n switch(T1.targetHeatingCoolingState) {\n \tcase 1:\n \t T1.currentHeatingCoolingState = 0\n // Heater off\n \tif (T1.currentTemperature >= (T1.targetTemperature)) {\n heat = false;\n T1.currentHeatingCoolingState = 0;\n }\n // Heater on\n \tif (T1.currentTemperature < (T1.targetTemperature - hysteresis)) {\n heat = true;\n T1.currentHeatingCoolingState = 1;\n \t}\n break;\n \tcase 3:\n \t\tif (state_C === true) \n \t\t {\n \t\t\tT1.targetTemperature = target_C;\n \t\t// Heater off\n \t\tif (T1.currentTemperature >= (T1.targetTemperature)) {\n \theat = false;\n \tT1.currentHeatingCoolingState = 0;\n \t}\n \t\t// Heater on\n \t\tif (T1.currentTemperature < (T1.targetTemperature - hysteresis)) {\n \theat = true;\n \tT1.currentHeatingCoolingState = 1;\n \t }\n \t\t}\n if (state_C === false) {\n \t\t\t T1.targetTemperature = target_C;\n heat = false;\n T1.currentHeatingCoolingState = 2;\n }\n break;\n \tdefault:\n // code block\n }\n}\nreturn [{payload: heat}, {payload: T1}];","outputs":2,"noerr":0,"initialize":"","finalize":"","libs":[],"x":630,"y":320,"wires":[["d5c3b3c4.cfca5","25a4a9f8dc1f16b6"],["aa00030f.bbe9e8"]]},{"id":"80b58cba.3eef7","type":"template","z":"b496e091.c65da8","name":"targetTemperature","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{ \"targetTemperature\": {{req.query.value}} }","output":"json","x":495,"y":760,"wires":[["567787a2.ac8028","dfd5743.e1abc08"]],"l":false},{"id":"3202bc84.cb8294","type":"change","z":"b496e091.c65da8","name":"","rules":[{"t":"set","p":"mydata","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":800,"y":380,"wires":[["25a4a9f8dc1f16b6"]]},{"id":"508b7c96.708b54","type":"http response","z":"b496e091.c65da8","name":"","statusCode":"","headers":{},"x":1050,"y":640,"wires":[]},{"id":"21fd74af.c472c4","type":"change","z":"b496e091.c65da8","name":"get flow.mydata","rules":[{"t":"set","p":"payload","pt":"msg","to":"mydata","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":900,"y":640,"wires":[["508b7c96.708b54"]]},{"id":"aa00030f.bbe9e8","type":"rbe","z":"b496e091.c65da8","name":"","func":"rbe","gap":"","start":"","inout":"out","property":"payload","x":675,"y":380,"wires":[["3202bc84.cb8294"]],"l":false},{"id":"b608e9f1.2a5e3","type":"function","z":"b496e091.c65da8","name":"CheckCal","func":"const state = msg.payload.on === true;\nvar msg1 = { payload: msg.payload.on };\nvar msg2 = { payload: Number(msg.payload.location) };\nif (state) {\nmsg1.topic = \"setState_C\";\nmsg1.payload = true;\nmsg2.topic = \"setTarget_C\";\nreturn [msg1, msg2];\n} else { \nmsg1.topic = \"setState_C\";\nmsg1.payload = false; \nreturn [msg1, null];\n}","outputs":2,"noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":360,"wires":[["567787a2.ac8028"],["567787a2.ac8028"]]},{"id":"58b4aec0.ea3d2","type":"change","z":"b496e091.c65da8","name":"get flow.mydata","rules":[{"t":"set","p":"payload","pt":"msg","to":"mydata","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":1020,"y":700,"wires":[[]]},{"id":"60a11e40.64a88","type":"inject","z":"b496e091.c65da8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"180","crontab":"","once":true,"onceDelay":"10","topic":"","payloadType":"date","x":850,"y":700,"wires":[["58b4aec0.ea3d2"]]},{"id":"d5c3b3c4.cfca5","type":"rbe","z":"b496e091.c65da8","name":"","func":"rbe","gap":"","start":"","inout":"out","property":"payload","x":635,"y":260,"wires":[["d6411535.f5dd08"]],"l":false},{"id":"e948e949.ab14b","type":"BooleanLogicUltimate","z":"b496e091.c65da8","name":"check Windows","filtertrue":"both","persist":true,"sInitializeWith":"false","triggertopic":"setWindow","outputtriggeredby":"all","inputCount":"2","topic":"WindowState","restrictinputevaluation":false,"x":496,"y":200,"wires":[[],["567787a2.ac8028"],[]],"icon":"font-awesome/fa-window-restore","l":false},{"id":"d6411535.f5dd08","type":"FilterUltimate","z":"b496e091.c65da8","name":"control","x":750,"y":260,"wires":[["beac9c1a92a32786"],["cab3896e.a53458"]]},{"id":"64971618ba3fc5c5","type":"inject","z":"b496e091.c65da8","name":"Window2 open","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"setWindow2","payload":"true","payloadType":"bool","x":320,"y":200,"wires":[["e948e949.ab14b"]]},{"id":"ea464be936e75b53","type":"inject","z":"b496e091.c65da8","name":"Window2 Close","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"setWindow2","payload":"false","payloadType":"bool","x":320,"y":240,"wires":[["e948e949.ab14b"]]},{"id":"6f6de497.bb68d4","type":"inject","z":"b496e091.c65da8","name":"currentTemperature 25","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"{\"currentTemperature\":25}","payloadType":"json","x":300,"y":280,"wires":[["567787a2.ac8028"]]},{"id":"a3dfcbe961c8eed1","type":"inject","z":"b496e091.c65da8","name":"targetHeatingCoolingState 1 (on)","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"{\"targetHeatingCoolingState\":1}","payloadType":"json","x":220,"y":580,"wires":[["567787a2.ac8028"]]},{"id":"1832ef0f033282bb","type":"inject","z":"b496e091.c65da8","name":"targetHeatingCoolingState 0 (off)","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"targetHeatingCoolingState\":0}","payloadType":"json","x":210,"y":540,"wires":[["567787a2.ac8028"]]},{"id":"25a4a9f8dc1f16b6","type":"debug","z":"b496e091.c65da8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1090,"y":340,"wires":[]},{"id":"653039ce9d7353e5","type":"inject","z":"b496e091.c65da8","name":"targetTemperature 30","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"{\"targetTemperature\":30}","payloadType":"json","x":240,"y":780,"wires":[["567787a2.ac8028"]]},{"id":"8a22c961beebe02e","type":"inject","z":"b496e091.c65da8","name":"currentTemperature 32","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"currentTemperature\":32}","payloadType":"json","x":300,"y":320,"wires":[["567787a2.ac8028"]]},{"id":"a1c8123b7e45c128","type":"inject","z":"b496e091.c65da8","name":"Cal example Target Temp 28","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"summary\":\"Heizen\",\"topic\":\"Heizen\",\"id\":\"123456\",\"location\":\"28\",\"eventStart\":\"2021-10-13T22:00:00.000Z\",\"eventEnd\":\"2021-10-14T22:00:00.000Z\",\"description\":\"na\",\"on\":true,\"calendarName\":\"Personal\",\"countdown\":{\"days\":-1,\"hours\":-16,\"minutes\":-32,\"seconds\":-33}}","payloadType":"json","x":280,"y":420,"wires":[["b608e9f1.2a5e3"]]},{"id":"f8d13115799b8431","type":"inject","z":"b496e091.c65da8","name":"targetHeatingCoolingState 3 (Cal mode)","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"targetHeatingCoolingState\":3}","payloadType":"json","x":230,"y":620,"wires":[["567787a2.ac8028"]]},{"id":"6f636ed9c07602d8","type":"inject","z":"b496e091.c65da8","name":"targetTemperature 20","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"targetTemperature\":20}","payloadType":"json","x":240,"y":820,"wires":[["567787a2.ac8028"]]},{"id":"594aba4cc481076c","type":"comment","z":"b496e091.c65da8","name":"Temp Sensor","info":"should be replaced by a Temp Sensor","x":100,"y":300,"wires":[]},{"id":"194443b643947f47","type":"comment","z":"b496e091.c65da8","name":"2nd Window Sensor","info":"2nd Window Sensor is optional","x":110,"y":220,"wires":[]},{"id":"449b3f9da5ea34fa","type":"comment","z":"b496e091.c65da8","name":"CalDav","info":"You can use a CalDav Server (like google cal or nextcloud Calendar) to manage you heater by calender entries.\n\n\nThere is no check for the calender subject. \n(I'm using a seperate Calender)\nThe location field is used for the target temp.\n\n\n","x":70,"y":360,"wires":[]},{"id":"841452a44a0426f9","type":"comment","z":"b496e091.c65da8","name":"This flow is optimized for HomeKit Web Thermostat","info":"This flow is optimized for HomeKit Web Thermostat @ Homebridge.\n\nsee https://github.com/Tommrodrigues/homebridge-web-thermostat\n\nexample config (homebridge):\n\n \"accessory\": \"Thermostat\",\n \"name\": \"Thermostat livingroom\",\n \"apiroute\": \"http://localhost:1880/temp-dev\",\n \"pollInterval\": 30,\n \"validStates\": [\n 0,\n 1,\n 3\n ]\n }","x":790,"y":580,"wires":[]},{"id":"5b4fbbac920504c4","type":"comment","z":"b496e091.c65da8","name":"Click on the comments to get more details !!!","info":"","x":710,"y":100,"wires":[],"icon":"node-red/alert.svg"},{"id":"beac9c1a92a32786","type":"delay","z":"b496e091.c65da8","name":"","pauseType":"delay","timeout":"2","timeoutUnits":"seconds","rate":"1","nbRateUnits":"","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":940,"y":240,"wires":[["a13df985.2bb858"]]},{"id":"fc38620e010ae48a","type":"http in","z":"b496e091.c65da8","name":"Shelly window","url":"/window1","method":"get","upload":false,"swaggerDoc":"","x":210,"y":120,"wires":[["e8a8905cedb20956"]]},{"id":"1f969da815d3081b","type":"http response","z":"b496e091.c65da8","name":"","statusCode":"","headers":{},"x":370,"y":140,"wires":[]},{"id":"f63dd9e9a276bb83","type":"change","z":"b496e091.c65da8","name":"","rules":[{"t":"change","p":"payload","pt":"msg","from":"close","fromt":"str","to":"false","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"open","fromt":"str","to":"true","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":255,"y":160,"wires":[["1f969da815d3081b","e948e949.ab14b"]],"l":false},{"id":"e8a8905cedb20956","type":"change","z":"b496e091.c65da8","name":"","rules":[{"t":"move","p":"payload.state","pt":"msg","to":"payload","tot":"msg"},{"t":"set","p":"topic","pt":"msg","to":"setWindow1","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":215,"y":160,"wires":[["f63dd9e9a276bb83"]],"l":false},{"id":"b0e45c897bf9becc","type":"comment","z":"b496e091.c65da8","name":"delay only for testing","info":"","x":960,"y":200,"wires":[]},{"id":"38f92eb14295b858","type":"comment","z":"b496e091.c65da8","name":"here is a Shelly RGBW2 in white mode used","info":"","x":1230,"y":200,"wires":[]}]