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)
IMG-4190 Screenshot-2021-10-18-at-12-56-31

2) Configuration

  1. Add required nodes to Node-Red
Screenshot-2021-10-19-at-10-35-59
  1. Add the URLs off your Shelly RGBW2
Screenshot-2021-10-19-at-10-03-13
  1. set up a temp sensor (currently no example available)

  2. Configure Calendar (optional)

  • set-up a calDav Calendar to the ical node
Screenshot-2021-10-19-at-10-14-42

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.

  1. set up Window Sensor (optional, example: Shelly Door/Window)
  • insert reporting URL to the Node-Red Http End-Point
Screenshot-2021-10-19-at-10-27-23
  1. 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":[]}]

Flow Info

Created 3 years, 5 months ago
Rating: 3 2

Owner

Actions

Rate:

Node Types

Core
  • change (x5)
  • comment (x7)
  • debug (x1)
  • delay (x2)
  • function (x2)
  • http in (x4)
  • http request (x2)
  • http response (x3)
  • inject (x11)
  • rbe (x2)
  • template (x2)
Other

Tags

  • underfloor
  • heating
  • thermostat
  • temperature
  • temperatur
  • heizung
  • homebridge
  • homekit
  • web-based
  • heat
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option