[ { "id": "0e938b2c7d519455", "type": "tab", "label": "Flow 1", "disabled": false, "info": "", "env": [ { "name": "in_port", "value": "20000", "type": "str" }, { "name": "out_port", "value": "20000", "type": "str" } ] }, { "id": "b9502e7d5adbd1fc", "type": "subflow", "name": "Pump Command", "info": "", "category": "", "in": [ { "x": 140, "y": 100, "wires": [ { "id": "26952e484ee2bdd1" } ] } ], "out": [ { "x": 380, "y": 100, "wires": [ { "id": "26952e484ee2bdd1", "port": 0 } ] } ], "env": [ { "name": "TARGET_NODE", "type": "num", "value": "" }, { "name": "PUMP_ID", "type": "num", "value": "" } ], "meta": {}, "color": "#DDAA99" }, { "id": "1a6e91b7110fc591", "type": "subflow", "name": "Druck", "info": "", "category": "", "in": [ { "x": 60, "y": 40, "wires": [ { "id": "6e8a2a39a56ae270" } ] } ], "out": [ { "x": 340, "y": 40, "wires": [ { "id": "6e8a2a39a56ae270", "port": 0 } ] } ], "env": [], "meta": {}, "color": "#DDAA99" }, { "id": "d9f42b9039b25aff", "type": "server", "name": "Home Assistant", "version": 6, "addon": false, "rejectUnauthorizedCerts": false, "ha_boolean": [ "y", "yes", "true", "on", "home", "open" ], "connectionDelay": true, "cacheJson": true, "heartbeat": true, "heartbeatInterval": 30, "areaSelector": "friendlyName", "deviceSelector": "friendlyName", "entitySelector": "friendlyName", "statusSeparator": ": ", "statusYear": "hidden", "statusMonth": "short", "statusDay": "numeric", "statusHourCycle": "default", "statusTimeFormat": "h:m", "enableGlobalContextStore": false }, { "id": "0a17864de5505a20", "type": "ha-device-config", "name": "Regenwasser", "hwVersion": "", "manufacturer": "Node-RED", "model": "", "swVersion": "" }, { "id": "79cd4bcaeefb790a", "type": "ha-entity-config", "server": "d9f42b9039b25aff", "deviceConfig": "0a17864de5505a20", "name": "Systemdruck", "version": 6, "entityType": "sensor", "haConfig": [ { "property": "name", "value": "Systemdruck" }, { "property": "icon", "value": "" }, { "property": "entity_picture", "value": "" }, { "property": "entity_category", "value": "" }, { "property": "device_class", "value": "pressure" }, { "property": "unit_of_measurement", "value": "Pa" }, { "property": "state_class", "value": "" } ], "resend": false, "debugEnabled": false }, { "id": "11087cbf4f9379d1", "type": "ha-entity-config", "server": "d9f42b9039b25aff", "deviceConfig": "0a17864de5505a20", "name": "Wasserstand", "version": 6, "entityType": "sensor", "haConfig": [ { "property": "name", "value": "Wasserstand" }, { "property": "icon", "value": "" }, { "property": "entity_picture", "value": "" }, { "property": "entity_category", "value": "" }, { "property": "device_class", "value": "pressure" }, { "property": "unit_of_measurement", "value": "Pa" }, { "property": "state_class", "value": "" } ], "resend": false, "debugEnabled": false }, { "id": "9cba2b96af7846f4", "type": "ha-entity-config", "server": "d9f42b9039b25aff", "deviceConfig": "0a17864de5505a20", "name": "Pumpe", "version": 6, "entityType": "binary_sensor", "haConfig": [ { "property": "name", "value": "Pumpe" }, { "property": "icon", "value": "" }, { "property": "entity_picture", "value": "" }, { "property": "entity_category", "value": "" }, { "property": "device_class", "value": "" } ], "resend": false, "debugEnabled": false }, { "id": "92579cd9fdb0eda1", "type": "ha-entity-config", "server": "d9f42b9039b25aff", "deviceConfig": "0a17864de5505a20", "name": "Quittierung Pumpenstörung", "version": 6, "entityType": "button", "haConfig": [ { "property": "name", "value": "Quittierunf Pumpenstörung" }, { "property": "icon", "value": "" }, { "property": "entity_picture", "value": "" }, { "property": "entity_category", "value": "config" }, { "property": "device_class", "value": "restart" } ], "resend": false, "debugEnabled": true }, { "id": "bcc6944e1ff6c6e7", "type": "ha-entity-config", "server": "d9f42b9039b25aff", "deviceConfig": "0a17864de5505a20", "name": "Pumpenfehler", "version": 6, "entityType": "binary_sensor", "haConfig": [ { "property": "name", "value": "Pumpenfehler" }, { "property": "icon", "value": "" }, { "property": "entity_picture", "value": "" }, { "property": "entity_category", "value": "" }, { "property": "device_class", "value": "problem" } ], "resend": false, "debugEnabled": false }, { "id": "fd41de37db3bcf9f", "type": "ha-entity-config", "server": "d9f42b9039b25aff", "deviceConfig": "0a17864de5505a20", "name": "Verlorene Frames", "version": 6, "entityType": "sensor", "haConfig": [ { "property": "name", "value": "Verlorene Frames" }, { "property": "icon", "value": "mdi:alert" }, { "property": "entity_picture", "value": "" }, { "property": "entity_category", "value": "diagnostic" }, { "property": "device_class", "value": "" }, { "property": "unit_of_measurement", "value": "" }, { "property": "state_class", "value": "measurement" } ], "resend": false, "debugEnabled": false }, { "id": "26952e484ee2bdd1", "type": "function", "z": "b9502e7d5adbd1fc", "name": "function 1", "func": "let targetNode = env.get(\"TARGET_NODE\");\nlet pumpId = env.get(\"PUMP_ID\");\nlet pumpCmd = msg.payload; \n\nif (typeof pumpCmd !== \"number\") {\n node.warn(\"Befehl muss eine Zahl sein!\");\n return null;\n}\n\n// Wir bauen NUR den Payload (die 3 Bytes Nutzdaten)\nmsg.payload = Buffer.from([ targetNode, pumpId, pumpCmd ]);\n\n// 1. Das komplette Objekt mit allen IDs aus dem globalen Speicher holen\nlet CAN = global.get(\"CAN\");\n\n// 2. Wir hängen dem Paket als \"Post-It\" an, auf welcher CAN-ID es später reisen soll.\n// CAN.CMD_PUMP holt genau die 0x020 aus deiner globalen Liste.\nmsg.canId = CAN.CMD_PUMP; \n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 260, "y": 100, "wires": [ [] ] }, { "id": "6e8a2a39a56ae270", "type": "function", "z": "1a6e91b7110fc591", "name": "Druck vom CAN", "func": "let buf = msg.payload;\n\n// Sicherheitscheck: 6 Bytes erwartet\nif (buf.length !== 6) return null;\n\n// Bytes auslesen\nlet sourceNode = buf.readUInt8(0);\nlet sensorId = buf.readUInt8(1);\nlet pressurePa = buf.readUInt32BE(2); // Die 300.000 auslesen\n\n// Für Home Assistant rechnen wir Pascal wieder in Bar um\nlet pressureBar = pressurePa / 100000.0;\n\n// Payload überschreiben (HA will nur den reinen Wert)\nmsg.payload = pressurePa;\nmsg.sensorId = sensorId;\nmsg.nodeId = sourceNode;\n\n// Grüne LED am Knoten für direktes Feedback\nnode.status({fill:\"green\", shape:\"dot\", text: pressureBar.toFixed(2) + \" Bar\"});\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 200, "y": 40, "wires": [ [] ] }, { "id": "d5928c3e9f080926", "type": "udp in", "z": "0e938b2c7d519455", "name": "Garten CAN UDP In", "iface": "", "port": "${in_port}", "ipv": "udp4", "multicast": "false", "group": "", "datatype": "buffer", "x": 110, "y": 160, "wires": [ [ "59119f4834036e0f" ] ] }, { "id": "59119f4834036e0f", "type": "function", "z": "0e938b2c7d519455", "name": "Input Decoder", "func": "// sender IP merken für spätere Antworten\nflow.set(\"last_gateway_ip\", msg.ip);\n\n// msg.payload enthält den rohen Byte-Buffer vom ESP32/STM32\nlet buf = msg.payload;\n\n// Sicherheitscheck 1: Ist das Paket überhaupt groß genug für den Header?\nif (buf.length < 8) return null;\n\n// Header auslesen (Offsets korrigiert)\nlet canId = buf.readUInt32BE(0);\nlet flags = buf.readUInt8(4);\nlet dlc = buf.readUInt8(5);\nlet seqNo = buf.readUInt16BE(6);\n\n// Sicherheitscheck 2: Stimmen DLC und Puffergrösse überein?\nif (buf.length !== dlc + 8) {\n node.status({ fill: \"red\", shape: \"dot\", text: \"Size mismatch\" });\n return null;\n}\n\n// 1. CAN-ID als sauberen Hex-String formatieren\nlet canIdHex = canId.toString(16).toUpperCase();\n\n// 2. Die echten CAN-Nutzdaten aus dem Buffer herausschneiden (ab Byte 8)\nlet canData = buf.slice(8, 8 + dlc);\n\n// 3. Nachricht für die Weiche und die Decoder vorbereiten\nmsg.canId = canId; // Für den switch-Knoten\nmsg.payload = canData; // Ab hier liegen nur noch die echten Daten im Payload\nmsg.seqNo = seqNo; // Falls ein späterer Node die Sequenznummer braucht\n\n// Grüne LED mit Hex-ID anzeigen\nnode.status({ fill: \"green\", shape: \"dot\", text: \"ID: 0x\" + canIdHex + \" | Seq: \" + seqNo + \" | DLC: \" + dlc });\n\n// --- SEQUENZPRÜFUNG ---\n// Holt die Werte aus dem flüchtigen RAM-Kontext der Node\nlet oldSeq = context.get('oldSeq');\nlet errorCounter = context.get('errorCounter') || 0;\n\nif (oldSeq === undefined) {\n context.set('oldSeq', seqNo);\n context.set('errorCounter', errorCounter);\n msg.errorCounter = errorCounter;\n return msg;\n}\n\n// Delta berechnen (16-Bit Maske wegen readUInt16BE)\nlet delta = (seqNo - oldSeq) & 0xFFFF;\n\nif (delta > 1) {\n // Da du oben 10000 statt 100 gewählt hast: absolut okay für 16-Bit-Zähler bei schnellen Telegrammen\n if (delta < 10000) {\n let lostFrames = delta - 1;\n errorCounter += lostFrames;\n // FIX: Variable auf errorCounter korrigiert\n node.warn(`${lostFrames} Frame(s) verloren! Zähler steht auf: ${errorCounter}`);\n } else {\n // FIX: Variable auf seqNo korrigiert\n node.log(`Sensor-Reset erkannt (Sprung von ${oldSeq} auf ${seqNo}).`);\n }\n}\n\n// Werte für das nächste Paket im RAM merken\ncontext.set('oldSeq', seqNo);\ncontext.set('errorCounter', errorCounter);\n\n// Fehlerzähler an die Nachricht hängen\nmsg.errorCounter = errorCounter;\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 360, "y": 160, "wires": [ [ "1be1fe0ccdfb3d67", "d2f4efb6f0a3199d" ] ] }, { "id": "f8eb45ba33d70b65", "type": "ha-button", "z": "0e938b2c7d519455", "name": "Quittierung Pumpenstörung", "version": 0, "debugenabled": false, "outputs": 1, "entityConfig": "92579cd9fdb0eda1", "outputProperties": [ { "property": "payload", "propertyType": "msg", "value": "0x05", "valueType": "num" } ], "x": 140, "y": 480, "wires": [ [ "a947a45468017733" ] ] }, { "id": "a0a226dd400461d8", "type": "comment", "z": "0e938b2c7d519455", "name": "", "info": "### Funktionale CAN-Map (11-Bit)\n\n| Hex-ID | Dezimal | Kategorie | Beschreibung |\n| :--- | :--- | :--- | :--- |\n| **`0x000`** | 0 | **NMT** | Network Management (Alle Halt/Start) |\n| **`0x010`** | 16 | **Alarm** | Emergency (Saugrohr trocken, Druck kritisch) |\n| **`0x020`** | 32 | **Befehl** | Pumpensteuerung (Auto/Aus/Reset) |\n| **`0x030`** | 48 | **Befehl** | Ventil setzen (Pos in %) |\n| **`0x100`** | 256 | **Wert** | Systemdruck (Pascal) |\n| **`0x110`** | 272 | **Wert** | Füllstand (Tank / Einperlverfahren) |\n| **`0x200`** | 512 | **Zähler** | Durchflusszähler (Ticks) |\n| **`0x220`** | 544 | **Status** | Pumpenstatus |\n| **`0x230`** | 560 | **Status** | Ventilstatus |\n| **`0x7E0`** | 2016| **System**| Firmware Update (bis 0x7EF) |\n| **`0x7F0`** | 2032| **System**| Debug & Logging (bis 0x7FF) |", "x": 80, "y": 60, "wires": [] }, { "id": "1be1fe0ccdfb3d67", "type": "switch", "z": "0e938b2c7d519455", "name": "", "property": "canId", "propertyType": "msg", "rules": [ { "t": "eq", "v": "CAN.STAT_PUMP", "vt": "global" }, { "t": "eq", "v": "CAN.VAL_PRESSURE", "vt": "global" } ], "checkall": "true", "repair": false, "outputs": 2, "x": 550, "y": 160, "wires": [ [ "0c60399fc4327d06" ], [ "5538f815301a6ace" ] ] }, { "id": "0c60399fc4327d06", "type": "function", "z": "0e938b2c7d519455", "name": "Pumpenstatus", "func": "let buf = msg.payload;\n\n// Sicherheitscheck: Wir wollen 3 Bytes lesen (Index 0, 1, 2)\nif (buf.length !== 3) return null;\n\nlet nodeId = buf.readUInt8(0);\nlet pumpId = buf.readUInt8(1);\nlet pumpState = buf.readUInt8(2);\n\nlet runState = false;\nlet errorState = false;\n\nswitch (pumpState) {\n case 0:\n runState = false;\n errorState = false;\n break;\n case 1:\n runState = true;\n errorState = false;\n break;\n case 2:\n default:\n runState = false;\n errorState = true;\n break;\n}\n\n// Nachricht 1 (Ausgang Oben) vorbereiten\nmsg.payload = runState;\nmsg.pumpId = pumpId;\nmsg.disabled = errorState;\nmsg.nodeId = nodeId;\n\n// Nachricht 2 (Ausgang Unten) vorbereiten\n// Wichtig: Wir müssen das Objekt erst erschaffen!\nlet msg_error = {\n payload: errorState,\n pumpId: pumpId,\n nodeId: nodeId\n};\n\n// Grüne LED anzeigen (wir nutzen msg.canId, falls du das vorher übergeben hast)\nnode.status({ fill: \"green\", shape: \"dot\", text: \"Pumpe \" + pumpId + \" | Status: \" + pumpState });\n\n// Ausgang 1 bekommt msg, Ausgang 2 bekommt msg_error\nreturn [msg, msg_error];", "outputs": 2, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 800, "y": 120, "wires": [ [ "2f788ae7d216ce09" ], [ "63466986c3d2f412" ] ] }, { "id": "2f788ae7d216ce09", "type": "ha-binary-sensor", "z": "0e938b2c7d519455", "name": "Pumpe", "entityConfig": "9cba2b96af7846f4", "version": 0, "state": "payload", "stateType": "msg", "attributes": [ { "property": "disable", "value": "disabled", "valueType": "msg" } ], "inputOverride": "allow", "outputProperties": [], "x": 1020, "y": 100, "wires": [ [] ] }, { "id": "63466986c3d2f412", "type": "ha-binary-sensor", "z": "0e938b2c7d519455", "name": "Pumpenfehler", "entityConfig": "bcc6944e1ff6c6e7", "version": 0, "state": "payload", "stateType": "msg", "attributes": [], "inputOverride": "block", "outputProperties": [], "x": 1040, "y": 160, "wires": [ [] ] }, { "id": "a947a45468017733", "type": "subflow:b9502e7d5adbd1fc", "z": "0e938b2c7d519455", "name": "", "env": [ { "name": "TARGET_NODE", "value": "1", "type": "num" }, { "name": "PUMP_ID", "value": "1", "type": "num" } ], "x": 400, "y": 480, "wires": [ [ "259a6eb05825188b" ] ] }, { "id": "36e2dae7fa76fa4f", "type": "debug", "z": "0e938b2c7d519455", "name": "Systemdruck", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "true", "targetType": "full", "statusVal": "", "statusType": "auto", "x": 1330, "y": 240, "wires": [] }, { "id": "efce4aea184f149f", "type": "inject", "z": "0e938b2c7d519455", "name": "", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": true, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 330, "y": 60, "wires": [ [ "14566063729419a9" ] ] }, { "id": "14566063729419a9", "type": "function", "z": "0e938b2c7d519455", "name": "Globals", "func": "// Wir nutzen jetzt echte Hex-Zahlen (ohne Anführungszeichen!)\nconst CAN_IDS = {\n // Alarme\n ALARM: 0x010, \n \n // Befehle\n CMD_PUMP: 0x020, \n CMD_VALVE: 0x030, \n \n // Werte\n VAL_PRESSURE: 0x100,\n\n // Status\n STAT_PUMP: 0x220,\n};\n\nglobal.set(\"CAN\", CAN_IDS);\n\nnode.status({fill:\"green\", shape:\"dot\", text:\"Globale CAN-IDs geladen\"});\nreturn null;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 540, "y": 60, "wires": [ [] ] }, { "id": "259a6eb05825188b", "type": "function", "z": "0e938b2c7d519455", "name": "Output Encoder", "func": "let targetIp = flow.get(\"last_gateway_ip\");\n\nif (!targetIp) {\n node.warn(\"Verworfen: Noch keine Gateway-IP gelernt. Warte auf erstes Paket vom ESP...\");\n return null;\n}\n\n// 2. Daten vom vorgeschalteten Knoten holen\nlet canData = msg.payload; // Die 3 Bytes aus dem Subflow\nlet canIdHex = msg.canId; // Die gewünschte CAN-ID (\"20\")\n\n// Sequenznummer global hochzählen\nlet seqNo = global.get(\"txSeqNo\") || 0;\nglobal.set(\"txSeqNo\", (seqNo + 1) % 65536);\n\n// 3. Den rohen UDP-Buffer bauen: 8 Bytes Header + X Bytes Daten\n// Das geniale: canData.length macht die DLC absolut dynamisch!\nlet buf = Buffer.alloc(8 + canData.length);\n\n// --- HEADER ---\nbuf.writeUInt32BE(parseInt(canIdHex, 16), 0); // CAN-ID umwandeln\nbuf.writeUInt8(0, 4); // Flags\nbuf.writeUInt8(canData.length, 5); // DLC (berechnet sich von selbst)\nbuf.writeUInt16BE(seqNo, 6); // Sequenznummer\n\n// --- PAYLOAD ---\n// Kopiert unsere 3 Nutzdaten-Bytes exakt an Position 8 in den großen Buffer\ncanData.copy(buf, 8);\n\n// 4. Feuer frei für den udp-out Knoten\nmsg.payload = buf;\nmsg.ip = targetIp;\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 600, "y": 480, "wires": [ [ "97252b8dc7a55b3e" ] ] }, { "id": "97252b8dc7a55b3e", "type": "udp out", "z": "0e938b2c7d519455", "name": "", "addr": "", "iface": "", "port": "${out_port}", "ipv": "udp4", "outport": "", "base64": false, "multicast": "false", "x": 840, "y": 480, "wires": [] }, { "id": "5538f815301a6ace", "type": "subflow:1a6e91b7110fc591", "z": "0e938b2c7d519455", "name": "", "x": 770, "y": 280, "wires": [ [ "d7b39d46bdc6d1c8" ] ] }, { "id": "c43b3701443a6dfe", "type": "ha-sensor", "z": "0e938b2c7d519455", "name": "Systemdruck", "entityConfig": "79cd4bcaeefb790a", "version": 0, "state": "payload", "stateType": "msg", "attributes": [], "inputOverride": "allow", "outputProperties": [], "x": 1130, "y": 240, "wires": [ [ "36e2dae7fa76fa4f" ] ] }, { "id": "d7b39d46bdc6d1c8", "type": "switch", "z": "0e938b2c7d519455", "name": "", "property": "sensorId", "propertyType": "msg", "rules": [ { "t": "eq", "v": "1", "vt": "num" }, { "t": "eq", "v": "2", "vt": "num" } ], "checkall": "true", "repair": false, "outputs": 2, "x": 910, "y": 280, "wires": [ [ "c43b3701443a6dfe" ], [ "aaedd1ee4c078969" ] ] }, { "id": "aaedd1ee4c078969", "type": "ha-sensor", "z": "0e938b2c7d519455", "name": "Wasserstand", "entityConfig": "11087cbf4f9379d1", "version": 0, "state": "payload", "stateType": "msg", "attributes": [], "inputOverride": "allow", "outputProperties": [], "x": 1130, "y": 300, "wires": [ [ "d547519394f23d1a" ] ] }, { "id": "d547519394f23d1a", "type": "debug", "z": "0e938b2c7d519455", "name": "Wasserstand", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 1330, "y": 300, "wires": [] }, { "id": "d2f4efb6f0a3199d", "type": "ha-sensor", "z": "0e938b2c7d519455", "name": "Verlorene Frames", "entityConfig": "fd41de37db3bcf9f", "version": 0, "state": "errorCounter", "stateType": "msg", "attributes": [], "inputOverride": "allow", "outputProperties": [], "x": 550, "y": 260, "wires": [ [] ] } ]