Victron Multiplus 2 (Quattro/Multiplus) Wechselrichter via vebus Steuern (Vorlage)

Moin zusammen! :waving_hand:

Ich habe da mal einen kleinen Flow zur Steuerung des Victron MultiPlus II über VE.Bus gebaut und wollte ihn mit euch teilen. :blush:

Leider hatte ich den MultiPlus II nur zwei Tage zum Testen, aber in der Zeit hat alles soweit funktioniert.

Was wird benötigt?
Eigentlich nur ein Victron VE.Bus MK3-USB-Adapter. Link-USB-C

Die komplette Steuerung läuft aktuell nur im Backend. Mit den vorhandenen Beispielen lässt sich das Ganze aber relativ einfach ins Frontend oder in die App integrieren.

Wichtig bei der Einrichtung:

  • Im Connection Flow muss der entsprechende USB-Port deaktiviert werden, an dem der MK3-Adapter angeschlossen ist.

  • Im importierten Flow muss der USB-Port entsprechend angepasst werden.

  • Kontrolliert bitte auch die Baudrate. Manchmal wird diese beim Import nicht korrekt übernommen. Sie muss auf 2400 Baud stehen.

  • Beim Übernehmen (Deploy) des Flows wird der Adapter automatisch entsprechend konfiguriert.

Was lässt sich damit steuern?

  • PowerAssist: Kann ein- und ausgeschaltet werden. Dafür muss vorher einmal die Geräteerkennung durchlaufen. Anschließend kann die Funktion ganz normal gesteuert werden.

  • Betriebsmodi: Wechselrichter ein/aus oder nur Laden (Charger Only).

  • Landstrombegrenzung: Lässt sich ebenfalls einstellen. Hier gibt es allerdings je nach Wechselrichter und PowerAssist-Einstellung unterschiedliche Mindestwerte.

Das Ganze ist natürlich noch etwas experimentell, da ich aktuell leider keinen MultiPlus II mehr hier habe, um mögliche Fehler gegenzuprüfen.

Ich hoffe, der Flow funktioniert bei euch genauso gut wie bei meinen Tests. Falls ihr ihn ausprobiert, würde ich mich natürlich über Feedback freuen! Vielleicht finden wir gemeinsam noch ein paar Dinge, die man verbessern kann. :grin:

