Skip to content

Instantly share code, notes, and snippets.

@G0sch

G0sch/README.md Secret

Last active October 20, 2021 07:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save G0sch/cc89862bac98a0770c812f40e6d05b3d to your computer and use it in GitHub Desktop.
Save G0sch/cc89862bac98a0770c812f40e6d05b3d to your computer and use it in GitHub Desktop.
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": []
}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment