Websockets Webinterface (ab FW 3.91)

Übersicht

Die Seite https://loxwiki.atlassian.net/l/cp/xpofdD5M zeigt Wege und Code um die mittlerweile älteren Luxtroniksteuerungen per NodeRed auszulesen und zu steuern. Die zur Verfügung gestellten Pakete haben leider für die FW 3.91.1 zusammen mit NodeRed 3.1 nicht funktioniert. Daher wurde das von@Bouni (Vielen Dank) erstellte Paket modifiziert und direkt als JavaScriptCode in eine Function Node implementiert.

NodeRed

Die folgenden Infos wurde mit NodeRed 3.1 getestet.

Schnittstelle (JavaScript in NodeRed)

image-20231224-104504.png

Der in eine FunctionNode einzufügende Code ist folgender:

//const WebSocket = require('ws'); const uri = 'ws://192.168.178.40:8214'; const login = "LOGIN;999999"; let parseString = XML2JS.parseString ws = new WebSocket(uri, "Lux_WS"); ws.onerror = ( event ) => { node.status({fill:"red",shape:"dot",text:"error"}); } ws.onopen = ( event ) => { node.status({fill:"green",shape:"dot",text:"connected"}); try { ws.send(login); ws.send("REFRESH"); } catch (e) { node.error("Error while connecting to Luxtronic heat pump controler!"); ws.terminate(); // make su } }; node.topic_parent = ''; node.topic_child = ''; node.topic_value = ''; node.topic = {}; if('topic' in msg){ if(msg.topic != '' && msg.topic.toString().includes('/')){ node.topic = msg.topic.toString().split('/'); if(node.topic.length == 2){ // e.g. Betriebsart/Heizkreis node.topic_parent = node.topic[0]; node.topic_child = node.topic[1]; if('payload' in msg){ node.topic_value = msg.payload.toString(); } }else{ node.topic_value = ''; node.topic_parent = ''; node.topic_child = ''; msg.topic = 'Invalid Input message, use: topic: Betriebsart/Heizkreis payload:TARGETVALUE'; } } } msg.payload = {}; node.count = 0; // main logic is here: ws.onmessage = ( event ) => { //ws.send(get_table) parseString(event.data, function (err, json) { // case right after login, we need to navigate to the "Information" tab on index 0 if("Navigation" in json) { for(var i in json.Navigation.item) { var name = json.Navigation.item[i].name[0]; if (name == 'Zugang: Benutzer' || name == 'Access: User' || name == 'Zeitschaltprogramm' || name == 'Fernsteuerung') { node.status({fill:"green",shape:"dot",text:"Skipping: "+name}); } else{ var id = json.Navigation.item[i].$.id; msg.payload[name] = {}; ws.send('GET;'+id); node.count++; } } } else { // Replys to the GET;<id> commands that get us the actual data let rootname = null if (!(json.Content.name)) { // fallback if rootname is not received data rootname = "Einstellungen" // TODO: Change to "Settings" }else{ rootname = json.Content.name[0]; } // Output XML structure with RAW values if(node.topic_parent.includes('INFO') || node.topic_child.includes('INFO')){ msg.payload['INFO - '+rootname] = json.Content; } for(var j in json.Content.item) { var group = json.Content.item[j].name[0]; node.status({fill:"green",shape:"dot",text:"process "+group}); msg.payload[rootname][group] = {}; if ('raw' in json.Content.item[j]) { var name = json.Content.item[j].name; var value = json.Content.item[j].value; try { msg.payload[rootname][group][name] = value[0]; } catch(err){ msg.payload[rootname][group][name] = value; } } else if('item' in json.Content.item[j]) { for(var k in json.Content.item[j].item) { var name = json.Content.item[j].item[k].name; var value = json.Content.item[j].item[k].value; if(typeof value === 'object') { msg.payload[rootname][group][name] = value[0]; } if('item' in json.Content.item[j].item[k]) { var group2 = json.Content.item[j].item[k].name[0]; node.status({fill:"green",shape:"dot",text:"process "+group2}); msg.payload[rootname][group2] = {}; delete msg.payload[rootname][group]; for(var l in json.Content.item[j].item[k].item) { var name = json.Content.item[j].item[k].item[l].name; var value = json.Content.item[j].item[k].item[l].value; if(typeof value === 'object') { msg.payload[rootname][group2][name] = value[0]; } } } // Send SET values SET;set_targetid;value if( json.Content.item[j].name.toString().includes(node.topic_parent) && json.Content.item[j].item[k].name.toString().includes(node.topic_child) && (node.topic_value != '')){ msg.topic = msg.topic + ' -> ' + node.topic_value; ws.send('SET;set_' + json.Content.item[j].item[k].$.id + ';' + node.topic_value); node.topic_parent = ''; node.topic_child = ''; node.topic = ''; node.topic_value = ''; ws.send('SAVE;1'); } } } } // Replys to the GET;<id> commands that get us the actual data // receive temperatures node.count--; if(node.count == 0) { ws.terminate(); node.send(msg); } } }); }; ws.onclose = function (evt) { node.status({fill:"grey",shape:"dot",text:"disconnected"}); ws = null; }