[
    {
        "id":  "vebus_multiplus2_tab",
        "type":  "tab",
        "label":  "VE.Bus MultiPlus-II",
        "disabled":  false,
        "info":  "MK2/MK3 lesen und kontrolliert steuern",
        "env":  [

                ]
    },
    {
        "id":  "vebus_note_intro",
        "type":  "comment",
        "z":  "vebus_multiplus2_tab",
        "name":  "MK2/MK3 an /dev/ttyPKW2 - 2400 8N1",
        "info":  "Der Flow setzt beim Start explizit VE.Bus-Adresse 0 (A 01 00), lernt die RAM-Skalierung und pollt Messwerte/Status. Steuerbefehle benoetigen confirm:true.",
        "x":  310,
        "y":  40,
        "wires":  [

                  ]
    },
    {
        "id":  "vebus_tick",
        "type":  "inject",
        "z":  "vebus_multiplus2_tab",
        "name":  "Poll 250 ms",
        "props":  [
                      {
                          "p":  "payload"
                      },
                      {
                          "p":  "topic",
                          "vt":  "str"
                      }
                  ],
        "repeat":  "0.25",
        "crontab":  "",
        "once":  true,
        "onceDelay":  "2",
        "topic":  "vebus/poll",
        "payload":  "",
        "payloadType":  "date",
        "x":  120,
        "y":  140,
        "wires":  [
                      [
                          "vebus_poller"
                      ]
                  ]
    },
    {
        "id":  "vebus_reinit",
        "type":  "inject",
        "z":  "vebus_multiplus2_tab",
        "name":  "Adresse 0 + Initialisierung neu",
        "props":  [
                      {
                          "p":  "payload"
                      },
                      {
                          "p":  "topic",
                          "vt":  "str"
                      }
                  ],
        "repeat":  "",
        "crontab":  "",
        "once":  true,
        "onceDelay":  "0.5",
        "topic":  "vebus/reinitialize",
        "payload":  "true",
        "payloadType":  "bool",
        "x":  160,
        "y":  100,
        "wires":  [
                      [
                          "vebus_poller"
                      ]
                  ]
    },
    {
        "id":  "vebus_discover",
        "type":  "inject",
        "z":  "vebus_multiplus2_tab",
        "name":  "Geraete suchen",
        "props":  [
                      {
                          "p":  "payload"
                      },
                      {
                          "p":  "topic",
                          "vt":  "str"
                      }
                  ],
        "repeat":  "",
        "crontab":  "",
        "once":  false,
        "onceDelay":  0.1,
        "topic":  "vebus/discover",
        "payload":  "true",
        "payloadType":  "bool",
        "x":  130,
        "y":  180,
        "wires":  [
                      [
                          "vebus_poller"
                      ]
                  ]
    },
    {
        "id":  "vebus_poller",
        "type":  "function",
        "z":  "vebus_multiplus2_tab",
        "name":  "VE.Bus Poller + Initialisierung",
        "func":  "// Node-RED Function-Node: erzeugt genau ein MK2/MK3-Telegramm pro Aufruf.\n\nfunction mk2Frame(command, payload) {\n    const body = [0xff, command.charCodeAt(0)].concat(payload || []);\n    const bytes = [body.length].concat(body);\n    let sum = 0;\n    for (const value of bytes) sum = (sum + value) \u0026 0xff;\n    bytes.push((-sum) \u0026 0xff);\n    return Buffer.from(bytes);\n}\n\nfunction outgoing(kind, command, payload, extra) {\n    const pending = Object.assign({ kind, sentAt: Date.now() }, extra || {});\n    flow.set(\"vebusPending\", pending);\n    const buffer = mk2Frame(command, payload);\n    node.status({ fill: \"blue\", shape: \"dot\", text: `${kind}: ${Array.from(buffer).map(x =\u003e x.toString(16).padStart(2, \"0\")).join(\" \")}` });\n    return { topic: `vebus/request/${kind}`, payload: buffer };\n}\n\nfunction outgoingFrames(kind, frames, extra) {\n    const pending = Object.assign({ kind, sentAt: Date.now() }, extra || {});\n    flow.set(\"vebusPending\", pending);\n    const buffer = Buffer.concat(frames);\n    node.status({ fill: \"yellow\", shape: \"dot\", text: `${kind}: ${buffer.length} Bytes` });\n    return { topic: `vebus/request/${kind}`, payload: buffer };\n}\n\nfunction failPowerAssistAction(action, reason) {\n    action.active = false;\n    action.status = \"failed\";\n    action.error = reason;\n    action.finished_at = new Date().toISOString();\n    flow.set(\"vebusPowerAssistAction\", action);\n    const state = flow.get(\"vebusPowerAssist\") || {};\n    state.last_action = action;\n    flow.set(\"vebusPowerAssist\", state);\n    node.warn(`PowerAssist: ${reason}`);\n}\n\nfunction startDiscovery() {\n    const existing = flow.get(\"vebusDiscovery\");\n    if (existing \u0026\u0026 existing.active) return existing;\n    const discovery = {\n        active: true,\n        stage: \"setAddress\",\n        address: 0,\n        selected_address: null,\n        consecutive_misses: 0,\n        found_addresses: [],\n        devices: {},\n        started_at: new Date().toISOString(),\n        finished_at: null\n    };\n    flow.set(\"vebusDiscovery\", discovery);\n    return discovery;\n}\n\nfunction advanceDiscovery(discovery, found) {\n    discovery.consecutive_misses = found ? 0 : discovery.consecutive_misses + 1;\n    discovery.address += 1;\n    discovery.stage = discovery.consecutive_misses \u003e= 3 || discovery.address \u003e 0x1f\n        ? \"restoreAddress0\"\n        : \"setAddress\";\n    flow.set(\"vebusDiscovery\", discovery);\n}\n\nif (msg.topic === \"vebus/discover\") {\n    startDiscovery();\n    node.warn(\"VE.Bus-Geraetesuche gestartet (Adressen 0 bis 31)\");\n}\n\nif (msg.topic === \"vebus/reinitialize\") {\n    flow.set(\"vebusAddressOk\", false);\n    flow.set(\"vebusRamInfo\", {});\n    flow.set(\"vebusRamInfoAttempts\", {});\n    flow.set(\"vebusSettingInfo\", {});\n    flow.set(\"vebusSettingInfoAttempts\", {});\n    flow.set(\"vebusSettingInfoPartial\", {});\n    flow.set(\"vebusChargerSettings\", { read_only: true, values: {}, details: {} });\n    flow.set(\"vebusChargerReadAttempts\", {});\n    flow.set(\"vebusPending\", null);\n    flow.set(\"vebusDiscovery\", null);\n    context.set(\"pollStep\", 0);\n    context.set(\"fastRamStep\", 0);\n    context.set(\"scanRamStep\", 0);\n    context.set(\"chargerSettingStep\", 0);\n    node.warn(\"VE.Bus-Initialisierung wurde neu gestartet\");\n}\n\nconst now = Date.now();\nlet pending = flow.get(\"vebusPending\");\nif (pending \u0026\u0026 now - pending.sentAt \u003c 900) return null;\n\nlet discovery = flow.get(\"vebusDiscovery\");\nlet powerAssistAction = flow.get(\"vebusPowerAssistAction\");\nif (pending \u0026\u0026 powerAssistAction \u0026\u0026 powerAssistAction.active \u0026\u0026 pending.kind === \"powerAssistWrite\") {\n    // Schreiben kann trotz fehlendem ACK erfolgt sein: immer per Readback pruefen.\n    powerAssistAction.stage = \"verify\";\n    powerAssistAction.warning = \"write acknowledgement timeout\";\n    flow.set(\"vebusPowerAssistAction\", powerAssistAction);\n} else if (pending \u0026\u0026 powerAssistAction \u0026\u0026 powerAssistAction.active \u0026\u0026\n           [\"powerAssistInfoAction\", \"powerAssistReadBefore\", \"powerAssistVerify\"].includes(pending.kind)) {\n    failPowerAssistAction(powerAssistAction, `Timeout in ${pending.kind}`);\n} else if (pending \u0026\u0026 discovery \u0026\u0026 discovery.active \u0026\u0026 pending.kind === \"discoveryProbeLow\") {\n    advanceDiscovery(discovery, false);\n    discovery = flow.get(\"vebusDiscovery\");\n} else if (pending \u0026\u0026 discovery \u0026\u0026 discovery.active \u0026\u0026 pending.kind === \"discoveryProbeHigh\") {\n    // Das Geraet wurde bereits durch Part 0 bestaetigt; Part 1 ist optional.\n    advanceDiscovery(discovery, true);\n    discovery = flow.get(\"vebusDiscovery\");\n} else if (pending \u0026\u0026 pending.kind === \"ramInfo\") {\n    const attempts = flow.get(\"vebusRamInfoAttempts\") || {};\n    if ((attempts[pending.id] || 0) \u003e= 3) {\n        const infos = flow.get(\"vebusRamInfo\") || {};\n        infos[pending.id] = { supported: false, reason: \"timeout after 3 attempts\" };\n        flow.set(\"vebusRamInfo\", infos);\n    }\n} else if (pending \u0026\u0026 pending.kind === \"settingInfo\") {\n    const attempts = flow.get(\"vebusSettingInfoAttempts\") || {};\n    if ((attempts[pending.id] || 0) \u003e= 3) {\n        const infos = flow.get(\"vebusSettingInfo\") || {};\n        infos[pending.id] = { supported: false, reason: \"timeout after 3 attempts\" };\n        flow.set(\"vebusSettingInfo\", infos);\n    }\n} else if (pending \u0026\u0026 pending.kind === \"chargerSettingRead\") {\n    const attempts = flow.get(\"vebusChargerReadAttempts\") || {};\n    if ((attempts[pending.id] || 0) \u003e= 3) {\n        const charger = flow.get(\"vebusChargerSettings\") || { read_only: true, values: {}, details: {} };\n        charger.details = charger.details || {};\n        charger.details[`setting_${pending.id}`] = {\n            id: pending.id,\n            supported: false,\n            reason: \"read timeout after 3 attempts\"\n        };\n        flow.set(\"vebusChargerSettings\", charger);\n    }\n}\nflow.set(\"vebusPending\", null);\n\nif (discovery \u0026\u0026 discovery.active) {\n    if (discovery.stage === \"setAddress\") {\n        return outgoing(\"discoverySetAddress\", \"A\", [0x01, discovery.address], { address: discovery.address });\n    }\n    if (discovery.stage === \"probeLow\") {\n        return outgoing(\"discoveryProbeLow\", \"W\", [0x05, 0x00, 0x00], { address: discovery.address });\n    }\n    if (discovery.stage === \"probeHigh\") {\n        return outgoing(\"discoveryProbeHigh\", \"W\", [0x06, 0x00, 0x00], { address: discovery.address });\n    }\n    if (discovery.stage === \"restoreAddress0\") {\n        return outgoing(\"discoveryRestore\", \"A\", [0x01, 0x00], { address: 0 });\n    }\n    return null;\n}\n\nif (!flow.get(\"vebusAddressOk\")) {\n    return outgoing(\"address\", \"A\", [0x01, 0x00]);\n}\n\npowerAssistAction = flow.get(\"vebusPowerAssistAction\");\nif (powerAssistAction \u0026\u0026 powerAssistAction.active) {\n    if (powerAssistAction.stage === \"getInfo\") {\n        return outgoing(\"powerAssistInfoAction\", \"W\", [0x35, 0x00, 0x00]);\n    }\n    if (powerAssistAction.stage === \"readFlags\") {\n        return outgoing(\"powerAssistReadBefore\", \"W\", [0x31, 0x00, 0x00]);\n    }\n    if (powerAssistAction.stage === \"write\") {\n        const value = powerAssistAction.desired_flags0;\n        if (!Number.isInteger(value) || value \u003c 0 || value \u003e 0xffff) {\n            failPowerAssistAction(powerAssistAction, \"Ungueltiger Flags0-Zielwert\");\n            return null;\n        }\n        const selectSetting = mk2Frame(\"W\", [0x33, 0x00, 0x00]);\n        const writeData = mk2Frame(\"W\", [0x34, value \u0026 0xff, (value \u003e\u003e 8) \u0026 0xff]);\n        powerAssistAction.stage = \"waitingWriteAck\";\n        powerAssistAction.status = \"writing\";\n        flow.set(\"vebusPowerAssistAction\", powerAssistAction);\n        return outgoingFrames(\"powerAssistWrite\", [selectSetting, writeData]);\n    }\n    if (powerAssistAction.stage === \"verify\") {\n        return outgoing(\"powerAssistVerify\", \"W\", [0x31, 0x00, 0x00]);\n    }\n    return null;\n}\n\nconst infos = flow.get(\"vebusRamInfo\") || {};\nlet missingId = null;\nfor (let id = 0; id \u003c= 19; id += 1) {\n    if (!Object.prototype.hasOwnProperty.call(infos, id)) {\n        missingId = id;\n        break;\n    }\n}\n\nif (missingId !== null) {\n    const attempts = flow.get(\"vebusRamInfoAttempts\") || {};\n    attempts[missingId] = (attempts[missingId] || 0) + 1;\n    flow.set(\"vebusRamInfoAttempts\", attempts);\n    return outgoing(\"ramInfo\", \"W\", [0x36, missingId \u0026 0xff, (missingId \u003e\u003e 8) \u0026 0xff], { id: missingId });\n}\n\n// Ausschliesslich lesend: Ladeparameter und ihre geraetespezifischen Skalierungen/Grenzen.\nconst chargerSettingInfoIds = [0, 2, 3, 4, 7, 8, 9, 10];\nconst settingInfos = flow.get(\"vebusSettingInfo\") || {};\nlet missingSettingId = null;\nfor (const id of chargerSettingInfoIds) {\n    if (!Object.prototype.hasOwnProperty.call(settingInfos, id)) {\n        missingSettingId = id;\n        break;\n    }\n}\n\nif (missingSettingId !== null) {\n    const attempts = flow.get(\"vebusSettingInfoAttempts\") || {};\n    attempts[missingSettingId] = (attempts[missingSettingId] || 0) + 1;\n    flow.set(\"vebusSettingInfoAttempts\", attempts);\n    return outgoing(\"settingInfo\", \"W\", [0x35, missingSettingId \u0026 0xff, (missingSettingId \u003e\u003e 8) \u0026 0xff], { id: missingSettingId });\n}\n\n// Nach der Skalierungsabfrage jeden Ladeparameter einmal zuegig einlesen.\nconst chargerBootstrap = flow.get(\"vebusChargerSettings\") || {};\nconst chargerDetails = chargerBootstrap.details || {};\nfor (const id of chargerSettingInfoIds) {\n    const info = settingInfos[id];\n    const alreadyRead = Object.keys(chargerDetails).some(name =\u003e chargerDetails[name] \u0026\u0026 chargerDetails[name].id === id);\n    if (info \u0026\u0026 info.supported \u0026\u0026 !alreadyRead) {\n        const attempts = flow.get(\"vebusChargerReadAttempts\") || {};\n        attempts[id] = (attempts[id] || 0) + 1;\n        flow.set(\"vebusChargerReadAttempts\", attempts);\n        return outgoing(\"chargerSettingRead\", \"W\", [0x31, id \u0026 0xff, (id \u003e\u003e 8) \u0026 0xff], { id });\n    }\n}\n\nconst fastIds = [4, 5, 13, 14, 15, 16];\nconst chargerSettingReadIds = [2, 3, 4, 7, 8, 9, 10];\nlet pollStep = context.get(\"pollStep\") || 0;\ncontext.set(\"pollStep\", (pollStep + 1) % 12);\n\nif (pollStep === 0) return outgoing(\"dcInfo\", \"F\", [0x00]);\nif (pollStep === 1) return outgoing(\"acInfo\", \"F\", [0x01]);\nif (pollStep === 2) return outgoing(\"masterInfo\", \"F\", [0x05]);\nif (pollStep === 3) return outgoing(\"ledInfo\", \"L\", []);\nif (pollStep === 4) return outgoing(\"state\", \"W\", [0x0e, 0x00, 0x00]);\nif (pollStep === 5) {\n    let step = context.get(\"fastRamStep\") || 0;\n    const id = fastIds[step % fastIds.length];\n    context.set(\"fastRamStep\", step + 1);\n    return outgoing(\"ramRead\", \"W\", [0x30, id \u0026 0xff, (id \u003e\u003e 8) \u0026 0xff], { id });\n}\nif (pollStep === 6) {\n    let step = context.get(\"scanRamStep\") || 0;\n    const id = step % 20;\n    context.set(\"scanRamStep\", step + 1);\n    return outgoing(\"ramRead\", \"W\", [0x30, id \u0026 0xff, (id \u003e\u003e 8) \u0026 0xff], { id });\n}\nif (pollStep === 7) return outgoing(\"powerAssistRead\", \"W\", [0x31, 0x00, 0x00]);\nif (pollStep === 8) {\n    let step = context.get(\"chargerSettingStep\") || 0;\n    const id = chargerSettingReadIds[step % chargerSettingReadIds.length];\n    context.set(\"chargerSettingStep\", step + 1);\n    return outgoing(\"chargerSettingRead\", \"W\", [0x31, id \u0026 0xff, (id \u003e\u003e 8) \u0026 0xff], { id });\n}\nif (pollStep === 9) return outgoing(\"mk2Version\", \"V\", []);\nif (pollStep === 10) return outgoing(\"targetVersion0\", \"W\", [0x05, 0x00, 0x00]);\nreturn outgoing(\"targetVersion1\", \"W\", [0x06, 0x00, 0x00]);\n",
        "outputs":  1,
        "timeout":  0,
        "noerr":  0,
        "initialize":  "",
        "finalize":  "",
        "libs":  [

                 ],
        "x":  440,
        "y":  140,
        "wires":  [
                      [
                          "vebus_serial_out",
                          "vebus_debug_tx"
                      ]
                  ]
    },
    {
        "id":  "vebus_serial_out",
        "type":  "serial out",
        "z":  "vebus_multiplus2_tab",
        "name":  "MK3 senden",
        "serial":  "vebus_serial_port",
        "x":  710,
        "y":  120,
        "wires":  [

                  ]
    },
    {
        "id":  "vebus_serial_in",
        "type":  "serial in",
        "z":  "vebus_multiplus2_tab",
        "name":  "MK3 empfangen",
        "serial":  "vebus_serial_port",
        "x":  130,
        "y":  260,
        "wires":  [
                      [
                          "vebus_parser"
                      ]
                  ]
    },
    {
        "id":  "vebus_debug_tx",
        "type":  "debug",
        "z":  "vebus_multiplus2_tab",
        "name":  "Gesendete Frames (bei Bedarf)",
        "active":  false,
        "tosidebar":  true,
        "console":  false,
        "tostatus":  false,
        "complete":  "true",
        "targetType":  "full",
        "statusVal":  "",
        "statusType":  "auto",
        "x":  760,
        "y":  160,
        "wires":  [

                  ]
    },
    {
        "id":  "vebus_parser",
        "type":  "function",
        "z":  "vebus_multiplus2_tab",
        "name":  "VE.Bus Stream-Parser + Decoder",
        "func":  "// Node-RED Function-Node: 2 Ausgaenge\n// 1: zusammengefasster Zustand, 2: jedes gueltige Rohtelegramm\n\nconst RAM_NAMES = [\n    \"ac_input_voltage_v\",\n    \"ac_input_current_a\",\n    \"ac_output_voltage_v\",\n    \"ac_output_current_a\",\n    \"battery_voltage_v\",\n    \"battery_current_a\",\n    \"battery_ripple_voltage_v\",\n    \"inverter_period_raw\",\n    \"mains_period_raw\",\n    \"ac_load_current_a\",\n    \"virtual_switch\",\n    \"ignore_ac_input\",\n    \"multifunction_relay\",\n    \"state_of_charge_pct\",\n    \"inverter_power_filtered_w\",\n    \"inverter_power_filtered_2_w\",\n    \"output_power_filtered_w\",\n    \"inverter_power_unfiltered_w\",\n    \"inverter_power_unfiltered_2_w\",\n    \"output_power_unfiltered_w\"\n];\n\nconst STATE_NAMES = [\n    \"Down\", \"Startup\", \"Off\", \"Slave\", \"InvertFull\",\n    \"InvertHalf\", \"InvertAES\", \"PowerAssist\", \"Bypass\", \"Charge\"\n];\n\nconst CHARGE_NAMES = [\n    \"Initializing\", \"Bulk\", \"Absorption\", \"Float\", \"Storage\",\n    \"Repeated absorption\", \"Forced absorption\", \"Equalise\", \"Bulk stopped\"\n];\n\nconst CHARGER_SETTING_NAMES = {\n    0: \"flags0\",\n    2: \"absorption_voltage_v\",\n    3: \"float_voltage_v\",\n    4: \"max_charge_current_a\",\n    7: \"repeated_absorption_time_min\",\n    8: \"repeated_absorption_interval_min\",\n    9: \"max_absorption_time_min\",\n    10: \"charge_characteristic_code\"\n};\n\nconst LED_NAMES = [\n    \"mains\", \"absorption\", \"bulk\", \"float\",\n    \"inverter\", \"overload\", \"low_battery\", \"temperature\"\n];\n\n// VE.Bus-Kompatibilitaets-/Produkt-IDs. Unbekannte IDs bleiben als Nummer sichtbar.\nconst PRODUCT_NAMES = {\n    2600: \"MultiPlus 12/3000/120-50\",\n    2603: \"Phoenix Multi Compact 12/800/35-16\",\n    2605: \"MultiPlus-II 12/3000/120-32\",\n    2606: \"MultiPlus Compact 12/1600/70-16\",\n    2607: \"MultiPlus Compact 12/1200/50-16\",\n    2608: \"MultiPlus Compact 12/2000/80-30\",\n    2609: \"MultiPlus 12/3000/120-16\",\n    2610: \"MultiPlus 24/3000/70-50 / EasySolar\",\n    2611: \"MultiPlus-II 24/3000/70-32\",\n    2612: \"MultiPlus 24/3000/70-16\",\n    2613: \"Phoenix Multi Compact 24/800/16-16\",\n    2614: \"MultiPlus 24/5000/120-100\",\n    2616: \"MultiPlus Compact 24/1600/40-16\",\n    2617: \"MultiPlus Compact 24/1200/25-16\",\n    2618: \"MultiPlus Compact 24/2000/50-30\",\n    2620: \"MultiPlus 48/3000/35-50 / EasySolar\",\n    2622: \"MultiPlus 48/3000/35-16\",\n    2623: \"MultiPlus-II 48/5000/70-50\",\n    2624: \"MultiPlus 48/5000/70-100 / EasySolar\",\n    2625: \"MultiPlus-II 48/3000/35-32\",\n    2628: \"MultiPlus-II 48/3000/35-32 (externer Sensor bis 50 A)\",\n    2629: \"MultiPlus-II 48/3000/35-32 / EasySolar-II\",\n    2667: \"MultiPlus 24/1200/25-16\",\n    2668: \"MultiPlus 24/1600/40-16\",\n    2670: \"Multi 48/500/6-16\",\n    2671: \"MultiPlus 48/800/8-16\",\n    2672: \"MultiPlus 48/1200/13-16\",\n    2673: \"MultiPlus 48/1600/20-16\",\n    2680: \"MultiGrid 12/3000/120-50\",\n    2685: \"MultiGrid 24/3000/70-50\",\n    2690: \"MultiGrid 48/3000/35-50\",\n    2702: \"MultiPlus 12/3000/120-50 120 V\",\n    2708: \"MultiPlus Compact 12/2000/80-50 120 V\",\n    2712: \"MultiPlus 24/3000/70-50 120 V\",\n    2718: \"MultiPlus Compact 24/2000/50-50 120 V\",\n    2733: \"Quattro 12/5000/120-2x100 120 V\",\n    2748: \"Quattro 24/5000/120-2x100 120 V\",\n    2751: \"Quattro 48/3000/35-2x50 120 V\",\n    2753: \"Quattro 48/5000/70-2x100 120 V\",\n    2754: \"Quattro 48/10000/140-2x100 120 V\"\n};\n\nfunction decorateDevice(device) {\n    if (!device || device.software_raw === undefined) return device;\n    const softwareNumber = String(device.software_raw).padStart(7, \"0\");\n    const productId = Number(softwareNumber.slice(0, 4));\n    const firmware = Number(softwareNumber.slice(-3));\n    const productName = PRODUCT_NAMES[productId] || `Unbekanntes VE.Bus-Geraet (Produkt-ID ${productId})`;\n    device.product_id = productId;\n    device.product_name = productName;\n    device.firmware_version = firmware;\n    device.firmware_display = `v${firmware}`;\n    device.description = `${productName} | Produkt-ID ${productId} | Firmware v${firmware}`;\n    return device;\n}\n\nfunction updateDiscoverySummary(discovery) {\n    if (!discovery) return discovery;\n    const devices = discovery.devices || {};\n    const addresses = Object.keys(devices).map(Number).sort((a, b) =\u003e a - b);\n    discovery.found_addresses = addresses;\n    discovery.device_count = addresses.length;\n    discovery.summary = addresses.map(address =\u003e {\n        const device = devices[String(address)] || {};\n        return `Adresse ${address}: ${device.description || \"VE.Bus-Geraet erkannt\"}`;\n    });\n    return discovery;\n}\n\nfunction u16(a, i) {\n    return (a[i] | (a[i + 1] \u003c\u003c 8)) \u003e\u003e\u003e 0;\n}\n\nfunction s16(a, i) {\n    const value = u16(a, i);\n    return value \u0026 0x8000 ? value - 0x10000 : value;\n}\n\nfunction u24(a, i) {\n    return (a[i] | (a[i + 1] \u003c\u003c 8) | (a[i + 2] \u003c\u003c 16)) \u003e\u003e\u003e 0;\n}\n\nfunction round(value, digits) {\n    if (!Number.isFinite(value)) return null;\n    const factor = Math.pow(10, digits || 3);\n    return Math.round(value * factor) / factor;\n}\n\nfunction scaleRam(id, raw, forceUnsigned) {\n    const infos = flow.get(\"vebusRamInfo\") || {};\n    const info = infos[id];\n    if (!info || !info.supported || !Number.isFinite(info.scale)) return null;\n\n    if (info.bit !== undefined) {\n        return Boolean((raw \u003e\u003e\u003e info.bit) \u0026 1);\n    }\n\n    let x = raw;\n    // Die AC-Stromwoerter der IDs 1, 3 und 9 koennen negative Werte liefern.\n    // Insbesondere wird ein kleiner negativer Nullpunkt sonst zu 655,xx A.\n    const signedWord = info.signed || id === 1 || id === 3 || id === 9;\n    if (!forceUnsigned \u0026\u0026 signedWord \u0026\u0026 raw \u003e 0x7fff) x = raw - 0x10000;\n    return info.scale * (x + info.offset);\n}\n\nfunction normalizeAcCurrent(value) {\n    if (!Number.isFinite(value)) return value;\n    return Math.abs(value) \u003c 0.05 ? 0 : value;\n}\n\nfunction scaleSetting(id, raw) {\n    const infos = flow.get(\"vebusSettingInfo\") || {};\n    const info = infos[id];\n    if (!info || !info.supported || !Number.isFinite(info.scale)) return null;\n    return info.scale * (raw + info.offset);\n}\n\nfunction settingDisplayValue(info, raw) {\n    if (!info || !info.supported || !Number.isFinite(info.scale) || !Number.isFinite(raw)) return null;\n    return round(info.scale * (raw + info.offset), 4);\n}\n\nfunction updateChargerFlags(charger, flags0) {\n    charger.flags0_raw = flags0;\n    charger.charger_enabled = !(flags0 \u0026 0x0040);\n    charger.bulk_protection_enabled = !(flags0 \u0026 0x0010);\n    charger.reduced_float_enabled = Boolean(flags0 \u0026 0x0800);\n    charger.weak_ac_enabled = Boolean(flags0 \u0026 0x4000);\n}\n\nfunction storeChargerSetting(id, raw, data) {\n    const charger = Object.assign({ read_only: true, values: {}, details: {} }, flow.get(\"vebusChargerSettings\") || {});\n    charger.values = charger.values || {};\n    charger.details = charger.details || {};\n    const infos = flow.get(\"vebusSettingInfo\") || {};\n    const info = infos[id];\n    const name = CHARGER_SETTING_NAMES[id] || `setting_${id}`;\n    const value = scaleSetting(id, raw);\n\n    charger.values[name] = round(value, 4);\n    charger.details[name] = {\n        id,\n        raw,\n        value: round(value, 4),\n        default: settingDisplayValue(info, info \u0026\u0026 info.default_raw),\n        minimum: settingDisplayValue(info, info \u0026\u0026 info.minimum_raw),\n        maximum: settingDisplayValue(info, info \u0026\u0026 info.maximum_raw),\n        supported: Boolean(info \u0026\u0026 info.supported)\n    };\n    if (id === 0) updateChargerFlags(charger, raw);\n    charger.last_read_at = new Date().toISOString();\n    flow.set(\"vebusChargerSettings\", charger);\n    data.charger_settings = charger;\n    return charger;\n}\n\nfunction frameHex(frame) {\n    return Array.from(frame, value =\u003e value.toString(16).padStart(2, \"0\")).join(\" \");\n}\n\n// Fuer den normalen Debug-Ausgang technische Rohwerte ausblenden.\n// Im internen Flow-Zustand und am zweiten Ausgang bleiben sie zur Diagnose erhalten.\nfunction readableState(value) {\n    const clone = JSON.parse(JSON.stringify(value));\n\n    function clean(item) {\n        if (!item || typeof item !== \"object\") return;\n        if (Array.isArray(item)) {\n            item.forEach(clean);\n            return;\n        }\n\n        for (const key of Object.keys(item)) {\n            const lower = key.toLowerCase();\n            if (lower === \"raw\" || lower.endsWith(\"_raw\") || lower.endsWith(\"_raw_hex\")) {\n                delete item[key];\n            } else {\n                clean(item[key]);\n            }\n        }\n    }\n\n    clean(clone);\n    return clone;\n}\n\nfunction clearPending(kind) {\n    const pending = flow.get(\"vebusPending\");\n    if (!pending || !kind || pending.kind === kind) flow.set(\"vebusPending\", null);\n}\n\nfunction setMainState(data, stateCode, subState) {\n    data.state = data.state || {};\n    data.state.code = stateCode;\n    data.state.name = STATE_NAMES[stateCode] || `Unknown (${stateCode})`;\n    if (stateCode === 9 \u0026\u0026 subState !== undefined) {\n        data.state.charge_code = subState;\n        data.state.charge_state = CHARGE_NAMES[subState] || `Unknown (${subState})`;\n    } else {\n        delete data.state.charge_code;\n        delete data.state.charge_state;\n    }\n\n    if (stateCode === 2) data.state.inferred_panel_mode = \"off\";\n    else if (stateCode \u003e= 4 \u0026\u0026 stateCode \u003c= 6) data.state.inferred_panel_mode = \"inverter_only\";\n    else if (stateCode \u003e= 7 \u0026\u0026 stateCode \u003c= 9) data.state.inferred_panel_mode = \"on\";\n}\n\nfunction decodeWinmon(payload, data, decoded) {\n    const pending = flow.get(\"vebusPending\");\n    decoded.winmon = [];\n\n    function parseSettingInfoResponse(bytes) {\n        const result = {};\n        if (!bytes.length) return result;\n        if (bytes[0] === 0x86) {\n            result.unsupported = true;\n            return result;\n        }\n\n        // Mode 2: ein langer Datensatz ohne 0x8A..0x8D-Markierungen.\n        // Mode 1: mehrere einzelne 3-Byte-Datensaetze. Ein W-Reply kann\n        // hoechstens drei davon enthalten, weitere folgen im naechsten Frame.\n        const longMode = bytes[0] === 0x89 \u0026\u0026 bytes.length \u003e 9;\n        if (longMode) {\n            result.sc = s16(bytes, 1);\n            result.offset = s16(bytes, 3);\n            result.default_raw = u16(bytes, 5);\n            result.minimum_raw = u16(bytes, 7);\n            result.maximum_raw = u16(bytes, 9);\n            if (bytes.length \u003e 11) result.access_level = bytes[11];\n            result.mode = 2;\n            return result;\n        }\n\n        for (let index = 0; index + 2 \u003c bytes.length; index += 3) {\n            const response = bytes[index];\n            if (response === 0x89) result.sc = s16(bytes, index + 1);\n            if (response === 0x8a) result.offset = s16(bytes, index + 1);\n            if (response === 0x8b) result.default_raw = u16(bytes, index + 1);\n            if (response === 0x8c) result.minimum_raw = u16(bytes, index + 1);\n            if (response === 0x8d) result.maximum_raw = u16(bytes, index + 1);\n        }\n        result.mode = 1;\n        return result;\n    }\n\n    function storePowerAssistState(changes) {\n        const state = Object.assign({}, flow.get(\"vebusPowerAssist\") || {}, changes || {});\n        flow.set(\"vebusPowerAssist\", state);\n        data.powerassist = state;\n        return state;\n    }\n\n    function finishPowerAssistAction(action, status, error) {\n        action.active = false;\n        action.status = status;\n        action.error = error || null;\n        action.finished_at = new Date().toISOString();\n        flow.set(\"vebusPowerAssistAction\", action);\n        const state = storePowerAssistState({});\n        state.last_action = JSON.parse(JSON.stringify(action));\n        flow.set(\"vebusPowerAssist\", state);\n        data.powerassist = state;\n    }\n\n    for (let index = 0; index + 2 \u003c payload.length; index += 3) {\n        decoded.winmon.push({\n            response: payload[index],\n            info0: payload[index + 1],\n            info1: payload[index + 2]\n        });\n    }\n\n    if (pending \u0026\u0026 pending.kind === \"settingInfo\") {\n        const infos = flow.get(\"vebusSettingInfo\") || {};\n        const partials = flow.get(\"vebusSettingInfoPartial\") || {};\n        const key = String(pending.id);\n        const partial = partials[key] || {};\n        const received = parseSettingInfoResponse(payload);\n        Object.assign(partial, received);\n\n        if (received.unsupported) {\n            infos[pending.id] = { supported: false, reason: \"not supported\" };\n            delete partials[key];\n            flow.set(\"vebusSettingInfo\", infos);\n            flow.set(\"vebusSettingInfoPartial\", partials);\n            clearPending(\"settingInfo\");\n        } else if (partial.sc === 0) {\n            infos[pending.id] = { supported: false, reason: \"not supported\" };\n            delete partials[key];\n            flow.set(\"vebusSettingInfo\", infos);\n            flow.set(\"vebusSettingInfoPartial\", partials);\n            clearPending(\"settingInfo\");\n        } else if (partial.sc !== undefined \u0026\u0026 partial.offset !== undefined \u0026\u0026\n                   partial.default_raw !== undefined \u0026\u0026 partial.minimum_raw !== undefined \u0026\u0026\n                   partial.maximum_raw !== undefined) {\n            const scale = partial.sc \u003e 0 ? partial.sc : 1 / (-partial.sc);\n            infos[pending.id] = {\n                supported: true,\n                sc: partial.sc,\n                scale,\n                offset: partial.offset,\n                default_raw: partial.default_raw,\n                minimum_raw: partial.minimum_raw,\n                maximum_raw: partial.maximum_raw,\n                access_level: partial.access_level,\n                winmon_mode: partial.mode,\n                default: round(scale * (partial.default_raw + partial.offset), 4),\n                minimum: round(scale * (partial.minimum_raw + partial.offset), 4),\n                maximum: round(scale * (partial.maximum_raw + partial.offset), 4)\n            };\n            delete partials[key];\n            flow.set(\"vebusSettingInfo\", infos);\n            flow.set(\"vebusSettingInfoPartial\", partials);\n            clearPending(\"settingInfo\");\n        } else {\n            partials[key] = partial;\n            flow.set(\"vebusSettingInfoPartial\", partials);\n        }\n\n        // Setting-Info-Antworten bestehen aus eigenen 0x89..0x8D-Datensaetzen.\n        // Sie duerfen nicht anschliessend als normale Winmon-Antworten interpretiert werden.\n        return;\n    }\n\n    if (pending \u0026\u0026 pending.kind === \"powerAssistInfoAction\") {\n        const received = parseSettingInfoResponse(payload);\n        const supportedMask = received.maximum_raw === undefined ? null : received.maximum_raw;\n        if (supportedMask !== null) {\n            const supported = Boolean(supportedMask \u0026 0x20);\n            storePowerAssistState({\n                supported,\n                flags0_supported_mask: supportedMask,\n                info_read_at: new Date().toISOString()\n            });\n            const action = flow.get(\"vebusPowerAssistAction\");\n            if (action \u0026\u0026 action.active) {\n                if (supported) {\n                    action.stage = \"readFlags\";\n                    action.status = \"reading\";\n                    flow.set(\"vebusPowerAssistAction\", action);\n                } else {\n                    finishPowerAssistAction(action, \"failed\", \"AssistEnabled-Bit wird vom Geraet nicht als unterstuetzt gemeldet\");\n                }\n            }\n            clearPending(\"powerAssistInfoAction\");\n        }\n    }\n\n    if (pending \u0026\u0026 pending.kind === \"ramInfo\") {\n        let sc = null;\n        let offset = null;\n        let winmonMode = 1;\n\n        // Mode 2 liefert: 8E, ScLo, ScHi, OffsetLo, OffsetHi.\n        // Mode 1 liefert zwei markierte 3-Byte-Datensaetze: 8E... 8F...\n        if (payload[0] === 0x8e \u0026\u0026 payload.length \u003e= 5 \u0026\u0026\n            (payload.length === 5 || payload[3] !== 0x8f)) {\n            sc = s16(payload, 1);\n            offset = s16(payload, 3);\n            winmonMode = 2;\n        } else {\n            for (let index = 0; index + 2 \u003c payload.length; index += 3) {\n                if (payload[index] === 0x8e) sc = s16(payload, index + 1);\n                if (payload[index] === 0x8f) offset = s16(payload, index + 1);\n            }\n        }\n\n        if (sc !== null) {\n            const infos = flow.get(\"vebusRamInfo\") || {};\n            if (sc === 0) {\n                infos[pending.id] = { supported: false, reason: \"not supported\" };\n            } else if (offset !== null) {\n                const absolute = Math.abs(sc);\n                const isBit = offset === -32768;\n                infos[pending.id] = {\n                    supported: true,\n                    signed: sc \u003c 0,\n                    offset: offset,\n                    scale: isBit ? 1 : (absolute \u003e= 0x4000 ? 1 / (0x8000 - absolute) : absolute),\n                    winmon_mode: winmonMode\n                };\n                if (isBit) infos[pending.id].bit = absolute - 1;\n            }\n            flow.set(\"vebusRamInfo\", infos);\n            clearPending(\"ramInfo\");\n        }\n    }\n\n    if (pending \u0026\u0026 pending.kind === \"ramRead\" \u0026\u0026 payload.length \u003e= 3) {\n        const response = payload[0];\n        if (response === 0x85) {\n            const raw = u16(payload, 1);\n            const value = scaleRam(pending.id, raw, false);\n            data.ram = data.ram || {};\n            data.ram[RAM_NAMES[pending.id] || `ram_${pending.id}`] = round(value, 4);\n            data.ram_raw = data.ram_raw || {};\n            data.ram_raw[pending.id] = raw;\n\n            if (pending.id === 7 \u0026\u0026 value) data.ram.ac_output_frequency_hz = round(10 / value, 3);\n            if (pending.id === 8 \u0026\u0026 value) data.ram.ac_input_frequency_hz = round(10 / value, 3);\n            clearPending(\"ramRead\");\n        } else if (response === 0x90) {\n            clearPending(\"ramRead\");\n        }\n    }\n\n    for (let index = 0; index + 2 \u003c payload.length; index += 3) {\n        const response = payload[index];\n        const lo = payload[index + 1];\n        const hi = payload[index + 2];\n        if (response === 0x82) {\n            if (pending \u0026\u0026 pending.kind === \"discoveryProbeLow\") {\n                const discovery = flow.get(\"vebusDiscovery\") || {};\n                discovery.devices = discovery.devices || {};\n                const key = String(pending.address);\n                discovery.devices[key] = discovery.devices[key] || { address: pending.address };\n                discovery.devices[key].software_low_word = u16(payload, index + 1);\n                discovery.devices[key].detected_at = new Date().toISOString();\n                updateDiscoverySummary(discovery);\n                discovery.consecutive_misses = 0;\n                discovery.stage = \"probeHigh\";\n                flow.set(\"vebusDiscovery\", discovery);\n                data.devices = discovery.devices;\n                data.discovery = discovery;\n                clearPending(\"discoveryProbeLow\");\n            } else {\n                data.device = data.device || {};\n                data.device.software_low_word = u16(payload, index + 1);\n                clearPending(\"targetVersion0\");\n            }\n        } else if (response === 0x83) {\n            if (pending \u0026\u0026 pending.kind === \"discoveryProbeHigh\") {\n                const discovery = flow.get(\"vebusDiscovery\") || {};\n                discovery.devices = discovery.devices || {};\n                const key = String(pending.address);\n                const device = discovery.devices[key] || { address: pending.address };\n                device.software_high_word = u16(payload, index + 1);\n                if (device.software_low_word !== undefined) {\n                    device.software_raw = ((device.software_high_word * 0x10000) + device.software_low_word) \u003e\u003e\u003e 0;\n                }\n                decorateDevice(device);\n                discovery.devices[key] = device;\n                updateDiscoverySummary(discovery);\n                discovery.consecutive_misses = 0;\n                discovery.address = pending.address + 1;\n                discovery.stage = discovery.address \u003e 0x1f ? \"restoreAddress0\" : \"setAddress\";\n                flow.set(\"vebusDiscovery\", discovery);\n                data.devices = discovery.devices;\n                data.discovery = discovery;\n                clearPending(\"discoveryProbeHigh\");\n            } else {\n                data.device = data.device || {};\n                data.device.software_high_word = u16(payload, index + 1);\n                clearPending(\"targetVersion1\");\n            }\n        } else if (response === 0x94) {\n            setMainState(data, lo, hi);\n            clearPending(\"state\");\n        } else if (response === 0x86 \u0026\u0026 pending \u0026\u0026\n                   [\"powerAssistRead\", \"powerAssistReadBefore\", \"powerAssistVerify\"].includes(pending.kind)) {\n            const flags0 = u16(payload, index + 1);\n            storeChargerSetting(0, flags0, data);\n            const state = storePowerAssistState({\n                supported: true,\n                enabled: Boolean(flags0 \u0026 0x20),\n                flags0_raw: flags0,\n                last_read_at: new Date().toISOString()\n            });\n            const action = flow.get(\"vebusPowerAssistAction\");\n\n            if (pending.kind === \"powerAssistReadBefore\" \u0026\u0026 action \u0026\u0026 action.active) {\n                action.original_flags0 = flags0;\n                if (action.action === \"read\") {\n                    finishPowerAssistAction(action, \"complete\", null);\n                } else {\n                    action.desired_flags0 = action.action === \"enable\"\n                        ? (flags0 | 0x20)\n                        : (flags0 \u0026 0xffdf);\n                    if (action.desired_flags0 === flags0) {\n                        finishPowerAssistAction(action, \"complete\", null);\n                    } else {\n                        action.stage = \"write\";\n                        action.status = \"ready_to_write\";\n                        flow.set(\"vebusPowerAssistAction\", action);\n                    }\n                }\n            } else if (pending.kind === \"powerAssistVerify\" \u0026\u0026 action \u0026\u0026 action.active) {\n                action.verified_flags0 = flags0;\n                if (flags0 === action.desired_flags0) {\n                    finishPowerAssistAction(action, \"complete\", null);\n                } else {\n                    finishPowerAssistAction(action, \"failed\", `Readback ${flags0} entspricht nicht Ziel ${action.desired_flags0}`);\n                }\n            }\n            data.powerassist = state;\n            clearPending(pending.kind);\n        } else if (response === 0x86 \u0026\u0026 pending \u0026\u0026 pending.kind === \"chargerSettingRead\") {\n            const raw = u16(payload, index + 1);\n            storeChargerSetting(pending.id, raw, data);\n            if (pending.id === 0) {\n                storePowerAssistState({\n                    supported: true,\n                    enabled: Boolean(raw \u0026 0x20),\n                    flags0_raw: raw,\n                    last_read_at: new Date().toISOString()\n                });\n            }\n            clearPending(\"chargerSettingRead\");\n        } else if (response === 0x91 \u0026\u0026 pending \u0026\u0026 pending.kind === \"chargerSettingRead\") {\n            const charger = Object.assign({ read_only: true, values: {}, details: {} }, flow.get(\"vebusChargerSettings\") || {});\n            const name = CHARGER_SETTING_NAMES[pending.id] || `setting_${pending.id}`;\n            charger.details[name] = { id: pending.id, supported: false };\n            flow.set(\"vebusChargerSettings\", charger);\n            data.charger_settings = charger;\n            clearPending(\"chargerSettingRead\");\n        } else if (response === 0x88 \u0026\u0026 pending \u0026\u0026 pending.kind === \"powerAssistWrite\") {\n            const action = flow.get(\"vebusPowerAssistAction\");\n            if (action \u0026\u0026 action.active) {\n                action.stage = \"verify\";\n                action.status = \"verifying\";\n                action.write_acknowledged = true;\n                flow.set(\"vebusPowerAssistAction\", action);\n            }\n            clearPending(\"powerAssistWrite\");\n        } else if (response === 0x91 \u0026\u0026 pending \u0026\u0026 pending.kind.startsWith(\"powerAssist\")) {\n            storePowerAssistState({ supported: false, last_read_at: new Date().toISOString() });\n            const action = flow.get(\"vebusPowerAssistAction\");\n            if (action \u0026\u0026 action.active) finishPowerAssistAction(action, \"failed\", \"Flags0-Setting wird nicht unterstuetzt\");\n            clearPending(pending.kind);\n        } else if (response === 0x80) {\n            decoded.protocol_error = { reason: lo, detail: hi };\n            const action = flow.get(\"vebusPowerAssistAction\");\n            if (pending \u0026\u0026 pending.kind.startsWith(\"powerAssist\") \u0026\u0026 action \u0026\u0026 action.active) {\n                finishPowerAssistAction(action, \"failed\", `Protokollfehler ${lo}/${hi}`);\n            }\n            flow.set(\"vebusPending\", null);\n        }\n    }\n\n    if (data.device \u0026\u0026 data.device.software_low_word !== undefined \u0026\u0026 data.device.software_high_word !== undefined) {\n        data.device.software_raw = ((data.device.software_high_word * 0x10000) + data.device.software_low_word) \u003e\u003e\u003e 0;\n        decorateDevice(data.device);\n    }\n}\n\nfunction decodeInfoFrame(payload, data, decoded) {\n    if (payload.length \u003c 14) {\n        decoded.warning = \"Info frame too short\";\n        return;\n    }\n\n    const phaseInfo = payload[4];\n    if (phaseInfo === 0x0c) {\n        const voltage = scaleRam(4, u16(payload, 5), true);\n        const inverterCurrent = scaleRam(5, u24(payload, 7), true);\n        const chargerCurrent = scaleRam(5, u24(payload, 10), true);\n        const period = scaleRam(7, payload[13], true);\n\n        const batteryCurrent = chargerCurrent === null || inverterCurrent === null ? null : chargerCurrent - inverterCurrent;\n        data.dc = {\n            battery_voltage_v: round(voltage, 3),\n            battery_current_a: round(batteryCurrent, 3),\n            battery_power_w: round(voltage === null || batteryCurrent === null ? null : voltage * batteryCurrent, 1),\n            charging_current_a: round(chargerCurrent, 3),\n            charging_power_w: round(voltage === null || chargerCurrent === null ? null : voltage * chargerCurrent, 1),\n            inverting_current_a: round(inverterCurrent, 3),\n            inverting_power_w: round(voltage === null || inverterCurrent === null ? null : voltage * inverterCurrent, 1),\n            output_frequency_hz: round(period ? 10 / period : null, 3)\n        };\n        decoded.frame_type = \"dc_info\";\n        clearPending(\"dcInfo\");\n        return;\n    }\n\n    const inputVoltage = scaleRam(0, u16(payload, 5), true);\n    const inputCurrentBase = scaleRam(1, u16(payload, 7), false);\n    const outputVoltage = scaleRam(2, u16(payload, 9), true);\n    const outputCurrentBase = scaleRam(3, u16(payload, 11), false);\n    const period = scaleRam(8, payload[13], true);\n    const inputCurrent = inputCurrentBase === null ? null : normalizeAcCurrent(inputCurrentBase * payload[0]);\n    const outputCurrent = outputCurrentBase === null ? null : normalizeAcCurrent(outputCurrentBase * payload[1]);\n\n    data.ac = {\n        phase_info: phaseInfo,\n        phase: phaseInfo \u003e= 8 ? 1 : (phaseInfo === 7 ? 2 : (phaseInfo === 6 ? 3 : 4)),\n        phase_count: phaseInfo \u003e= 8 \u0026\u0026 phaseInfo \u003c= 11 ? phaseInfo - 7 : null,\n        input_voltage_v: round(inputVoltage, 3),\n        input_current_a: round(inputCurrent, 3),\n        output_voltage_v: round(outputVoltage, 3),\n        output_current_a: round(outputCurrent, 3),\n        input_frequency_hz: round(period ? 10 / period : null, 3),\n        input_apparent_power_va: round(inputVoltage === null || inputCurrent === null ? null : Math.abs(inputVoltage * inputCurrent), 1),\n        output_apparent_power_va: round(outputVoltage === null || outputCurrent === null ? null : Math.abs(outputVoltage * outputCurrent), 1)\n    };\n    setMainState(data, payload[3]);\n    decoded.frame_type = \"ac_info\";\n    clearPending(\"acInfo\");\n}\n\nfunction decodeMasterFrame(payload, data, decoded) {\n    if (payload.length \u003c 11) {\n        decoded.warning = \"MasterMultiLED frame too short\";\n        return;\n    }\n\n    const config = payload[4];\n    data.ac_input = {\n        config_raw: config,\n        last_active_input: config \u0026 0x03,\n        overridden_by_panel: Boolean(config \u0026 0x04),\n        remote_panel_detected: Boolean(config \u0026 0x80),\n        minimum_limit_a: round(u16(payload, 5) / 10, 1),\n        maximum_limit_a: round(u16(payload, 7) / 10, 1),\n        actual_limit_a: round(u16(payload, 9) / 10, 1)\n    };\n\n    if (payload.length \u003e= 12) {\n        const register = payload[11];\n        const charge = Boolean(register \u0026 0x10);\n        const invert = Boolean(register \u0026 0x20);\n        data.ac_input.switch_register = register;\n        data.state = data.state || {};\n        data.state.panel_mode_code = charge \u0026\u0026 invert ? 3 : (charge ? 1 : (invert ? 2 : 4));\n        data.state.panel_mode = { 1: \"charger_only\", 2: \"inverter_only\", 3: \"on\", 4: \"off\" }[data.state.panel_mode_code];\n    }\n\n    decoded.frame_type = \"master_multi_led\";\n    clearPending(\"masterInfo\");\n}\n\nfunction decodeFrame(frame) {\n    const body = Array.from(frame.slice(1, -1));\n    const mk2 = body[0] === 0xff;\n    const command = mk2 ? body[1] : body[0];\n    const payload = body.slice(mk2 ? 2 : 1);\n    const data = flow.get(\"vebusData\") || { connection: {}, device: {}, state: {} };\n    const decoded = {\n        timestamp: new Date().toISOString(),\n        raw_hex: frameHex(frame),\n        length: frame.length,\n        checksum_valid: true,\n        protocol: mk2 ? \"MK2/MK3\" : \"VE.Bus\",\n        command_hex: `0x${command.toString(16).padStart(2, \"0\")}`,\n        command_ascii: command \u003e= 32 \u0026\u0026 command \u003c= 126 ? String.fromCharCode(command) : null\n    };\n\n    data.connection = data.connection || {};\n    data.connection.online = true;\n    data.connection.last_seen = decoded.timestamp;\n    data.connection.last_raw_hex = decoded.raw_hex;\n\n    if (mk2 \u0026\u0026 command === 0x41) {\n        const pending = flow.get(\"vebusPending\");\n        flow.set(\"vebusAddressOk\", true);\n        data.device = data.device || {};\n        const selectedAddress = payload[1] === undefined ? 0 : payload[1];\n        data.connection.selected_address = selectedAddress;\n\n        if (pending \u0026\u0026 pending.kind === \"discoverySetAddress\") {\n            const discovery = flow.get(\"vebusDiscovery\") || {};\n            discovery.selected_address = selectedAddress;\n            discovery.stage = \"probeLow\";\n            flow.set(\"vebusDiscovery\", discovery);\n            data.discovery = discovery;\n            clearPending(\"discoverySetAddress\");\n            decoded.frame_type = \"discovery_address_selected\";\n        } else if (pending \u0026\u0026 pending.kind === \"discoveryRestore\") {\n            const discovery = flow.get(\"vebusDiscovery\") || {};\n            discovery.active = false;\n            discovery.stage = \"complete\";\n            discovery.selected_address = 0;\n            discovery.finished_at = new Date().toISOString();\n            updateDiscoverySummary(discovery);\n            flow.set(\"vebusDiscovery\", discovery);\n            data.discovery = discovery;\n            data.devices = discovery.devices || {};\n            data.device.address = 0;\n            clearPending(\"discoveryRestore\");\n            decoded.frame_type = \"discovery_complete\";\n        } else {\n            data.device.address = selectedAddress;\n            clearPending(\"address\");\n            decoded.frame_type = \"address_reply\";\n        }\n    } else if (mk2 \u0026\u0026 command === 0x56) {\n        data.device = data.device || {};\n        data.device.mk2_firmware_bytes = payload;\n        clearPending(\"mk2Version\");\n        decoded.frame_type = \"mk2_version\";\n    } else if (mk2 \u0026\u0026 (command === 0x57 || command === 0x58 || command === 0x59 || command === 0x5a)) {\n        decodeWinmon(payload, data, decoded);\n        decoded.frame_type = decoded.frame_type || \"winmon\";\n    } else if (mk2 \u0026\u0026 command === 0x4c) {\n        const on = payload[0] || 0;\n        const blink = payload[1] || 0;\n        data.led = {};\n        LED_NAMES.forEach((name, bit) =\u003e {\n            data.led[name] = {\n                on: Boolean(on \u0026 (1 \u003c\u003c bit)),\n                blinking: Boolean(blink \u0026 (1 \u003c\u003c bit))\n            };\n        });\n        decoded.frame_type = \"led_status\";\n        clearPending(\"ledInfo\");\n    } else if (!mk2 \u0026\u0026 command === 0x20) {\n        decodeInfoFrame(payload, data, decoded);\n    } else if (!mk2 \u0026\u0026 command === 0x41) {\n        decodeMasterFrame(payload, data, decoded);\n    } else {\n        decoded.frame_type = \"unhandled\";\n        decoded.data = payload;\n    }\n\n    const discovery = flow.get(\"vebusDiscovery\");\n    if (discovery) {\n        data.discovery = discovery;\n        data.devices = discovery.devices || data.devices || {};\n    }\n    const powerAssist = flow.get(\"vebusPowerAssist\");\n    if (powerAssist) data.powerassist = powerAssist;\n    const chargerSettings = flow.get(\"vebusChargerSettings\");\n    if (chargerSettings) data.charger_settings = chargerSettings;\n    data.updated_at = decoded.timestamp;\n    flow.set(\"vebusData\", data);\n\n    const stateName = data.state \u0026\u0026 data.state.name ? data.state.name : \"warte auf Status\";\n    const voltage = data.dc \u0026\u0026 data.dc.battery_voltage_v !== null ? `${data.dc.battery_voltage_v} V` : \"-- V\";\n    node.status({ fill: \"green\", shape: \"dot\", text: `${voltage} | ${stateName}` });\n    return { data, decoded };\n}\n\nlet chunk;\ntry {\n    chunk = Buffer.isBuffer(msg.payload) ? msg.payload : Buffer.from(msg.payload);\n} catch (error) {\n    node.warn(`VE.Bus: Payload ist kein Buffer: ${error.message}`);\n    return null;\n}\n\nlet receiveBuffer = context.get(\"receiveBuffer\") || Buffer.alloc(0);\nreceiveBuffer = Buffer.concat([receiveBuffer, chunk]);\nconst stateMessages = [];\nconst diagnosticMessages = [];\n\nwhile (receiveBuffer.length \u003e= 3) {\n    const contentLength = receiveBuffer[0] \u0026 0x7f;\n    const totalLength = contentLength + 2;\n\n    if (contentLength \u003c 1 || contentLength \u003e 64) {\n        receiveBuffer = receiveBuffer.slice(1);\n        continue;\n    }\n    if (receiveBuffer.length \u003c totalLength) break;\n\n    const candidate = receiveBuffer.slice(0, totalLength);\n    let sum = 0;\n    for (const value of candidate) sum = (sum + value) \u0026 0xff;\n    if (sum !== 0) {\n        receiveBuffer = receiveBuffer.slice(1);\n        continue;\n    }\n\n    receiveBuffer = receiveBuffer.slice(totalLength);\n    const result = decodeFrame(candidate);\n    stateMessages.push({ topic: \"vebus/state\", payload: readableState(result.data) });\n    diagnosticMessages.push({ topic: \"vebus/frame\", payload: result.decoded });\n}\n\nif (receiveBuffer.length \u003e 256) {\n    node.warn(\"VE.Bus: Empfangspuffer verworfen (keine gueltigen Frames)\");\n    receiveBuffer = Buffer.alloc(0);\n}\ncontext.set(\"receiveBuffer\", receiveBuffer);\n\nreturn [stateMessages.length ? stateMessages : null, diagnosticMessages.length ? diagnosticMessages : null];\n",
        "outputs":  2,
        "timeout":  0,
        "noerr":  0,
        "initialize":  "",
        "finalize":  "",
        "libs":  [

                 ],
        "x":  420,
        "y":  260,
        "wires":  [
                      [
                          "vebus_debug_state"
                      ],
                      [
                          "vebus_debug_frames"
                      ]
                  ]
    },
    {
        "id":  "vebus_debug_state",
        "type":  "debug",
        "z":  "vebus_multiplus2_tab",
        "name":  "Lesbare Werte / Zustand",
        "active":  true,
        "tosidebar":  true,
        "console":  false,
        "tostatus":  false,
        "complete":  "payload",
        "targetType":  "msg",
        "statusVal":  "",
        "statusType":  "auto",
        "x":  760,
        "y":  240,
        "wires":  [

                  ]
    },
    {
        "id":  "vebus_debug_frames",
        "type":  "debug",
        "z":  "vebus_multiplus2_tab",
        "name":  "Rohframes (bei Bedarf)",
        "active":  false,
        "tosidebar":  true,
        "console":  false,
        "tostatus":  false,
        "complete":  "payload",
        "targetType":  "msg",
        "statusVal":  "",
        "statusType":  "auto",
        "x":  760,
        "y":  290,
        "wires":  [

                  ]
    },
    {
        "id":  "vebus_note_control",
        "type":  "comment",
        "z":  "vebus_multiplus2_tab",
        "name":  "ACHTUNG: Inject schaltet reale Leistungshardware",
        "info":  "Erst Messwerte und gemeldete Grenzen pruefen. input_limit_a ist das AC-EINGANGS-Limit, nicht der maximale DC-Batterieladestrom.",
        "x":  260,
        "y":  390,
        "wires":  [

                  ]
    },
    {
        "id":  "vebus_on",
        "type":  "inject",
        "z":  "vebus_multiplus2_tab",
        "name":  "EIN",
        "props":  [
                      {
                          "p":  "payload"
                      }
                  ],
        "repeat":  "",
        "crontab":  "",
        "once":  false,
        "onceDelay":  0.1,
        "topic":  "",
        "payload":  "{\"mode\":\"on\",\"confirm\":true}",
        "payloadType":  "json",
        "x":  100,
        "y":  450,
        "wires":  [
                      [
                          "vebus_control"
                      ]
                  ]
    },
    {
        "id":  "vebus_off",
        "type":  "inject",
        "z":  "vebus_multiplus2_tab",
        "name":  "AUS",
        "props":  [
                      {
                          "p":  "payload"
                      }
                  ],
        "repeat":  "",
        "crontab":  "",
        "once":  false,
        "onceDelay":  0.1,
        "topic":  "",
        "payload":  "{\"mode\":\"off\",\"confirm\":true}",
        "payloadType":  "json",
        "x":  100,
        "y":  490,
        "wires":  [
                      [
                          "vebus_control"
                      ]
                  ]
    },
    {
        "id":  "vebus_inv_only",
        "type":  "inject",
        "z":  "vebus_multiplus2_tab",
        "name":  "Nur Wechselrichter",
        "props":  [
                      {
                          "p":  "payload"
                      }
                  ],
        "repeat":  "",
        "crontab":  "",
        "once":  false,
        "onceDelay":  0.1,
        "topic":  "",
        "payload":  "{\"mode\":\"inverter_only\",\"confirm\":true}",
        "payloadType":  "json",
        "x":  140,
        "y":  530,
        "wires":  [
                      [
                          "vebus_control"
                      ]
                  ]
    },
    {
        "id":  "vebus_charger_only",
        "type":  "inject",
        "z":  "vebus_multiplus2_tab",
        "name":  "Nur Ladegeraet",
        "props":  [
                      {
                          "p":  "payload"
                      }
                  ],
        "repeat":  "",
        "crontab":  "",
        "once":  false,
        "onceDelay":  0.1,
        "topic":  "",
        "payload":  "{\"mode\":\"charger_only\",\"confirm\":true}",
        "payloadType":  "json",
        "x":  130,
        "y":  570,
        "wires":  [
                      [
                          "vebus_control"
                      ]
                  ]
    },
    {
        "id":  "vebus_limit_10",
        "type":  "inject",
        "z":  "vebus_multiplus2_tab",
        "name":  "EIN + AC-Limit 10 A",
        "props":  [
                      {
                          "p":  "payload"
                      }
                  ],
        "repeat":  "",
        "crontab":  "",
        "once":  false,
        "onceDelay":  0.1,
        "topic":  "",
        "payload":  "{\"mode\":\"on\",\"input_limit_a\":10,\"confirm\":true}",
        "payloadType":  "json",
        "x":  140,
        "y":  610,
        "wires":  [
                      [
                          "vebus_control"
                      ]
                  ]
    },
    {
        "id":  "vebus_limit_4_only",
        "type":  "inject",
        "z":  "vebus_multiplus2_tab",
        "name":  "AC-Limit 4 A (nur Limit)",
        "props":  [
                      {
                          "p":  "payload"
                      }
                  ],
        "repeat":  "",
        "crontab":  "",
        "once":  false,
        "onceDelay":  0.1,
        "topic":  "",
        "payload":  "{\"input_limit_a\":4,\"confirm\":true}",
        "payloadType":  "json",
        "x":  140,
        "y":  650,
        "wires":  [
                      [
                          "vebus_control"
                      ]
                  ]
    },
    {
        "id":  "vebus_control",
        "type":  "function",
        "z":  "vebus_multiplus2_tab",
        "name":  "VE.Bus Steuerbefehl + Schutz",
        "func":  "// Node-RED Function-Node: 2 Ausgaenge\n// 1: Buffer an serial out, 2: Audit/Fehler an Debug\n\nfunction mk2Frame(command, payload) {\n    const body = [0xff, command.charCodeAt(0)].concat(payload || []);\n    const bytes = [body.length].concat(body);\n    let sum = 0;\n    for (const value of bytes) sum = (sum + value) \u0026 0xff;\n    bytes.push((-sum) \u0026 0xff);\n    return Buffer.from(bytes);\n}\n\nfunction reject(reason, request) {\n    node.status({ fill: \"red\", shape: \"ring\", text: reason });\n    return [null, {\n        topic: \"vebus/control/rejected\",\n        payload: { ok: false, reason, request, timestamp: new Date().toISOString() }\n    }];\n}\n\nlet request = msg.payload;\nif (typeof request === \"string\") request = { mode: request };\nif (!request || typeof request !== \"object\") return reject(\"Payload muss ein Objekt sein\", request);\nif (request.confirm !== true) return reject(\"confirm:true fehlt\", request);\nconst powerAssistAction = flow.get(\"vebusPowerAssistAction\");\nif (powerAssistAction \u0026\u0026 powerAssistAction.active) {\n    return reject(\"PowerAssist-Konfigurationsauftrag laeuft; bitte warten\", request);\n}\n\nconst MODE_CODES = {\n    charger_only: 1,\n    inverter_only: 2,\n    on: 3,\n    off: 4\n};\n\nlet mode = request.mode;\nconst live = flow.get(\"vebusData\") || {};\nif (!mode \u0026\u0026 request.input_limit_a !== undefined) {\n    mode = live.state \u0026\u0026 (live.state.panel_mode || live.state.inferred_panel_mode);\n}\nif (!Object.prototype.hasOwnProperty.call(MODE_CODES, mode)) {\n    return reject(\"Modus fehlt/ungueltig; on, off, inverter_only oder charger_only verwenden\", request);\n}\n\nlet limit;\nlet limitSource;\nif (request.input_limit_a !== undefined) {\n    limit = Number(request.input_limit_a);\n    limitSource = \"request\";\n    if (!Number.isFinite(limit) || limit \u003c= 0 || limit \u003e 100) {\n        return reject(\"input_limit_a muss groesser 0 und maximal 100 A sein\", request);\n    }\n\n    const maximum = live.ac_input \u0026\u0026 live.ac_input.maximum_limit_a;\n    if (Number.isFinite(maximum) \u0026\u0026 limit \u003e maximum) {\n        return reject(`Limit ${limit} A ist groesser als das gemeldete Maximum ${maximum} A`, request);\n    }\n} else {\n    // Das S-Telegramm besitzt keinen Modus-ohne-Limit-Befehl. 0x8000 bedeutet\n    // nicht \"unveraendert\", sondern \"Sollwert ignorieren\" und fuehrt beim Multi\n    // typischerweise zum Geraetemaximum. Deshalb den gelesenen Istwert mitsenden.\n    limit = Number(live.ac_input \u0026\u0026 live.ac_input.actual_limit_a);\n    limitSource = \"current_state\";\n    if (!Number.isFinite(limit) || limit \u003c= 0 || limit \u003e 100) {\n        return reject(\"Aktuelles AC-Eingangslimit ist unbekannt; zuerst Status einlesen oder input_limit_a mitsenden\", request);\n    }\n}\n\nconst limitRaw = Math.round(limit * 10);\n\nconst payload = [\n    MODE_CODES[mode],\n    limitRaw \u0026 0xff,\n    (limitRaw \u003e\u003e 8) \u0026 0xff,\n    0x01,\n    0x80 // Variante 2: absolutes AC-Eingangslimit in 0,1 A\n];\nconst buffer = mk2Frame(\"S\", payload);\nconst audit = {\n    ok: true,\n    requested_mode: mode,\n    requested_input_limit_a: request.input_limit_a === undefined ? null : Number(request.input_limit_a),\n    sent_input_limit_a: limit,\n    limit_source: limitSource,\n    tx_hex: Array.from(buffer).map(x =\u003e x.toString(16).padStart(2, \"0\")).join(\" \"),\n    timestamp: new Date().toISOString()\n};\n\nnode.status({ fill: \"yellow\", shape: \"dot\", text: `${mode}, Limit ${limit} A (${limitSource})` });\nreturn [\n    { topic: \"vebus/control/tx\", payload: buffer },\n    { topic: \"vebus/control/audit\", payload: audit }\n];\n",
        "outputs":  2,
        "timeout":  0,
        "noerr":  0,
        "initialize":  "",
        "finalize":  "",
        "libs":  [

                 ],
        "x":  430,
        "y":  550,
        "wires":  [
                      [
                          "vebus_serial_out"
                      ],
                      [
                          "vebus_debug_control"
                      ]
                  ]
    },
    {
        "id":  "vebus_debug_control",
        "type":  "debug",
        "z":  "vebus_multiplus2_tab",
        "name":  "Steuerung Audit / Fehler",
        "active":  true,
        "tosidebar":  true,
        "console":  false,
        "tostatus":  false,
        "complete":  "payload",
        "targetType":  "msg",
        "statusVal":  "",
        "statusType":  "auto",
        "x":  760,
        "y":  540,
        "wires":  [

                  ]
    },
    {
        "id":  "vebus_note_powerassist",
        "type":  "comment",
        "z":  "vebus_multiplus2_tab",
        "name":  "PowerAssist: Flags0 lesen, Bit 5 sicher aendern, Readback",
        "info":  "Schreiben wird bei laufender Suche oder mehreren erkannten Geraeten abgewiesen.",
        "x":  300,
        "y":  700,
        "wires":  [

                  ]
    },
    {
        "id":  "vebus_pa_read",
        "type":  "inject",
        "z":  "vebus_multiplus2_tab",
        "name":  "PowerAssist Status lesen",
        "props":  [
                      {
                          "p":  "payload"
                      }
                  ],
        "repeat":  "",
        "crontab":  "",
        "once":  false,
        "onceDelay":  0.1,
        "topic":  "",
        "payload":  "{\"action\":\"read\"}",
        "payloadType":  "json",
        "x":  150,
        "y":  750,
        "wires":  [
                      [
                          "vebus_powerassist_control"
                      ]
                  ]
    },
    {
        "id":  "vebus_pa_enable",
        "type":  "inject",
        "z":  "vebus_multiplus2_tab",
        "name":  "PowerAssist AKTIVIEREN",
        "props":  [
                      {
                          "p":  "payload"
                      }
                  ],
        "repeat":  "",
        "crontab":  "",
        "once":  false,
        "onceDelay":  0.1,
        "topic":  "",
        "payload":  "{\"action\":\"enable\",\"confirm\":true}",
        "payloadType":  "json",
        "x":  160,
        "y":  790,
        "wires":  [
                      [
                          "vebus_powerassist_control"
                      ]
                  ]
    },
    {
        "id":  "vebus_pa_disable",
        "type":  "inject",
        "z":  "vebus_multiplus2_tab",
        "name":  "PowerAssist DEAKTIVIEREN",
        "props":  [
                      {
                          "p":  "payload"
                      }
                  ],
        "repeat":  "",
        "crontab":  "",
        "once":  false,
        "onceDelay":  0.1,
        "topic":  "",
        "payload":  "{\"action\":\"disable\",\"confirm\":true}",
        "payloadType":  "json",
        "x":  170,
        "y":  830,
        "wires":  [
                      [
                          "vebus_powerassist_control"
                      ]
                  ]
    },
    {
        "id":  "vebus_powerassist_control",
        "type":  "function",
        "z":  "vebus_multiplus2_tab",
        "name":  "PowerAssist sicher lesen/schalten",
        "func":  "// Node-RED Function-Node: 2 Ausgaenge\n// 1: Trigger an den zentralen Poller, 2: Audit/Fehler\n\nfunction reject(reason, request) {\n    node.status({ fill: \"red\", shape: \"ring\", text: reason });\n    return [null, {\n        topic: \"vebus/powerassist/rejected\",\n        payload: { ok: false, reason, request, timestamp: new Date().toISOString() }\n    }];\n}\n\nconst request = msg.payload \u0026\u0026 typeof msg.payload === \"object\" ? msg.payload : {};\nconst actionName = String(request.action || \"read\").toLowerCase();\nif (![\"read\", \"enable\", \"disable\"].includes(actionName)) {\n    return reject(\"action muss read, enable oder disable sein\", request);\n}\nif (actionName !== \"read\" \u0026\u0026 request.confirm !== true) {\n    return reject(\"Zum Schalten ist confirm:true erforderlich\", request);\n}\n\nconst discovery = flow.get(\"vebusDiscovery\");\nif (discovery \u0026\u0026 discovery.active) {\n    return reject(\"Geraetesuche laeuft; danach erneut versuchen\", request);\n}\nif (actionName !== \"read\" \u0026\u0026 (!discovery || discovery.stage !== \"complete\")) {\n    return reject(\"Vor dem Schalten einmal \u0027Geraete suchen\u0027 ausfuehren\", request);\n}\n\nconst detected = discovery \u0026\u0026 Array.isArray(discovery.found_addresses)\n    ? discovery.found_addresses\n    : [];\nif (actionName !== \"read\" \u0026\u0026 detected.length \u003e 1) {\n    return reject(\"Mehrere VE.Bus-Geraete erkannt; PowerAssist muss systemweit konsistent konfiguriert werden\", request);\n}\n\nconst live = flow.get(\"vebusData\") || {};\nconst selectedAddress = live.connection \u0026\u0026 live.connection.selected_address;\nif (selectedAddress !== undefined \u0026\u0026 selectedAddress !== 0) {\n    return reject(`Aktuell ist Adresse ${selectedAddress} ausgewaehlt; erwartet wird Adresse 0`, request);\n}\n\nconst existing = flow.get(\"vebusPowerAssistAction\");\nif (existing \u0026\u0026 existing.active) {\n    return reject(\"Ein PowerAssist-Auftrag laeuft bereits\", request);\n}\n\nconst action = {\n    active: true,\n    action: actionName,\n    stage: actionName === \"read\" ? \"readFlags\" : \"getInfo\",\n    requested_at: new Date().toISOString(),\n    original_flags0: null,\n    desired_flags0: null,\n    status: \"queued\",\n    error: null\n};\nflow.set(\"vebusPowerAssistAction\", action);\n\nnode.status({ fill: \"yellow\", shape: \"dot\", text: `${actionName} vorgemerkt` });\nreturn [\n    { topic: \"vebus/powerassist/run\", payload: true },\n    {\n        topic: \"vebus/powerassist/audit\",\n        payload: { ok: true, queued: true, action: actionName, timestamp: action.requested_at }\n    }\n];\n",
        "outputs":  2,
        "timeout":  0,
        "noerr":  0,
        "initialize":  "",
        "finalize":  "",
        "libs":  [

                 ],
        "x":  470,
        "y":  790,
        "wires":  [
                      [
                          "vebus_poller"
                      ],
                      [
                          "vebus_debug_powerassist"
                      ]
                  ]
    },
    {
        "id":  "vebus_debug_powerassist",
        "type":  "debug",
        "z":  "vebus_multiplus2_tab",
        "name":  "PowerAssist Audit / Fehler",
        "active":  true,
        "tosidebar":  true,
        "console":  false,
        "tostatus":  false,
        "complete":  "payload",
        "targetType":  "msg",
        "statusVal":  "",
        "statusType":  "auto",
        "x":  790,
        "y":  810,
        "wires":  [

                  ]
    },
    {
        "id":  "vebus_serial_port",
        "type":  "serial-port",
        "name":  "MK3 /dev/ttyPKW2 (2400 8N1)",
        "serialport":  "/dev/ttyPKW2",
        "serialbaud":  "2400",
        "databits":  "8",
        "parity":  "none",
        "stopbits":  "1",
        "waitfor":  "",
        "dtr":  "none",
        "rts":  "none",
        "cts":  "none",
        "dsr":  "none",
        "newline":  "100",
        "bin":  "bin",
        "out":  "interbyte",
        "addchar":  "",
        "responsetimeout":  "10000"
    }
]

Viel Spaß beim Ausprobieren!

1 Like