Ich denke ich habe es geschafft und zeige hier den Flow, für alle die vor einem ählichen Thema stehen.
[
{
"id": "924b846988fa37b8",
"type": "tab",
"label": "Pytes",
"disabled": false,
"info": "",
"env": []
},
{
"id": "7b0f9153dc441129",
"type": "inject",
"z": "924b846988fa37b8",
"name": "Start Impuls",
"props": [
{
"p": "payload"
}
],
"repeat": "900",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "bat",
"payloadType": "str",
"x": 270,
"y": 420,
"wires": [
[
"93bb9e44683dd328",
"00be40db9cc98e10",
"7d79635bc32374b4",
"a7456323ba755bf3",
"7b0d5d1b02bd9085",
"f3f5ae187655f871",
"92a88797a1b73b4a",
"03519a44cb4f244f",
"2b0172f542977f16"
]
]
},
{
"id": "b377e7183086a062",
"type": "trigger",
"z": "924b846988fa37b8",
"name": "bat 1",
"op1": "bat 1",
"op2": "",
"op1type": "str",
"op2type": "nul",
"duration": "250",
"extend": false,
"overrideDelay": false,
"units": "ms",
"reset": "",
"bytopic": "all",
"topic": "topic",
"outputs": 1,
"x": 950,
"y": 420,
"wires": [
[
"26225d8717897af1"
]
]
},
{
"id": "26225d8717897af1",
"type": "serial request",
"z": "924b846988fa37b8",
"name": "BMSMaster",
"serial": "d031dd424e7b1ff2",
"x": 1250,
"y": 420,
"wires": [
[
"b2c11fc7a1bd0a9c"
]
]
},
{
"id": "30b978183f66ca0b",
"type": "debug",
"z": "924b846988fa37b8",
"name": "debug 4",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 2100,
"y": 500,
"wires": []
},
{
"id": "be4f995ff9ae4db1",
"type": "function",
"z": "924b846988fa37b8",
"name": "Nachrichten erzeugen",
"func": "if (msg.messageType !== \"data\") {\n return null;\n}\n\nvar f_battery = flow.get(\"f_battery\");\n\nconst battery = msg.payload;\n\nif (!battery || battery.battery === undefined) {\n node.error(\"Ungültiges Battery-Objekt\", msg);\n return null;\n}\n\nconst batteryId = battery.battery;\n\nconst states = [\n {\n id: \"voltage\",\n value: battery.volt,\n name: \"Voltage\",\n role: \"value.voltage\",\n type: \"number\",\n readonly: true,\n unit: \"mV\",\n min: 0\n },\n {\n id: \"temperature\",\n value: battery.tempr / 1000,\n name: \"Temperature\",\n role: \"value.temperature\",\n type: \"number\",\n readonly: true,\n unit: \"°C\"\n },\n {\n id: \"baseState\",\n value: battery.baseState,\n name: \"Base State\",\n role: \"text\",\n type: \"string\",\n readonly: true\n },\n {\n id: \"voltageState\",\n value: battery.voltState,\n name: \"Voltage State\",\n role: \"text\",\n type: \"string\",\n readonly: true\n },\n {\n id: \"currentState\",\n value: battery.currState,\n name: \"Current State\",\n role: \"text\",\n type: \"string\",\n readonly: true\n },\n {\n id: \"temperatureState\",\n value: battery.tempState,\n name: \"Temperature State\",\n role: \"text\",\n type: \"string\",\n readonly: true\n },\n {\n id: \"charge\",\n value: battery.coulomb,\n name: \"Charge\",\n role: \"value.battery\",\n type: \"number\",\n readonly: true,\n unit: \"%\",\n min: 0,\n max: 100\n },\n {\n id: \"capacity\",\n value: battery.capacity,\n name: \"Capacity\",\n role: \"value\",\n type: \"number\",\n readonly: true,\n unit: \"mAh\",\n min: 0\n }\n];\n\nconst messages = [];\n\nfor (const state of states) {\n const out = {\n topic: `0_userdata.0.pytes2.${f_battery}.${batteryId}.${state.id}`,\n payload: state.value,\n\n stateName: state.name,\n stateRole: state.role,\n stateType: state.type,\n stateReadonly: state.readonly\n };\n\n if (state.unit !== undefined) {\n out.stateUnit = state.unit;\n }\n\n if (state.min !== undefined) {\n out.stateMin = state.min;\n }\n\n if (state.max !== undefined) {\n out.stateMax = state.max;\n }\n\n messages.push(out);\n}\n\nreturn [messages];",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1860,
"y": 420,
"wires": [
[
"30b978183f66ca0b",
"26c696e68b092904"
]
]
},
{
"id": "93bb9e44683dd328",
"type": "function",
"z": "924b846988fa37b8",
"name": "bat 1",
"func": "var f_battery = flow.get('f_battery') || 1;\nf_battery = 1;\nflow.set('f_battery', f_battery);\nmsg.payload=f_battery;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 810,
"y": 420,
"wires": [
[
"b377e7183086a062"
]
]
},
{
"id": "8ccdf3ea686387b8",
"type": "trigger",
"z": "924b846988fa37b8",
"name": "bat 2",
"op1": "bat 2",
"op2": "",
"op1type": "str",
"op2type": "nul",
"duration": "250",
"extend": false,
"overrideDelay": false,
"units": "ms",
"reset": "",
"bytopic": "all",
"topic": "topic",
"outputs": 1,
"x": 950,
"y": 520,
"wires": [
[
"26225d8717897af1"
]
]
},
{
"id": "489520c23b161924",
"type": "function",
"z": "924b846988fa37b8",
"name": "bat 2",
"func": "var f_battery = flow.get('f_battery') || 1;\nf_battery = 2;\nflow.set('f_battery', f_battery);\nmsg.payload=f_battery;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 810,
"y": 520,
"wires": [
[
"8ccdf3ea686387b8"
]
]
},
{
"id": "00be40db9cc98e10",
"type": "delay",
"z": "924b846988fa37b8",
"name": "",
"pauseType": "delay",
"timeout": "1",
"timeoutUnits": "minutes",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 640,
"y": 520,
"wires": [
[
"489520c23b161924"
]
]
},
{
"id": "ae16d8955790d92a",
"type": "trigger",
"z": "924b846988fa37b8",
"name": "bat 3",
"op1": "bat 3",
"op2": "",
"op1type": "str",
"op2type": "nul",
"duration": "250",
"extend": false,
"overrideDelay": false,
"units": "ms",
"reset": "",
"bytopic": "all",
"topic": "topic",
"outputs": 1,
"x": 950,
"y": 620,
"wires": [
[
"26225d8717897af1"
]
]
},
{
"id": "4538197122a9bdc2",
"type": "function",
"z": "924b846988fa37b8",
"name": "bat 3",
"func": "var f_battery = flow.get('f_battery') || 1;\nf_battery = 3;\nflow.set('f_battery', f_battery);\nmsg.payload=f_battery;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 810,
"y": 620,
"wires": [
[
"ae16d8955790d92a"
]
]
},
{
"id": "7d79635bc32374b4",
"type": "delay",
"z": "924b846988fa37b8",
"name": "",
"pauseType": "delay",
"timeout": "2",
"timeoutUnits": "minutes",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 640,
"y": 620,
"wires": [
[
"4538197122a9bdc2"
]
]
},
{
"id": "faf99396662dd9fe",
"type": "trigger",
"z": "924b846988fa37b8",
"name": "bat 4",
"op1": "bat 4",
"op2": "",
"op1type": "str",
"op2type": "nul",
"duration": "250",
"extend": false,
"overrideDelay": false,
"units": "ms",
"reset": "",
"bytopic": "all",
"topic": "topic",
"outputs": 1,
"x": 950,
"y": 720,
"wires": [
[
"26225d8717897af1"
]
]
},
{
"id": "2935dde14a7e9b69",
"type": "function",
"z": "924b846988fa37b8",
"name": "bat 4",
"func": "var f_battery = flow.get('f_battery') || 1;\nf_battery = 4;\nflow.set('f_battery', f_battery);\nmsg.payload=f_battery;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 810,
"y": 720,
"wires": [
[
"faf99396662dd9fe"
]
]
},
{
"id": "a7456323ba755bf3",
"type": "delay",
"z": "924b846988fa37b8",
"name": "",
"pauseType": "delay",
"timeout": "3",
"timeoutUnits": "minutes",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 640,
"y": 720,
"wires": [
[
"2935dde14a7e9b69"
]
]
},
{
"id": "0283b7e720d4eae8",
"type": "trigger",
"z": "924b846988fa37b8",
"name": "bat 5",
"op1": "bat 5",
"op2": "",
"op1type": "str",
"op2type": "nul",
"duration": "250",
"extend": false,
"overrideDelay": false,
"units": "ms",
"reset": "",
"bytopic": "all",
"topic": "topic",
"outputs": 1,
"x": 950,
"y": 820,
"wires": [
[
"26225d8717897af1"
]
]
},
{
"id": "035e99dbc09bc62e",
"type": "function",
"z": "924b846988fa37b8",
"name": "bat 5",
"func": "var f_battery = flow.get('f_battery') || 1;\nf_battery = 5;\nflow.set('f_battery', f_battery);\nmsg.payload=f_battery;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 810,
"y": 820,
"wires": [
[
"0283b7e720d4eae8"
]
]
},
{
"id": "7b0d5d1b02bd9085",
"type": "delay",
"z": "924b846988fa37b8",
"name": "",
"pauseType": "delay",
"timeout": "4",
"timeoutUnits": "minutes",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 640,
"y": 820,
"wires": [
[
"035e99dbc09bc62e"
]
]
},
{
"id": "caa332d68e18fa0e",
"type": "trigger",
"z": "924b846988fa37b8",
"name": "bat 6",
"op1": "bat 6",
"op2": "",
"op1type": "str",
"op2type": "nul",
"duration": "250",
"extend": false,
"overrideDelay": false,
"units": "ms",
"reset": "",
"bytopic": "all",
"topic": "topic",
"outputs": 1,
"x": 950,
"y": 920,
"wires": [
[
"26225d8717897af1"
]
]
},
{
"id": "f0b60bc3dd7fab8b",
"type": "function",
"z": "924b846988fa37b8",
"name": "bat 6",
"func": "var f_battery = flow.get('f_battery') || 1;\nf_battery = 6;\nflow.set('f_battery', f_battery);\nmsg.payload=f_battery;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 810,
"y": 920,
"wires": [
[
"caa332d68e18fa0e"
]
]
},
{
"id": "f3f5ae187655f871",
"type": "delay",
"z": "924b846988fa37b8",
"name": "",
"pauseType": "delay",
"timeout": "5",
"timeoutUnits": "minutes",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 640,
"y": 920,
"wires": [
[
"f0b60bc3dd7fab8b"
]
]
},
{
"id": "a4f9934bfcf077f9",
"type": "trigger",
"z": "924b846988fa37b8",
"name": "bat 7",
"op1": "bat 7",
"op2": "",
"op1type": "str",
"op2type": "nul",
"duration": "250",
"extend": false,
"overrideDelay": false,
"units": "ms",
"reset": "",
"bytopic": "all",
"topic": "topic",
"outputs": 1,
"x": 950,
"y": 1020,
"wires": [
[
"26225d8717897af1"
]
]
},
{
"id": "225333085c3b1b95",
"type": "function",
"z": "924b846988fa37b8",
"name": "bat 7",
"func": "var f_battery = flow.get('f_battery') || 1;\nf_battery = 7;\nflow.set('f_battery', f_battery);\nmsg.payload=f_battery;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 810,
"y": 1020,
"wires": [
[
"a4f9934bfcf077f9"
]
]
},
{
"id": "92a88797a1b73b4a",
"type": "delay",
"z": "924b846988fa37b8",
"name": "",
"pauseType": "delay",
"timeout": "6",
"timeoutUnits": "minutes",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 640,
"y": 1020,
"wires": [
[
"225333085c3b1b95"
]
]
},
{
"id": "9089dacc8e8b8393",
"type": "trigger",
"z": "924b846988fa37b8",
"name": "bat 8",
"op1": "bat 8",
"op2": "",
"op1type": "str",
"op2type": "nul",
"duration": "250",
"extend": false,
"overrideDelay": false,
"units": "ms",
"reset": "",
"bytopic": "all",
"topic": "topic",
"outputs": 1,
"x": 950,
"y": 1120,
"wires": [
[
"26225d8717897af1"
]
]
},
{
"id": "2a37874391c9c5d0",
"type": "function",
"z": "924b846988fa37b8",
"name": "bat 8",
"func": "var f_battery = flow.get('f_battery') || 1;\nf_battery = 8;\nflow.set('f_battery', f_battery);\nmsg.payload=f_battery;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 810,
"y": 1120,
"wires": [
[
"9089dacc8e8b8393"
]
]
},
{
"id": "03519a44cb4f244f",
"type": "delay",
"z": "924b846988fa37b8",
"name": "",
"pauseType": "delay",
"timeout": "7",
"timeoutUnits": "minutes",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 640,
"y": 1120,
"wires": [
[
"2a37874391c9c5d0"
]
]
},
{
"id": "f4a30319262f08c0",
"type": "catch",
"z": "924b846988fa37b8",
"name": "",
"scope": "group",
"uncaught": false,
"x": 1370,
"y": 300,
"wires": [
[
"676dc7e85bab0fb0"
]
]
},
{
"id": "2948459dbbd003a5",
"type": "debug",
"z": "924b846988fa37b8",
"name": "debug 1",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 1700,
"y": 560,
"wires": []
},
{
"id": "76351a67c6019345",
"type": "function",
"z": "924b846988fa37b8",
"name": "parser 232",
"func": "/*\n * Robuster Parser für RS232-Batteriedaten\n *\n * Eigenschaften:\n * - verarbeitet fragmentierte Serial-Daten über mehrere msg.payload hinweg\n * - erkennt Beginn eines Datensatzes anhand der Headerzeile\n * - erkennt Ende anhand \"Command completed successfully\"\n * - erwartet Batterien 0..15 genau einmal\n * - verwirft unvollständige Datensätze komplett\n * - prüft Zahlen, Prozentwert, Capacity und grobe Wertebereiche\n * - erzeugt Diagnosemeldungen für Warnungen/Fehler\n * - schreibt Warnungen zusätzlich in die Node-RED Debug-/Log-Ausgabe\n *\n * EIN Ausgang:\n * msg.messageType === \"data\" -> gültige Batteriedaten\n * msg.messageType === \"warning\" -> Diagnose/Warnung\n * msg.messageType === \"error\" -> Diagnose/Fehler\n */\n \n \n// ------------------------------------------------------------\n// Konfiguration\n// ------------------------------------------------------------\n \nconst EXPECTED_BATTERIES = 16;\n \n// Maximale Zeit, die ein begonnener Datensatz offen bleiben darf.\n// Die Prüfung erfolgt beim nächsten eintreffenden msg.\nconst BLOCK_TIMEOUT_MS = 30000;\n \n// Schutz gegen unbegrenzt anwachsende Fragmentpuffer\nconst MAX_FRAGMENT_LENGTH = 4096;\n \n// Schutz gegen völlig entartete Datensätze\nconst MAX_BLOCK_LINES = 100;\n \n// Sehr großzügige Plausibilitätsgrenzen.\n// Diese können später an die tatsächliche Anlage angepasst werden.\nconst LIMITS = {\n voltMin: 0,\n voltMax: 10000,\n \n temprMin: -100000,\n temprMax: 200000,\n \n coulombMin: 0,\n coulombMax: 100,\n \n capacityMin: 0,\n capacityMax: 1000000\n};\n \n \n// ------------------------------------------------------------\n// Eingang prüfen\n// ------------------------------------------------------------\n \nconst text = msg.payload;\n \nif (typeof text !== \"string\") {\n \n const diagnostic = createDiagnostic(\n \"error\",\n \"INVALID_PAYLOAD_TYPE\",\n \"msg.payload ist kein String\",\n {\n receivedType: typeof text\n }\n );\n \n node.error(diagnostic, msg);\n \n return [[diagnostic]];\n}\n \n \n// ------------------------------------------------------------\n// Zustand aus Node-Context holen\n// ------------------------------------------------------------\n \nlet fragment = context.get(\"serialFragment\") || \"\";\nlet block = context.get(\"batteryBlock\") || null;\n \n \n// ------------------------------------------------------------\n// Ausgaben dieser Function-Ausführung\n// ------------------------------------------------------------\n \nconst output = [];\n \n \n// ------------------------------------------------------------\n// Diagnosefunktion\n// ------------------------------------------------------------\n \nfunction createDiagnostic(severity, code, message, details) {\n \n return {\n ...msg,\n \n messageType: severity,\n \n payload: {\n severity: severity,\n code: code,\n message: message,\n timestamp: new Date().toISOString(),\n details: details || {}\n }\n };\n}\n \n \nfunction diagnostic(severity, code, message, details) {\n \n const diag = createDiagnostic(\n severity,\n code,\n message,\n details\n );\n \n output.push(diag);\n \n // Zusätzlich in Node-RED sichtbar machen\n if (severity === \"error\") {\n node.error(\n code + \": \" + message +\n (details ? \" | \" + JSON.stringify(details) : \"\")\n );\n } else {\n node.warn(\n code + \": \" + message +\n (details ? \" | \" + JSON.stringify(details) : \"\")\n );\n }\n}\n \n \n// ------------------------------------------------------------\n// Hilfsfunktionen\n// ------------------------------------------------------------\n \nfunction isHeader(line) {\n \n return /^Battery\\s+Volt\\s+Tempr\\s+Base\\s+State/i.test(line);\n}\n \n \nfunction isEndMarker(line) {\n \n return /^Command completed successfully\\s*$/i.test(line);\n}\n \n \nfunction isPromptOrNoise(line) {\n \n return (\n line === \"\" ||\n line === \"$$\" ||\n line === \"@\" ||\n /^PYTES>/i.test(line)\n );\n}\n \n \n// ------------------------------------------------------------\n// Batteriezeile parsen\n// ------------------------------------------------------------\n \nfunction parseBatteryLine(line) {\n \n /*\n * Beispiel:\n *\n * 0 3462 28000 Charge Normal\n * Normal Normal 97% 95980 mAH\n *\n * Absichtlich nicht nur split(/\\s{2,}/), damit kleinere\n * Änderungen der Abstände nicht sofort zum Fehler führen.\n */\n \n const match = line.match(\n /^(\\d+)\\s+(-?\\d+(?:\\.\\d+)?)\\s+(-?\\d+(?:\\.\\d+)?)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\d+(?:\\.\\d+)?)%\\s+(\\d+)\\s*mAH\\s*$/i\n );\n \n if (!match) {\n return {\n valid: false,\n reason: \"FORMAT_ERROR\"\n };\n }\n \n const battery = {\n battery: Number(match[1]),\n volt: Number(match[2]),\n tempr: Number(match[3]),\n baseState: match[4],\n voltState: match[5],\n currState: match[6],\n tempState: match[7],\n coulomb: Number(match[8]),\n capacity: Number(match[9])\n };\n \n \n // --------------------------------------------------------\n // Datentypen prüfen\n // --------------------------------------------------------\n \n const numericValues = [\n battery.battery,\n battery.volt,\n battery.tempr,\n battery.coulomb,\n battery.capacity\n ];\n \n if (numericValues.some(v => !Number.isFinite(v))) {\n \n return {\n valid: false,\n reason: \"INVALID_NUMBER\",\n battery: battery\n };\n }\n \n \n // --------------------------------------------------------\n // Harte Plausibilitätsprüfungen\n // --------------------------------------------------------\n \n if (\n battery.battery < 0 ||\n battery.battery >= EXPECTED_BATTERIES\n ) {\n return {\n valid: false,\n reason: \"INVALID_BATTERY_NUMBER\",\n battery: battery\n };\n }\n \n \n if (\n battery.coulomb < LIMITS.coulombMin ||\n battery.coulomb > LIMITS.coulombMax\n ) {\n return {\n valid: false,\n reason: \"INVALID_COULOMB_VALUE\",\n battery: battery\n };\n }\n \n \n if (\n battery.capacity < LIMITS.capacityMin ||\n battery.capacity > LIMITS.capacityMax\n ) {\n return {\n valid: false,\n reason: \"INVALID_CAPACITY_VALUE\",\n battery: battery\n };\n }\n \n \n if (\n battery.volt < LIMITS.voltMin ||\n battery.volt > LIMITS.voltMax\n ) {\n return {\n valid: false,\n reason: \"INVALID_VOLTAGE_VALUE\",\n battery: battery\n };\n }\n \n \n if (\n battery.tempr < LIMITS.temprMin ||\n battery.tempr > LIMITS.temprMax\n ) {\n return {\n valid: false,\n reason: \"INVALID_TEMPERATURE_VALUE\",\n battery: battery\n };\n }\n \n \n return {\n valid: true,\n battery: battery\n };\n}\n \n \n// ------------------------------------------------------------\n// Angefangenen Block verwerfen\n// ------------------------------------------------------------\n \nfunction discardBlock(reason, additionalDetails) {\n \n if (!block) {\n return;\n }\n \n const receivedBatteries = Object.keys(block.batteries)\n .map(Number)\n .sort((a, b) => a - b);\n \n diagnostic(\n \"warning\",\n \"BLOCK_DISCARDED\",\n \"Unvollständiger oder beschädigter Batteriedatensatz wurde verworfen.\",\n {\n reason: reason,\n receivedBatteryCount: receivedBatteries.length,\n receivedBatteries: receivedBatteries,\n invalidLines: block.invalidLines,\n startedAt: new Date(block.startedAt).toISOString(),\n ...additionalDetails\n }\n );\n \n block = null;\n}\n \n \n// ------------------------------------------------------------\n// Fertigen Block validieren\n// ------------------------------------------------------------\n \nfunction finishBlock() {\n \n if (!block) {\n \n diagnostic(\n \"warning\",\n \"END_WITHOUT_HEADER\",\n \"Blockende erkannt, obwohl kein aktiver Datensatz vorhanden war.\"\n );\n \n return;\n }\n \n \n const batteryNumbers = Object.keys(block.batteries)\n .map(Number)\n .sort((a, b) => a - b);\n \n \n // Erwartete Batterien erzeugen\n const expected = [];\n \n for (let i = 0; i < EXPECTED_BATTERIES; i++) {\n expected.push(i);\n }\n \n \n const missing = expected.filter(\n batteryNumber => !batteryNumbers.includes(batteryNumber)\n );\n \n \n // --------------------------------------------------------\n // Datensatz unvollständig\n // --------------------------------------------------------\n \n if (\n batteryNumbers.length !== EXPECTED_BATTERIES ||\n missing.length > 0 ||\n block.invalidLines.length > 0\n ) {\n \n diagnostic(\n \"warning\",\n \"INCOMPLETE_DATASET\",\n \"Datensatz wurde vollständig empfangen beendet, enthält aber nicht alle erwarteten gültigen Batteriedaten.\",\n {\n expectedBatteryCount: EXPECTED_BATTERIES,\n receivedBatteryCount: batteryNumbers.length,\n receivedBatteries: batteryNumbers,\n missingBatteries: missing,\n invalidLines: block.invalidLines,\n startedAt: new Date(block.startedAt).toISOString(),\n durationMs: Date.now() - block.startedAt\n }\n );\n \n block = null;\n \n return;\n }\n \n \n // --------------------------------------------------------\n // Vollständigen Datensatz ausgeben\n // --------------------------------------------------------\n \n const batchId =\n Date.now().toString(36) +\n \"-\" +\n Math.random().toString(36).substring(2, 8);\n \n \n for (let i = 0; i < EXPECTED_BATTERIES; i++) {\n \n output.push({\n ...msg,\n \n messageType: \"data\",\n \n batchId: batchId,\n \n dataQuality: {\n valid: true,\n completeDataset: true,\n batteryCount: EXPECTED_BATTERIES\n },\n \n payload: block.batteries[i]\n });\n }\n \n \n // optional im Node-Status sichtbar\n node.status({\n fill: \"green\",\n shape: \"dot\",\n text: \"16 Batterien OK\"\n });\n \n \n block = null;\n}\n \n \n// ------------------------------------------------------------\n// Einzelne komplette Textzeile verarbeiten\n// ------------------------------------------------------------\n \nfunction processLine(rawLine) {\n \n const line = rawLine.trim();\n \n \n // --------------------------------------------------------\n // Header\n // --------------------------------------------------------\n \n if (isHeader(line)) {\n \n // Es läuft bereits ein Block:\n // der vorherige wurde offensichtlich nicht sauber beendet.\n if (block) {\n \n discardBlock(\n \"NEW_HEADER_BEFORE_END\",\n {\n newHeader: line\n }\n );\n }\n \n \n block = {\n startedAt: Date.now(),\n batteries: {},\n invalidLines: [],\n lineCount: 0\n };\n \n \n node.status({\n fill: \"yellow\",\n shape: \"ring\",\n text: \"Datensatz empfangen...\"\n });\n \n return;\n }\n \n \n // --------------------------------------------------------\n // Blockende\n // --------------------------------------------------------\n \n if (isEndMarker(line)) {\n \n finishBlock();\n \n return;\n }\n \n \n // --------------------------------------------------------\n // bekannte Prompt-/Steuerzeilen ignorieren\n // --------------------------------------------------------\n \n if (isPromptOrNoise(line)) {\n return;\n }\n \n \n // --------------------------------------------------------\n // Außerhalb eines Blocks\n // --------------------------------------------------------\n \n if (!block) {\n \n // Batteriezeile außerhalb eines Headers ist verdächtig\n if (/^\\d+\\s/.test(line)) {\n \n diagnostic(\n \"warning\",\n \"DATA_OUTSIDE_BLOCK\",\n \"Batteriezeile außerhalb eines gültigen Datensatzes erkannt und verworfen.\",\n {\n rawLine: line\n }\n );\n }\n \n return;\n }\n \n \n block.lineCount++;\n \n \n // --------------------------------------------------------\n // Sicherheitslimit\n // --------------------------------------------------------\n \n if (block.lineCount > MAX_BLOCK_LINES) {\n \n discardBlock(\n \"TOO_MANY_LINES\",\n {\n lineCount: block.lineCount\n }\n );\n \n return;\n }\n \n \n // --------------------------------------------------------\n // Nur Zeilen, die mit Batterie-ID anfangen, interessieren\n // --------------------------------------------------------\n \n if (!/^\\d+\\s/.test(line)) {\n \n diagnostic(\n \"warning\",\n \"UNEXPECTED_LINE\",\n \"Unerwartete Zeile innerhalb eines Batteriedatensatzes.\",\n {\n rawLine: line\n }\n );\n \n return;\n }\n \n \n // --------------------------------------------------------\n // Batteriezeile parsen\n // --------------------------------------------------------\n \n const parsed = parseBatteryLine(line);\n \n \n if (!parsed.valid) {\n \n block.invalidLines.push({\n line: line,\n reason: parsed.reason\n });\n \n \n diagnostic(\n \"warning\",\n \"INVALID_BATTERY_LINE\",\n \"Batteriezeile konnte nicht sicher verarbeitet werden.\",\n {\n reason: parsed.reason,\n rawLine: line,\n parsedData: parsed.battery || null\n }\n );\n \n return;\n }\n \n \n const batteryNumber = parsed.battery.battery;\n \n \n // --------------------------------------------------------\n // Doppelte Batterie-ID\n // --------------------------------------------------------\n \n if (block.batteries[batteryNumber] !== undefined) {\n \n block.invalidLines.push({\n line: line,\n reason: \"DUPLICATE_BATTERY\"\n });\n \n \n diagnostic(\n \"warning\",\n \"DUPLICATE_BATTERY\",\n \"Batterienummer wurde innerhalb desselben Datensatzes mehrfach empfangen.\",\n {\n battery: batteryNumber,\n rawLine: line\n }\n );\n \n return;\n }\n \n \n block.batteries[batteryNumber] = parsed.battery;\n}\n \n \n// ------------------------------------------------------------\n// Timeout des vorherigen Blocks prüfen\n// ------------------------------------------------------------\n \nif (\n block &&\n Date.now() - block.startedAt > BLOCK_TIMEOUT_MS\n) {\n \n discardBlock(\n \"BLOCK_TIMEOUT\",\n {\n timeoutMs: BLOCK_TIMEOUT_MS\n }\n );\n}\n \n \n// ------------------------------------------------------------\n// Fragmentierung der seriellen Schnittstelle behandeln\n// ------------------------------------------------------------\n \nfragment += text;\n \n \n// Schutz gegen kaputte / niemals abgeschlossene Eingabe\nif (fragment.length > MAX_FRAGMENT_LENGTH) {\n \n diagnostic(\n \"error\",\n \"SERIAL_FRAGMENT_OVERFLOW\",\n \"Der serielle Fragmentpuffer wurde zu groß und wurde zurückgesetzt.\",\n {\n fragmentLength: fragment.length,\n maxFragmentLength: MAX_FRAGMENT_LENGTH,\n fragmentPreview: fragment.substring(0, 500)\n }\n );\n \n fragment = \"\";\n \n discardBlock(\n \"SERIAL_FRAGMENT_OVERFLOW\"\n );\n}\n \n \n// ------------------------------------------------------------\n// Zeilenenden erkennen\n//\n// Unterstützt:\n// \\r\\n\n// \\n\n// \\r\n// ------------------------------------------------------------\n \nconst parts = fragment.split(/\\r\\n|\\n|\\r/);\n \n \n// Das letzte Element könnte eine noch unvollständige Zeile sein\nfragment = parts.pop() || \"\";\n \n \n// Alle sicher abgeschlossenen Zeilen verarbeiten\nfor (const line of parts) {\n processLine(line);\n}\n \n \n// ------------------------------------------------------------\n// Sonderfall:\n// Serial-Node liefert möglicherweise bereits jeweils EINE komplette\n// Zeile, entfernt aber selbst das Zeilenende.\n//\n// Wenn das aktuelle Fragment eindeutig wie eine vollständige\n// relevante Zeile aussieht, kann es sofort verarbeitet werden.\n// ------------------------------------------------------------\n \nconst trimmedFragment = fragment.trim();\n \nif (trimmedFragment !== \"\") {\n \n const completeBatteryLine =\n parseBatteryLine(trimmedFragment).valid;\n \n const obviousCompleteLine =\n isHeader(trimmedFragment) ||\n isEndMarker(trimmedFragment) ||\n isPromptOrNoise(trimmedFragment) ||\n completeBatteryLine;\n \n if (obviousCompleteLine) {\n \n processLine(trimmedFragment);\n \n fragment = \"\";\n }\n}\n \n \n// ------------------------------------------------------------\n// Zustand speichern\n// ------------------------------------------------------------\n \ncontext.set(\"serialFragment\", fragment);\ncontext.set(\"batteryBlock\", block);\n \n \n// ------------------------------------------------------------\n// Node-Status bei Fehlern/Warnungen\n// ------------------------------------------------------------\n \nif (output.some(m => m.messageType === \"error\")) {\n \n node.status({\n fill: \"red\",\n shape: \"dot\",\n text: \"RS232 Fehler\"\n });\n \n} else if (output.some(m => m.messageType === \"warning\")) {\n \n node.status({\n fill: \"yellow\",\n shape: \"dot\",\n text: \"RS232 Warnung\"\n });\n}\n \n \n// ------------------------------------------------------------\n// Alle Nachrichten nacheinander über denselben Ausgang senden\n// ------------------------------------------------------------\n \nreturn [output];",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1610,
"y": 420,
"wires": [
[
"be4f995ff9ae4db1",
"2948459dbbd003a5"
]
]
},
{
"id": "ddc4662ffce50bd4",
"type": "serial control",
"z": "924b846988fa37b8",
"name": "",
"serial": "d031dd424e7b1ff2",
"x": 2070,
"y": 240,
"wires": [
[]
]
},
{
"id": "676dc7e85bab0fb0",
"type": "function",
"z": "924b846988fa37b8",
"name": "RS232 schliessen",
"func": "// @ts-nocheck\nmsg.payload = {\n \"serialport\": \"/dev/serial/by-id/usb-FTDI_USB_Serial_Converter_FTFS8I5L-if00-port0\",\n \"serialbaud\": \"115200\",\n \"databits\": 8,\n \"parity\": \"none\",\n \"stopbits\": 1,\n \"enabled\": false\n};\n\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1630,
"y": 300,
"wires": [
[
"ddc4662ffce50bd4"
]
]
},
{
"id": "a5b021a6e4d25d63",
"type": "inject",
"z": "924b846988fa37b8",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 1380,
"y": 340,
"wires": [
[
"676dc7e85bab0fb0"
]
]
},
{
"id": "2b0172f542977f16",
"type": "function",
"z": "924b846988fa37b8",
"name": "RS232 öffnen",
"func": "// @ts-nocheck\nmsg.payload = {\n \"serialport\": \"/dev/serial/by-id/usb-FTDI_USB_Serial_Converter_FTFS8I5L-if00-port0\",\n \"serialbaud\": \"115200\",\n \"databits\": 8,\n \"parity\": \"none\",\n \"stopbits\": 1,\n \"enabled\": true\n};\n\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 480,
"y": 240,
"wires": [
[
"ddc4662ffce50bd4"
]
]
},
{
"id": "58e6f27bc04e9d56",
"type": "inject",
"z": "924b846988fa37b8",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 280,
"y": 240,
"wires": [
[
"2b0172f542977f16"
]
]
},
{
"id": "b2c11fc7a1bd0a9c",
"type": "switch",
"z": "924b846988fa37b8",
"name": "",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "cont",
"v": "completed",
"vt": "str"
},
{
"t": "else"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 1430,
"y": 420,
"wires": [
[
"76351a67c6019345"
],
[
"2948459dbbd003a5",
"676dc7e85bab0fb0"
]
]
},
{
"id": "26c696e68b092904",
"type": "iobout",
"z": "924b846988fa37b8",
"name": "iobroker",
"state": "",
"inputProperty": "payload",
"setMode": "value",
"autoCreate": true,
"stateName": "",
"stateRole": "",
"payloadType": "",
"stateReadonly": "",
"stateUnit": "",
"stateMin": "",
"stateMax": "",
"enableHistory": false,
"historyAdapter": "",
"historyTemplate": "",
"historySettings": "",
"server": "0e0cc42ad0c78fbc",
"x": 2100,
"y": 420,
"wires": []
},
{
"id": "d031dd424e7b1ff2",
"type": "serial-port",
"name": "Pytes",
"serialport": "/dev/serial/by-id/usb-FTDI_USB_Serial_Converter_FTFS8I5L-if00-port0",
"serialbaud": "115200",
"databits": 8,
"parity": "none",
"stopbits": 1,
"waitfor": "@",
"dtr": "none",
"rts": "none",
"cts": "none",
"dsr": "none",
"newline": "250",
"bin": "false",
"out": "time",
"addchar": "\\r\\n",
"responsetimeout": "250"
},
{
"id": "0e0cc42ad0c78fbc",
"type": "iob-config",
"name": "IoB Datenpunkte",
"iobhost": "192.168.130.10",
"iobport": "8091",
"usessl": false,
"isDefault": false
},
{
"id": "1b8fbb2c72c666d5",
"type": "global-config",
"env": [],
"modules": {
"node-red-node-serialport": "2.0.3",
"node-red-contrib-iobroker": "1.4.1"
}
}
]
[image: Bildschirmfoto vom 2026-09-19 17-46-53.png]
Dieser Flow läuft unterdessen in einem eigenen LXC-Container, weil sich mein Iobroker sehr seltsam verhalten hat, wenn die RS232 Schnittstelle der Pytes eine Störung hat. Da es unterdessen Nods gibt mit denen man Daten direkt in die Datenbunkte aus Node-Red einfügen kann, ist das Problem einfach umgangen.
IoB Node-Red Nods
Es ist absolut klar, die Profis hier werden es besser und schlanker programieren können, aber jeder beginnt am Anfang. Ein Tip an die Profis: schaut euch doch den vorhanden Adapter an, dann wären alle diese Bemühungen hier gar nicht notwenidig gewesen, oder korrigiert die Beschreibung im Adapter zu welchen genauen Typen der Pytes BMS er passen soll. Danke