Flow - WP Ein / Ausschalten

Der oben gezeigt Wärmepumpencontroler wird im meinem Beispiel mit folgendem Konzept betrieben:

  • Alle Heizkreise werden über Shelly Relays der Pro Serie gesteuert.

  • Die Schaltzustände der 15 Relais werden bei Änderungen immer in eine globale NodeRed Variable geschrieben.

  • Alle 30 Sekunden wird die globale Variable abgefragt und bei Veränderung wird die WP entsprechend geschaltet, also “Auto” oder “Aus”. Somit laufen die Umwälzpumpen nur wenn sie auch wirklich gebraucht werden. Die von der WP zur Verfügung gestellte Option “Pumpenoptimierung” hat Nachteile, die dadurch umgangen werden.

image-20240114-213704.png
[ { "id": "84d592873fbd2c7d", "type": "tab", "label": "Luxtronic", "disabled": false, "info": "", "env": [] }, { "id": "f3563295513c4fe4", "type": "group", "z": "84d592873fbd2c7d", "style": { "stroke": "#999999", "stroke-opacity": "1", "fill": "none", "fill-opacity": "1", "label": true, "label-position": "nw", "color": "#a4a4a4" }, "nodes": [ "4a7b9c9a95ccaa97", "427bfbfdd7923ab3", "0942709237a85f5e", "1da965ecadf5d7eb", "700489fa59ae9f94", "252f7fa733f45ac4", "c24ab45386e95498", "5e5227df4d1d9acf", "37fa68568c0ac23d", "056237b712d80276", "5021f4c978e21fb0", "5f091ebdcdfe4787", "0322f38b560c66b8", "9b753bc6dcb14372", "20f1a60e189ad6d4", "23415a2ef19deba9", "14162379bb780ba3", "aec0dda2eda4ed81", "d9cbee2894e8da73", "f5fc6768c8226358", "b1981a45bf8d411a", "3f843a3706137855", "556f7168c5d67777", "c09b694b2b299bec", "bfdf0c08512641ab", "c6af3cdc2259be3c" ], "x": 134, "y": 319, "w": 1292, "h": 482 }, { "id": "23415a2ef19deba9", "type": "junction", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "x": 1120, "y": 700, "wires": [ [ "14162379bb780ba3", "427bfbfdd7923ab3" ] ] }, { "id": "4a7b9c9a95ccaa97", "type": "inject", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "trigger (5min loop)", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "300", "crontab": "", "once": true, "onceDelay": "", "topic": "", "payload": "true", "payloadType": "bool", "x": 270, "y": 540, "wires": [ [ "f5fc6768c8226358" ] ] }, { "id": "427bfbfdd7923ab3", "type": "function", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "Luxtronic Heatpump controler", "func": "//const WebSocket = require('ws');\nconst uri = 'ws://192.168.172.23:8214';\nconst login = \"LOGIN;999999\";\n\nlet parseString = XML2JS.parseString\n\nws = new WebSocket(uri, \"Lux_WS\");\n\nws.onerror = ( event ) => {\n node.status({fill:\"red\",shape:\"dot\",text:\"error\"});\n}\n\nws.onopen = ( event ) => {\n node.status({fill:\"green\",shape:\"dot\",text:\"connected\"});\n try {\n ws.send(login);\n ws.send(\"REFRESH\");\n } catch (e) {\n node.error(\"Error while connecting to Luxtronic heat pump controler!\");\n ws.terminate(); // make su\n }\n};\n\nnode.topic_parent = '';\n node.topic_child = '';\n node.topic_value = '';\n node.topic = {};\n\n if('topic' in msg){\n if(msg.topic != '' && msg.topic.toString().includes('/')){\n node.topic = msg.topic.toString().split('/');\n if(node.topic.length == 2){ // e.g. Betriebsart/Heizkreis\n node.topic_parent = node.topic[0];\n node.topic_child = node.topic[1];\n if('payload' in msg){\n node.topic_value = msg.payload.toString();\n }\n }else{\n node.topic_value = '';\n node.topic_parent = '';\n node.topic_child = '';\n msg.topic = 'Invalid Input message, use: topic: Betriebsart/Heizkreis payload:TARGETVALUE';\n }\n }\n }\n \n msg.payload = {};\n node.count = 0;\n\n// main logic is here:\nws.onmessage = ( event ) => {\n //ws.send(get_table)\n parseString(event.data, function (err, json) {\n // case right after login, we need to navigate to the \"Information\" tab on index 0\n if(\"Navigation\" in json) {\n for(var i in json.Navigation.item) {\n var name = json.Navigation.item[i].name[0];\n if (name == 'Zugang: Benutzer' || \n name == 'Access: User' || \n name == 'Zeitschaltprogramm' || \n name == 'Fernsteuerung') {\n node.status({fill:\"green\",shape:\"dot\",text:\"Skipping: \"+name});\n } else{\n var id = json.Navigation.item[i].$.id;\n msg.payload[name] = {};\n ws.send('GET;'+id);\n node.count++;\n }\n }\n\n } else {\n // Replys to the GET;<id> commands that get us the actual data\n let rootname = null\n \n if (!(json.Content.name)) {\n // fallback if rootname is not received data\n rootname = \"Einstellungen\" // TODO: Change to \"Settings\"\n }else{\n rootname = json.Content.name[0];\n }\n\n // Output XML structure with RAW values\n if(node.topic_parent.includes('INFO') || node.topic_child.includes('INFO')){\n msg.payload['INFO - '+rootname] = json.Content;\n }\n\n for(var j in json.Content.item) {\n var group = json.Content.item[j].name[0];\n node.status({fill:\"green\",shape:\"dot\",text:\"process \"+group});\n msg.payload[rootname][group] = {};\n\n if ('raw' in json.Content.item[j]) {\n var name = json.Content.item[j].name;\n var value = json.Content.item[j].value;\n try {\n msg.payload[rootname][group][name] = value[0];\n } catch(err){\n msg.payload[rootname][group][name] = value;\n }\n } else if('item' in json.Content.item[j]) {\n for(var k in json.Content.item[j].item) {\n var name = json.Content.item[j].item[k].name;\n var value = json.Content.item[j].item[k].value;\n if(typeof value === 'object') {\n msg.payload[rootname][group][name] = value[0];\n }\n if('item' in json.Content.item[j].item[k]) {\n var group2 = json.Content.item[j].item[k].name[0];\n node.status({fill:\"green\",shape:\"dot\",text:\"process \"+group2});\n msg.payload[rootname][group2] = {};\n delete msg.payload[rootname][group];\n for(var l in json.Content.item[j].item[k].item) {\n var name = json.Content.item[j].item[k].item[l].name;\n var value = json.Content.item[j].item[k].item[l].value;\n if(typeof value === 'object') {\n msg.payload[rootname][group2][name] = value[0];\n }\n }\n }\n\n // Send SET values SET;set_targetid;value\n if( json.Content.item[j].name.toString().includes(node.topic_parent)\n && json.Content.item[j].item[k].name.toString().includes(node.topic_child)\n && (node.topic_value != '')){\n msg.topic = msg.topic + ' -> ' + node.topic_value;\n ws.send('SET;set_' + json.Content.item[j].item[k].$.id + ';' + node.topic_value);\n node.topic_parent = '';\n node.topic_child = '';\n node.topic = '';\n node.topic_value = '';\n ws.send('SAVE;1');\n }\n }\n }\n }\n // Replys to the GET;<id> commands that get us the actual data\n // receive temperatures \n node.count--;\n \n if(node.count == 0) {\n ws.terminate();\n node.send(msg);\n }\n\n }\n \n }); \n};\n\nws.onclose = function (evt) {\n node.status({fill:\"grey\",shape:\"dot\",text:\"disconnected\"});\n ws = null;\n}\n\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [ { "var": "WebSocket", "module": "ws" }, { "var": "XML2JS", "module": "xml2js" } ], "x": 730, "y": 580, "wires": [ [ "1da965ecadf5d7eb", "252f7fa733f45ac4", "bfdf0c08512641ab" ] ] }, { "id": "0942709237a85f5e", "type": "debug", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "debug 36", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 1260, "y": 540, "wires": [] }, { "id": "1da965ecadf5d7eb", "type": "function", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "data to db objject", "func": "var payload = msg.payload\n\n// parser for all values\nlet parse_hpvalues = (s) => {\n if (s == \"Ein\") {\n return Boolean(true);\n }\n\n if (s == \"Aus\") {\n return Boolean(false);\n }\n if (s == \"--- l/h\") {\n return 0;\n }\n return parseFloat(s);\n}\n\nmsg = {\"payload\":{}};\n\n// define which fields we want to extract\nlet fields2store = {\n \"Temperaturen\" : [\n \"Vorlauf\", \"Rücklauf\", \"Rückl.-Soll\", \"Rückl.-Extern\", \"Außentemperatur\", \"Mitteltemperatur\", \"Warmwasser-Ist\", \n \"Mischkreis1-Vorlauf\", \"Mischkreis1 VL-Soll\", \n \"Mischkreis2-Vorlauf\", \"Mischkreis2 VL-Soll\", \"TFL1\", \"TFL2\"],\n \"Eingänge\": [\"Durchfluss\", \"STB E-Stab\"],\n \"Ausgänge\": [\"BUP\", \"FUP 1\", \"HUP\", \"Mischer 1 Auf\", \"Mischer 1 Zu\", \"Ventil.-BOSUP\", \"Verdichter 1\", \"ZIP\", \"ZUP\", \"FUP 2\", \"Freq. aktuell\", \"EEV Heizen\", \"ZWE 1\", \"ZWE 3\"],\n \"Betriebsstunden\": [\"Betriebstunden Heiz.\", \"Betriebstunden WW\", \"Impulse Verdichter 1\" ],\n \"Anlagenstatus\": [\"Heizleistung Ist\", \"Abtaubedarf\"],\n \"Wärmemenge\": [\"Heizung\", \"Warmwasser\"],\n \"Eingesetzte Energie\": [\"Heizung\", \"Warmwasser\"] \n};\n\nlet data = {};\n\n// Extract\nfor(const key_group of Object.keys(fields2store)) {\n //data[key_group] = {};\n for (const k of fields2store[key_group]) {\n data[key_group + \"-\" + k] = parse_hpvalues(payload.Informationen[key_group][k])\n }\n}\n\nreturn {\"payload\": [\n {\"measurement\": \"heatpump\",\n \"fields\": data,\n \"tags\": {\"Betriebszustand\": payload[\"Informationen\"][\"Anlagenstatus\"][\"Betriebszustand\"]}\n }]\n}", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1050, "y": 580, "wires": [ [ "0942709237a85f5e", "700489fa59ae9f94" ] ] }, { "id": "700489fa59ae9f94", "type": "influxdb batch", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "influxdb": "ea9e7a3bc9d4fb9b", "precision": "", "retentionPolicy": "", "name": "heatpump", "database": "mydb", "precisionV18FluxV20": "ms", "retentionPolicyV18Flux": "", "org": "organisation", "bucket": "bucket", "x": 1260, "y": 580, "wires": [] }, { "id": "252f7fa733f45ac4", "type": "debug", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "debug 37", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 1020, "y": 620, "wires": [] }, { "id": "c24ab45386e95498", "type": "function", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "Betriebsart/Warmwasser: ", "func": "\n// Warmwasser\nmsg = {\n // \"payload\": 0, // 0: Automatik\n // \"payload\": 1, // 1: Zusätzlicher \n // \"payload\": 2, // Party\n // \"payload\": 3, // Ferien\n //\"payload\": 4, // Aus\n \"payload\": msg.payload, \n \"topic\": \"Betriebsart/Warmwasser\"\n} \n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 710, "y": 400, "wires": [ [ "427bfbfdd7923ab3" ] ] }, { "id": "5e5227df4d1d9acf", "type": "inject", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "WW: Auto (0)", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "0", "payloadType": "str", "x": 370, "y": 360, "wires": [ [ "c24ab45386e95498" ] ] }, { "id": "37fa68568c0ac23d", "type": "inject", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "WW: Party (2)", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "2", "payloadType": "str", "x": 370, "y": 400, "wires": [ [ "c24ab45386e95498" ] ] }, { "id": "056237b712d80276", "type": "inject", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "WW: Aus (4)", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "4", "payloadType": "str", "x": 370, "y": 440, "wires": [ [ "c24ab45386e95498" ] ] }, { "id": "ca5db5f87a5755a0", "type": "comment", "z": "84d592873fbd2c7d", "name": "2Do Wasser zum günstigtem Zeitpunkt nachts", "info": "", "x": 470, "y": 280, "wires": [] }, { "id": "5021f4c978e21fb0", "type": "function", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "Update HP Heating state", "func": "let heating_pump_state_required = msg.payload\n// \"payload\": 0, // 0: Automatik\n// \"payload\": 1, // 1: Zusätzlicher\n// \"payload\": 2, // Party\n// \"payload\": 3, // Ferien\n//\"payload\": 4, // Aus\n\nlet msg_hk = {\"topic\": \"Betriebsart/Heizkreis\"}\nlet msg_mk2 = { \"topic\": \"Betriebsart/Mischkreis 2\" }\n\nif (heating_pump_state_required==true) {\n msg_hk[\"payload\"] = 0 // Automatik\n msg_mk2[\"payload\"] = 0 // Automatik\n\n } else {\n msg_hk[\"payload\"] = 4 //Aus\n msg_mk2[\"payload\"] = 4 // Aus\n}\n\nreturn [msg_hk, msg_mk2];", "outputs": 2, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 770, "y": 700, "wires": [ [ "23415a2ef19deba9" ], [ "5f091ebdcdfe4787" ] ] }, { "id": "5f091ebdcdfe4787", "type": "delay", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "", "pauseType": "delay", "timeout": "5", "timeoutUnits": "seconds", "rate": "1", "nbRateUnits": "1", "rateUnits": "second", "randomFirst": "1", "randomLast": "5", "randomUnits": "seconds", "drop": false, "allowrate": false, "outputs": 1, "x": 1000, "y": 740, "wires": [ [ "23415a2ef19deba9" ] ] }, { "id": "0322f38b560c66b8", "type": "rbe", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "only when changed", "func": "rbe", "gap": "", "start": "", "inout": "out", "septopics": false, "property": "payload", "topi": "topic", "x": 530, "y": 700, "wires": [ [ "5021f4c978e21fb0" ] ] }, { "id": "9b753bc6dcb14372", "type": "function", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "get heat state from global", "func": "return { \"payload\": global.get('heating_pump_state_required')};", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 290, "y": 700, "wires": [ [ "0322f38b560c66b8" ] ] }, { "id": "20f1a60e189ad6d4", "type": "inject", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "Loop 30 s", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "30", "crontab": "", "once": true, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 250, "y": 640, "wires": [ [ "9b753bc6dcb14372" ] ] }, { "id": "14162379bb780ba3", "type": "debug", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "debug 41", "active": false, "tosidebar": true, "console": true, "tostatus": true, "complete": "payload", "targetType": "msg", "statusVal": "payload", "statusType": "auto", "x": 1310, "y": 700, "wires": [] }, { "id": "aec0dda2eda4ed81", "type": "comment", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "2Do Logging nur wenn WW oder H an ist, sonst nur alle halbe Stunde", "info": "", "x": 410, "y": 760, "wires": [] }, { "id": "d9cbee2894e8da73", "type": "inject", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "trigger (30min loop)", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "1800", "crontab": "", "once": true, "onceDelay": "", "topic": "", "payload": "false", "payloadType": "bool", "x": 280, "y": 580, "wires": [ [ "3f843a3706137855" ] ] }, { "id": "f5fc6768c8226358", "type": "switch", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "", "property": "payload", "propertyType": "msg", "rules": [ { "t": "eq", "v": "heating_pump_state_required", "vt": "global" } ], "checkall": "true", "repair": false, "outputs": 1, "x": 450, "y": 540, "wires": [ [ "427bfbfdd7923ab3" ] ] }, { "id": "b1981a45bf8d411a", "type": "comment", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "Logging: 5min if hp is on, otherwise every 30 min", "info": "", "x": 360, "y": 500, "wires": [] }, { "id": "3f843a3706137855", "type": "switch", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "", "property": "payload", "propertyType": "msg", "rules": [ { "t": "eq", "v": "heating_pump_state_required", "vt": "global" } ], "checkall": "true", "repair": false, "outputs": 1, "x": 450, "y": 580, "wires": [ [ "427bfbfdd7923ab3" ] ] }, { "id": "556f7168c5d67777", "type": "inject", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 560, "y": 460, "wires": [ [ "427bfbfdd7923ab3" ] ] }, { "id": "c09b694b2b299bec", "type": "inject", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "Reupdate", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "true", "payloadType": "bool", "x": 740, "y": 760, "wires": [ [ "5021f4c978e21fb0" ] ] }, { "id": "bfdf0c08512641ab", "type": "function", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "time since running 2 global", "func": "let verdichter = msg.payload[\"Informationen\"]['Ausgänge'][\"Verdichter 1\"]\nlet heatpump_start_time = global.get('heatpump_start_time') // if not started, then undefined\n\nif (verdichter == \"Ein\" && heatpump_start_time == undefined) {\n global.set('heatpump_start_time', Date.now())\n} else if (verdichter == \"Aus\" && (heatpump_start_time)) {\n // set to undefined which means \"off\"\n global.set('heatpump_start_time', undefined)\n //set operation time to zero\n global.set('heatpump_current_operating_time_minutes', 0)\n\n} else if (verdichter == \"Ein\" && (heatpump_start_time)) {\n // update time info\n // @ts-ignore\n let delta_time_minutes = (Date.now() - heatpump_start_time) / 1000 / 60\n global.set('heatpump_current_operating_time_minutes', delta_time_minutes)\n}\n\nreturn msg\n\n// return {\n// \"payload\": {\n// \"delta_time\": global.get('heatpump_current_operating_time_minutes'), \"start_time\": global.get('heatpump_start_time'), \"instance\": (heatpump_start_time instanceof Date)\n// } };", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1040, "y": 480, "wires": [ [ "c6af3cdc2259be3c" ] ] }, { "id": "c6af3cdc2259be3c", "type": "debug", "z": "84d592873fbd2c7d", "g": "f3563295513c4fe4", "name": "debug 42", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 1260, "y": 480, "wires": [] }, { "id": "ea9e7a3bc9d4fb9b", "type": "influxdb", "hostname": "192.168.178.54", "port": "8086", "protocol": "http", "database": "mydb", "name": "192.168.172.40", "usetls": false, "tls": "", "influxdbVersion": "1.8-flux", "url": "http://192.168.172.40:8086", "rejectUnauthorized": false } ]

Flow für Heizkreise

Die Heizkreisventile sind so verkabelt, dass die normalen Uralt-Bi-Metall-Thermostate funktionieren, aber auch NodeRed die Heizkreise über die Shellies schalten kann. Die Heizkreise werden in der aktuellen Vorstufe noch geschaltet über folgende NodeRed Logiken:

  • Logik für PV Überschuß → wenn länger als 5 Minuten über 800W (Wert für Winter) anliegen, dann wir die WP für mindestens eine Stunde betrieben, andernfalls wieder ausgeschaltet

  • Logik für günstigen Strom mit dynamischem Strompreis von tibber.com. Wenn der Strom günstiger als die Grenze ist, dann wird geheizt.

  • Manuelles Schalten der Relais per App

Sobald mehr Zeit ist, sollen die Algorithmen besser werden, bzw. von einer API übernommen werden. Der Flow ohne die oben genannten Algorithmen sieht so aus:

Bauteilaktivierung / Beton-Wärme-Speicher

Wie im Flow oben zu sehen ist kommt im Beispiel eine Decken und Bodenplattenheizung zum Wärmespeichern zum Einsatz, welche es ermöglicht mit günstigem dynamischen Strompreisen oder PV Überschuß viel Wärme ins Haus zu bringen und anschließend mehrere Tage nicht Heizen zu müssen. Mehr Infos dazu hier: https://youtu.be/CUq6-UIAEdM?si=spFmEu3PpAI8KkNL .