NEWS
NodeRED ÖkoFEN Pelletronic Touch JSON
-
Hallo zusammen,
ich habe seit ein paar Wochen meinen Pelletkessel, aufgrund eines defektes der alten Steuerung, auf die neue Pelletronic Touch umbauen lassen und natürlich im Vorfeld recherchiert was meine Möglichkeiten der Anbindung an die Heimautomatisierung sind. Ich habe gelesen, dass es einerseits die Möglichkeit gibt das ganze über ModBUS TCP anzusprechen und in neueren Firmware-Versionen auch via JSON Interface. Laut Auskunft vom Techniker der den Umbau durchgeführt hat, führt die Verwendung von ModBUS jedoch zu einem schnelleren Verschleiß der Speichermodule des Bedienfeldes. Kann ich zwar nicht ganz nachvollziehen, jedoch kenne ich die genaue Programmierung der Firmware natürlich auch nicht.
Ich dachte daher, ich sehe mir das JSON Interface genauer an und die Möglichkeiten die ich damit habe. Ursprünglich wollte ich dann einen "echten" Adapter bauen, damit komm ich aber nicht zurecht - sind zuviele neue technologien (NodeJS, devcontainer, ...) auf einmal.
Stattdessen habe ich in NodeRED einen Flow zusammengebaut der im Großen und Ganzen das gleiche wie ein Adapter macht.
Wichtig ist, dass man dem NodeRED Adapter in ioBroker das Recht "Erstellung von Fremd-Objekten zulassen" erteilt. Ansonsten müsste man die Nodes entsprechend anpassen und pelletronic.0 durch einen anderen Objektpfad ersetzen.
Im Node "Set Pelletronic details" gibt man die IP, den Port und das sogenannte Passwort ein, das man direkt auf dem Touchdisplay des Kessels festlegen/ablesen kann wenn man JSON aktiviert. Ich habe bei mir übrigens die Option "Ein" gewählt (man kann zwischen Aus, Ein (kompatibel), Ein wählen). Dieser Node schreibt die Werte dann in den global Store.
Der Node "Start Interface" baut dann die URL aus den Variablen zusammen und startet die Abfrage an die Steuerung und wird alle 30 Sekunden ausgeführt. Da der Webserver leider kein Encoding im Header zurückgibt führt das zu Problemen mit den °-Sonderzeichen, daher habe ich die Antwort nicht direkt als JSON weiterverarbeiten können und muss den Umweg über Binär und als latin1 encoden gehen.
Der Flow ist so aufgebaut, dass die Objekte im ioBroker automatisch angelegt werden und auch die Werte für min/max/unit automatisch gesetzt werden. Zusätzlich speichert der Flow die JSON Antwort auch im Globalstore da manche Werte in der Pelletronic zusätzlich einen Umrechnungsfaktor benötigen (meistens 0.1 bei Temperaturwerten). Das wird in der JSON-Antwort aber mitgeliefert. Der Flow rechnet die Werte vom JSON entsprechend des Faktors um bevor er sie in ioBroker schreibt, bzw auch umgekehrt wenn die Werte in ioBroker geändert werden, bevor sie an die Pelletronic gesendet werden. Datenfelder welche ReadOnly sind, werden ebenfalls so in ioBroker angelegt (erkennbar am L_ Präfix). Der Flow berücksichtigt auch die min/max-Werte bevor sie an die Steuerung gesendet werden und bricht ab - die States in ioBroker bleiben dann auf unbestätigt. Wenn das Interface einen Fehler bei einer aktualisierung eines Wertes zurückgibt, bleibt der state ebenfalls unbestätigt.
Ich kann natürlich nur die Funktionen die mein Ofen hat testen, sprich ich habe keine Solaranlage, keinen Pufferspeicher und nur einen Heizkreis. Da das JSON jedoch einem Schema folgt sollte es mit jeder Variante funktionieren denke ich.
Vielleicht kann's jemand hier noch gebrauchen.
[ { "id": "227a035e.39564c", "type": "tab", "label": "OekoFEN JSON Interface", "disabled": false, "info": "" }, { "id": "1d31f46f.b15f5c", "type": "http request", "z": "227a035e.39564c", "name": "request pelletronic data", "method": "use", "ret": "bin", "paytoqs": "query", "url": "", "tls": "", "persist": false, "proxy": "", "authType": "", "x": 330, "y": 160, "wires": [ [ "4c31bdd2.c3edf4" ] ] }, { "id": "56f4d2d4.277fac", "type": "inject", "z": "227a035e.39564c", "name": "Start interface", "props": [ { "p": "url", "v": "\"http://\" & $globalContext(\"settings.interface.ip\") & \":\" & $globalContext(\"settings.interface.port\") & \"/\" & $globalContext(\"settings.interface.password\") & \"/all?\"", "vt": "jsonata" }, { "p": "method", "v": "get", "vt": "str" } ], "repeat": "30", "crontab": "", "once": true, "onceDelay": "10", "topic": "", "x": 120, "y": 160, "wires": [ [ "1d31f46f.b15f5c" ] ] }, { "id": "1f1faf59.a89af1", "type": "split", "z": "227a035e.39564c", "name": "Split answer into subsystems (hk1, ww1, pe1, ...)", "splt": "\\n", "spltType": "str", "arraySplt": 1, "arraySpltType": "len", "stream": false, "addname": "", "x": 220, "y": 240, "wires": [ [ "266726d5.1fcb0a" ] ] }, { "id": "748f86c5.818278", "type": "json", "z": "227a035e.39564c", "name": "", "property": "payload", "action": "obj", "pretty": false, "x": 390, "y": 200, "wires": [ [ "1f1faf59.a89af1" ] ] }, { "id": "e15a1071.b68dd", "type": "split", "z": "227a035e.39564c", "name": "split values of subsystems (pe1, hk1, ...) into multiple messages", "splt": "\\n", "spltType": "str", "arraySplt": 1, "arraySpltType": "len", "stream": false, "addname": "", "x": 270, "y": 320, "wires": [ [ "1bff57ba.6805b8" ] ] }, { "id": "266726d5.1fcb0a", "type": "function", "z": "227a035e.39564c", "name": "delete unnecessary parts and set first part of topic", "func": "msg.topic = \"pelletronic.0.\" + msg.parts.key;\ndelete msg.parts;\ndelete msg.filename;\ndelete msg.method;\ndelete msg.statusCode;\ndelete msg.responseUrl;\ndelete msg.url;\ndelete msg.redirectList;\ndelete msg.headers;\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 230, "y": 280, "wires": [ [ "e15a1071.b68dd" ] ] }, { "id": "1bff57ba.6805b8", "type": "function", "z": "227a035e.39564c", "name": "set correct topic for the value and prepare for ioBroker", "func": "msg.topic = msg.topic + \".\" + msg.parts.key;\nif (msg.parts.key.startsWith(\"L_\")) {\n \n msg.stateReadonly = true;\n \n} else {\n \n msg.stateReadonly = false;\n}\n\nmsg.stateName = msg.parts.key;\nvar originalPayload = RED.util.cloneMessage(msg.payload);\n\nif (typeof(msg.payload) === \"string\") {\n msg.stateType = \"string\";\n msg.payload = originalPayload;\n} else if (typeof(msg.payload.val) === \"string\") {\n msg.stateType = \"string\";\n msg.payload = originalPayload.val;\n} else {\n if (originalPayload.unit !== undefined) {msg.stateUnit = originalPayload.unit;}\n \n if (originalPayload.min !== undefined) {\n if (originalPayload.factor === undefined) {\n msg.stateMin = originalPayload.min;\n } else {\n msg.stateMin = originalPayload.min * originalPayload.factor;\n }\n }\n \n if (originalPayload.max !== undefined) {\n if (originalPayload.factor === undefined) {\n msg.stateMax = originalPayload.max;\n } else {\n msg.stateMax = originalPayload.max * originalPayload.factor;\n }\n }\n if (originalPayload.max === undefined && originalPayload.min === undefined && originalPayload.format !== undefined) {\n let tempFormat = originalPayload.format.split(\"|\");\n msg.stateMin = tempFormat[0].split(\":\")[0];\n msg.stateMax = tempFormat[tempFormat.length -1].split(\":\")[0];\n msg.stateName = msg.stateName + \"(\" + originalPayload.format + \")\";\n }\n \n\n \n if (originalPayload.factor === undefined) {\n msg.payload = originalPayload.val;\n } else {\n msg.payload = originalPayload.val * originalPayload.factor;\n }\n msg.stateType = \"number\";\n \n}\nlet globalPath = msg.topic.replace(\"pelletronic.0.\", \"\");\nglobal.set(globalPath, originalPayload);\nmsg.ack = true;\ndelete msg.parts;\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 240, "y": 360, "wires": [ [ "bec6b706.feea88" ] ] }, { "id": "bec6b706.feea88", "type": "ioBroker out", "z": "227a035e.39564c", "name": "", "topic": "", "ack": "true", "autoCreate": "true", "stateName": "", "role": "", "payloadType": "", "readonly": "", "stateUnit": "", "stateMin": "", "stateMax": "", "x": 620, "y": 360, "wires": [] }, { "id": "4c31bdd2.c3edf4", "type": "function", "z": "227a035e.39564c", "name": "encode answer with latin1", "func": "msg.payload = msg.payload.toString('latin1');\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 150, "y": 200, "wires": [ [ "748f86c5.818278" ] ] }, { "id": "cd22fc7f.ddfc8", "type": "ioBroker in", "z": "227a035e.39564c", "name": "", "topic": "pelletronic.0.*", "payloadType": "value", "onlyack": "", "func": "all", "gap": "", "fireOnStart": "false", "x": 110, "y": 520, "wires": [ [ "43732747.f8ed08" ] ] }, { "id": "43732747.f8ed08", "type": "function", "z": "227a035e.39564c", "name": "process the change from ioBroker", "func": "function replaceAll(str, find, replace) {\n return str.replace(new RegExp(find, 'g'), replace);\n}\n\nfunction getFactor(strTopic) {\n let gObject = global.get(strTopic.replace(\"pelletronic.0.\", \"\"));\n if (gObject.factor !== undefined) {\n return gObject.factor;\n } else {\n return 1;\n }\n}\n\nfunction isValueValid(strTopic, newValue) {\n let gObject = global.get(strTopic.replace(\"pelletronic.0.\", \"\"));\n if (gObject.min !== undefined && gObject.max !== undefined) {\n if (gObject.factor !== undefined) {\n if (((newValue / gObject.factor) >= gObject.min) && ((newValue / gObject.factor) <= gObject.max)) {\n return true;\n } \n }\n \n if (newValue >= gObject.min && newValue <= gObject.max) {\n return true;\n }\n }\n \n if (gObject.format !== undefined) {\n let helper = gObject.format.split(\"|\");\n for (var element of helper) {\n if (element.startsWith(newValue.toString())) {\n return true;\n }\n }\n }\n return false;\n}\n\nif (msg.acknowledged || msg.payload == \"\") {\n return null;\n} else {\n if (isValueValid(replaceAll(msg.topic, \"/\", \".\"), msg.payload)) {\n var tempUrl = msg.topic.replace(\"pelletronic/0/\", \"\");\n msg.topic = replaceAll(msg.topic, \"/\", \".\");\n tempUrl = replaceAll(tempUrl, \"/\", \".\");\n let oGlobSet = global.get(\"settings\");\n tempUrl = \"http://\" + oGlobSet.interface.ip + \":\" + oGlobSet.interface.port + \"/\" + oGlobSet.interface.password + \"/\" + tempUrl + \"=\" + (msg.payload / getFactor(replaceAll(msg.topic, \"/\", \".\")));\n msg.url = tempUrl;\n msg.originalPayload = msg.payload;\n delete msg.acknowledged;\n return msg;\n } else {\n return null;\n }\n}", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 340, "y": 520, "wires": [ [ "2c95cff0.32142" ] ] }, { "id": "9849d1ee.526ba", "type": "ioBroker out", "z": "227a035e.39564c", "name": "", "topic": "", "ack": "true", "autoCreate": "false", "stateName": "", "role": "", "payloadType": "", "readonly": "", "stateUnit": "", "stateMin": "", "stateMax": "", "x": 1140, "y": 540, "wires": [] }, { "id": "2c95cff0.32142", "type": "http request", "z": "227a035e.39564c", "name": "update pelletronic control", "method": "GET", "ret": "txt", "paytoqs": "ignore", "url": "", "tls": "", "persist": false, "proxy": "", "authType": "", "x": 610, "y": 520, "wires": [ [ "b8f1e197.87104" ] ] }, { "id": "f2851be8.7718e8", "type": "inject", "z": "227a035e.39564c", "name": "Set Pelletronic details", "props": [ { "p": "interface.ip", "v": "1.2.3.4", "vt": "str" }, { "p": "interface.port", "v": "8082", "vt": "str" }, { "p": "interface.password", "v": "abcDEFxyZ", "vt": "str" } ], "repeat": "", "crontab": "", "once": true, "onceDelay": "1", "topic": "", "payloadType": "str", "x": 140, "y": 40, "wires": [ [ "3931637.11f2d9c" ] ] }, { "id": "3931637.11f2d9c", "type": "function", "z": "227a035e.39564c", "name": "write settings to global store", "func": "global.set(\"settings\", msg);\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 380, "y": 40, "wires": [ [] ] }, { "id": "70c2cf04.38b1d", "type": "function", "z": "227a035e.39564c", "name": "write log that pelletronic set failed", "func": "node.warn(\"set of datapoint \" + msg.topic + \"failed\");\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1020, "y": 500, "wires": [ [] ] }, { "id": "b8f1e197.87104", "type": "switch", "z": "227a035e.39564c", "name": "", "property": "payload", "propertyType": "msg", "rules": [ { "t": "cont", "v": "Failed", "vt": "str" }, { "t": "else" } ], "checkall": "true", "repair": false, "outputs": 2, "x": 790, "y": 520, "wires": [ [ "70c2cf04.38b1d" ], [ "4eef250e.69f02c" ] ] }, { "id": "4eef250e.69f02c", "type": "change", "z": "227a035e.39564c", "name": "", "rules": [ { "t": "set", "p": "payload", "pt": "msg", "to": "originalPayload", "tot": "msg" }, { "t": "set", "p": "ack", "pt": "msg", "to": "true", "tot": "bool" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 960, "y": 540, "wires": [ [ "9849d1ee.526ba" ] ] } ]