Einbindung Microwechselrichter über Shelly 1PM in VenusOS mit Node-Red

inspiriert vom Video von Schatten-PV (Victron ESS - Jeden Wechselrichter per Virtual Device über NodeRed in Venus OS einbinden! habe ich die Einbindung des Microwechselrichters umgestellt auf virtual Devices

Dazu muss nur die Palette victronenergy/node-red-contrib-victron heruntergeladen werden. Wie, wird im Video erklärt.

Falls jemand also ebenfalls seinen Microwechselrichter auf diese Art in sein VenusOS integrieren will findet er nachstehend den Flow.

Ändert dann noch den Hostnamen auf die IP-Adresse des Shelly und beim virtual Device den Namen

Node-Red Flow

[
{
"id": "081ffd4f7abcba01",
"type": "tab",
"label": "SUN als WR",
"disabled": false,
"info": "",
"env":
},
{
"id": "220bfe5db6671af3",
"type": "victron-virtual",
"z": "081ffd4f7abcba01",
"name": "SUN 1000",
"device": "pvinverter",
"default_values": false,
"battery_capacity": 25,
"include_battery_temperature": false,
"grid_nrofphases": 1,
"include_motor_temp": false,
"include_controller_temp": false,
"include_coolant_temp": false,
"include_motor_rpm": true,
"include_motor_direction": true,
"position": 0,
"pvinverter_nrofphases": 1,
"fluid_type": 0,
"include_tank_battery": false,
"include_tank_temperature": false,
"tank_battery_voltage": 3.3,
"tank_capacity": 0.2,
"temperature_type": 2,
"include_humidity": false,
"include_pressure": false,
"include_temp_battery": false,
"temp_battery_voltage": 3.3,
"x": 280,
"y": 120,
"wires":
},
{
"id": "6a51cc31b755e78a",
"type": "victron-output-custom",
"z": "081ffd4f7abcba01",
"service": "com.victronenergy.pvinverter/100",
"path": "/Ac/Power",
"serviceObj": {
"service": "com.victronenergy.pvinverter/100",
"name": "SUN 1000 via Shelly (100)"
},
"pathObj": {
"path": "/Ac/Power",
"name": "/Ac/Power",
"type": "object",
"value": null
},
"name": "",
"onlyChanges": false,
"x": 1030,
"y": 160,
"wires":
},
{
"id": "aae41889abb7cf37",
"type": "inject",
"z": "081ffd4f7abcba01",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "2",
"crontab": "",
"once": true,
"onceDelay": 0.1,
"topic": "",
"payload": "100",
"payloadType": "num",
"x": 130,
"y": 380,
"wires": [
[
"392102021be1267c"
]
]
},
{
"id": "392102021be1267c",
"type": "shelly-gen1",
"z": "081ffd4f7abcba01",
"hostname": "192.168.68.100",
"description": "Shelly 1PM Garage",
"mode": "polling",
"verbose": false,
"server": "",
"outputmode": "event",
"uploadretryinterval": 5000,
"pollinginterval": 5000,
"pollstatus": false,
"getstatusoncommand": true,
"devicetype": "SHSW-PM",
"devicetypemustmatchexactly": true,
"outputs": 1,
"x": 370,
"y": 380,
"wires": [
[
"7ac31c67a74b62bb",
"89b5df38d95712f8",
"5ad24980ad920896",
"79aec1f573b9cfab",
"5e01a201f93af5c3",
"3107d3f57cf74771"
]
]
},
{
"id": "7ac31c67a74b62bb",
"type": "change",
"z": "081ffd4f7abcba01",
"name": "Power",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "payload.meters[0].power",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 630,
"y": 200,
"wires": [
[
"6a51cc31b755e78a",
"61c9686a042a4811"
]
]
},
{
"id": "61c9686a042a4811",
"type": "victron-output-custom",
"z": "081ffd4f7abcba01",
"service": "com.victronenergy.pvinverter/100",
"path": "/Ac/L1/Power",
"serviceObj": {
"service": "com.victronenergy.pvinverter/100",
"name": "SUN 1000 via Shelly (100)"
},
"pathObj": {
"path": "/Ac/L1/Power",
"name": "/Ac/L1/Power",
"type": "number",
"value": 0
},
"name": "",
"onlyChanges": false,
"x": 1040,
"y": 240,
"wires":
},
{
"id": "a80edd66b25dfc2f",
"type": "victron-output-custom",
"z": "081ffd4f7abcba01",
"service": "com.victronenergy.pvinverter/100",
"path": "/Ac/L1/Voltage",
"serviceObj": {
"service": "com.victronenergy.pvinverter/100",
"name": "SUN 1000 via Shelly (100)"
},
"pathObj": {
"path": "/Ac/L1/Voltage",
"name": "/Ac/L1/Voltage",
"type": "number",
"value": 0
},
"name": "",
"onlyChanges": false,
"x": 1050,
"y": 300,
"wires":
},
{
"id": "89b5df38d95712f8",
"type": "change",
"z": "081ffd4f7abcba01",
"name": "Voltage",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "230",
"tot": "num"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 640,
"y": 300,
"wires": [
[
"a80edd66b25dfc2f"
]
]
},
{
"id": "5ad24980ad920896",
"type": "change",
"z": "081ffd4f7abcba01",
"name": "Current",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "$round(payload.meters[0].power /230, 2)",
"tot": "jsonata"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 640,
"y": 380,
"wires": [
[
"35c7aeaf8d881459"
]
]
},
{
"id": "35c7aeaf8d881459",
"type": "victron-output-custom",
"z": "081ffd4f7abcba01",
"service": "com.victronenergy.pvinverter/100",
"path": "/Ac/L1/Current",
"serviceObj": {
"service": "com.victronenergy.pvinverter/100",
"name": "SUN 1000 via Shelly (100)"
},
"pathObj": {
"path": "/Ac/L1/Current",
"name": "/Ac/L1/Current",
"type": "number",
"value": 0
},
"name": "",
"onlyChanges": false,
"x": 1050,
"y": 380,
"wires":
},
{
"id": "79aec1f573b9cfab",
"type": "change",
"z": "081ffd4f7abcba01",
"name": "Total",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "$round(payload.meters[0].total/1000/60, 2)",
"tot": "jsonata"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 630,
"y": 480,
"wires": [
[
"1733336d15a6f093",
"3eabf6c9655deea2"
]
]
},
{
"id": "1733336d15a6f093",
"type": "victron-output-custom",
"z": "081ffd4f7abcba01",
"service": "com.victronenergy.pvinverter/100",
"path": "/Ac/Energy/Forward",
"serviceObj": {
"service": "com.victronenergy.pvinverter/100",
"name": "SUN 1000 via Shelly (100)"
},
"pathObj": {
"path": "/Ac/Energy/Forward",
"name": "/Ac/Energy/Forward",
"type": "object",
"value": null
},
"name": "",
"onlyChanges": false,
"x": 1060,
"y": 460,
"wires":
},
{
"id": "3eabf6c9655deea2",
"type": "victron-output-custom",
"z": "081ffd4f7abcba01",
"service": "com.victronenergy.pvinverter/100",
"path": "/Ac/L1/Energy/Forward",
"serviceObj": {
"service": "com.victronenergy.pvinverter/100",
"name": "SUN 1000 via Shelly (100)"
},
"pathObj": {
"path": "/Ac/L1/Energy/Forward",
"name": "/Ac/L1/Energy/Forward",
"type": "number",
"value": 0
},
"name": "",
"onlyChanges": false,
"x": 1070,
"y": 520,
"wires":
},
{
"id": "7702c569fc8748cc",
"type": "victron-output-custom",
"z": "081ffd4f7abcba01",
"service": "com.victronenergy.pvinverter/100",
"path": "/StatusCode",
"serviceObj": {
"service": "com.victronenergy.pvinverter/100",
"name": "SUN 1000 (100)"
},
"pathObj": {
"path": "/StatusCode",
"name": "/StatusCode",
"type": "object",
"value": null
},
"name": "",
"onlyChanges": false,
"x": 1010,
"y": 600,
"wires":
},
{
"id": "5e01a201f93af5c3",
"type": "function",
"z": "081ffd4f7abcba01",
"name": "WR on or off",
"func": "\nif (msg.payload.relays[0].ison == true) {\n msg.payload = 1;\n} else {\n msg.payload = 0;\n}\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": ,
"x": 650,
"y": 580,
"wires": [
[
"7702c569fc8748cc"
]
]
},
{
"id": "3107d3f57cf74771",
"type": "debug",
"z": "081ffd4f7abcba01",
"name": "debug 17",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 400,
"y": 240,
"wires":
}
]

Viel Spaß beim implementieren

2 „Gefällt mir“

Danke für den Flow. Klappt grundsätzlich gut.

Eine Frage nur. Ich habe auf diese Art einen PV Inverter angelegt, eingebunden und mit EXTERNEN Daten via MQTT gefüttert.

Leider wird er nicht in der Solarertragsübersicht unter PV-Inverter I'm Venus OS GUI angezeigt, wenn er auf AC in steht. Wenn ich auf einen der Ausgänge im Device umstelle, wird er angezeigt.

Hat hier jemand eine Lösung?

Leider nicht, dieser Virtual Inverter wird nur bei AC Lasten angezeigt, wenn er im System mit AC Out hinterlegt ist

Habe ich mir schon fast gedacht. Danke

Danke fürs Teilen, bei mir lässt sich der Flow leider nicht importieren.

Hab die notwendige Palette natürlich installiert.

Es scheint beim 1:1 kopieren irgendein Formatierungsfehler vorzuliegen.

SyntaxError: Unexpected token '}', ...", "env": }, { "id":"... is not valid JSON

Anbei mein Flow (ist zwar ein 1PM Mini Gen 3, sollte man aber anpassen können ^^)

Shelly PM Mini Gen 3.json.txt (11,0 KB)

1 „Gefällt mir“

Schreibe doch bitte einmal welchen Shelly Du im Einsatz hast

Ich habe Dir einmal als Textdatei meine Daten angehangen und einen Screenshot

SUN_als_WR_JSON.txt (10,4 KB)

Nachtrag: Wenn Du weiter auf Probleme stößt, dann können wir auch einmal die Telefonnummern per PN austauschen

1 „Gefällt mir“

Mit VenusOS 3.5 haben virtual devices Einzug gehalten.
Ein Tasmota Lesekopf kann nun als Smartmeter eingebunden werden.

Ändere die Dateiendung von txt auf json und importiert es

Dann noch den richtigen Broker auswählen und das in Tasmota hinterlegte Topic

Dann sollte ein Grid im Menue sichtbar sein

flows.txt (16,3 KB)

Hier der Flow

Zwei Anmerkungen zu deinem Flow:

  1. kann nur empfehlen das virtual device 3 phasig zu wählen. Nur so ist man aktuell in der Lage den WR auch der tatsächlichen Phase zuzuweisen. Als 1phasiges device läuft das bisher leider immer über L1. Ist das real nicht so, dann weicht die visuelle VRM Darstellung von der Realität ab.

  2. Einzelne D-Bus Lese und Schreibzugriffe sind recht belastend für das System. Wenn man alles auf einen Output zentralisiert, lässt sich da noch einiges optimieren (Last reduzieren).
    Als Beispiel hier zu finden: RaW07/victron-nodered-shelly-balkonkraftwerk: Virtual PV Inverter for Victron via Node-RED and Shelly power meter — correct phase assignment for single-phase balcony power plants - Codeberg.org
    Beste Grüße

Der Virtual Device ist in diesem Flow 3-Phasig angelegt.

Siehe in dem angehängten JSON
"grid_nrofphases": "3",

Wenn die Einspeisung an einer anderen Phase wie L1 erfolgt, dann kann man sich mit dem Trick behelfen das man als Beispiel Power_curr_p3 an L1 hängt

Siehe nachstehendes Beispiel

"func": "// Prüfen, ob das SML-Objekt und Power_curr existieren\nif (msg.payload && msg.payload.sn.SML && msg.payload.sn.SML.Power_curr_p1 !== undefined) {\n \n // Extrahiere den aktuellen Leistungswert\n let power = msg.payload.sn.SML.Power_curr_p3;\n \n // Setze den Wert als neuen Payload (als Zahl)\n \n msg.payload = {\n "/Ac/L1/Power": power\n };\n \n return msg;\n}\n\n// Falls der Wert nicht im Datensatz war, ignoriere die Nachricht\nreturn null;\n",

Ist weiß nicht was Du mit Dieses und Jenes meinst. Ich habe auf Deinen Beitrag #6 geantwortet siehe Link/Referenz meiner Antwort.

Dort steht:

"pvinverter_nrofphases": 1,

Das war ein Beitrag aus Dezember 2025

Du hattest Deinen Kommentar unter meinen letzten Beitrag geschrieben, daher ging ich natürlich davon aus das dieser Flow gemeint war.

In meinem letzten Flow ist ein Gridmeter mit 3 Phasen abgebildet

Aber ich habe einen Fehler noch drin. Ich hatte bei mir die Phase 3 auf L1 gehängt, weil der Multiplus an L3 hängt und der grundsätzlich den ersten Multiplus an L1 erwartet. Diesen Flow hatte ich nur in Node-Red implementiert und deaktiviert falls mein Shelly 3EM einmal ausfallen sollte.

Ich mache einen neuen Beitrag mit dem korrigierten JSON.

den Intervall, in welchem zeitlichen Abstand der Tasmota sendet, legt man über die Config des Tasmota fest und für eine Null-Einspeisung sind 5 Sekunden zu lang.

Anbei ein Node-Red Flow für die Einbindung eines Tasmota Lesekopfs als Smartmeter

flows.json.txt (16,0 KB)

Angepasst werden muss noch der MQTT Server und der Topic unter dem der Tasmota Kopf sendet

Macht aber auch den aktuellen Flow nicht besser.

[
    {
        "id": "grid_tab_improved",
        "type": "tab",
        "label": "Tasmota Grid (konsolidiert)",
        "disabled": false,
        "info": "Tasmota SML-Zaehler -> ein Mapping-Node -> victron-virtual Grid.\nErsetzt die Variante mit 13 Function-Nodes.",
        "env": []
    },
    {
        "id": "grid_mqtt_in",
        "type": "mqtt in",
        "z": "grid_tab_improved",
        "name": "SML Telemetrie",
        "topic": "<Tasmota Topic hier eintragen>",
        "qos": "2",
        "datatype": "auto-detect",
        "broker": "grid_broker",
        "nl": false,
        "rap": true,
        "rh": 0,
        "inputs": 0,
        "x": 170,
        "y": 120,
        "wires": [
            [
                "grid_json"
            ]
        ]
    },
    {
        "id": "grid_json",
        "type": "json",
        "z": "grid_tab_improved",
        "name": "parse",
        "property": "payload",
        "action": "obj",
        "pretty": false,
        "x": 350,
        "y": 120,
        "wires": [
            [
                "grid_map"
            ]
        ]
    },
    {
        "id": "grid_map",
        "type": "function",
        "z": "grid_tab_improved",
        "name": "SML -> dbus (1 Objekt)",
        "func": "// Tasmota SML  ->  Victron virtual grid meter\n// EIN Node ersetzt die 13 Einzel-Nodes:\n//   - liest das bereits geparste JSON EINMAL,\n//   - baut EIN dbus-Objekt (Pfad -> Wert),\n//   - schreibt es in EINEM atomaren Payload an den victron-virtual Node.\n\nconst sml = msg.payload?.sn?.SML;\nif (!sml) return null;   // kein SML-Block -> nichts zu tun.\n                         // Optional Chaining verhindert den TypeError,\n                         // den der Original-Guard bei fehlendem \"sn\" wirft.\n\n// Zentrale Mapping-Tabelle: Tasmota-Feld -> Victron dbus-Pfad.\n// Alle Zuordnungen an EINER Stelle. Label, Code und Kommentar koennen\n// nicht mehr auseinanderlaufen, weil es nur noch eine Zeile pro Wert gibt.\n//\n// HINWEIS zum Phasentausch: Im Original standen in den p2-Nodes Kommentare\n// wie \"L1 und L3 werden vertauscht, da der Multiplus an L3 haengt\" - der Code\n// mappte aber gerade durch (p2 -> L2). Solche Vertauschungen gehoeren, falls\n// wirklich gewollt, HIER als sichtbare Zeile hin, nicht als toter Kommentar\n// in einem von dreizehn Nodes. Aktuell: 1:1-Mapping (p1->L1, p2->L2, p3->L3).\nconst MAP = {\n  Power_curr:    \"/Ac/Power\",\n  Total_in:      \"/Ac/Energy/Forward\",\n  Total_out:     \"/Ac/Energy/Reverse\",\n  Volt_p1:       \"/Ac/L1/Voltage\",\n  Volt_p2:       \"/Ac/L2/Voltage\",\n  Volt_p3:       \"/Ac/L3/Voltage\",\n  Amperage_p1:   \"/Ac/L1/Current\",\n  Amperage_p2:   \"/Ac/L2/Current\",\n  Amperage_p3:   \"/Ac/L3/Current\",\n  Power_curr_p1: \"/Ac/L1/Power\",\n  Power_curr_p2: \"/Ac/L2/Power\",\n  Power_curr_p3: \"/Ac/L3/Power\",\n  frequency:     \"/Ac/Frequency\",\n};\n\nconst out = {};\nfor (const [field, path] of Object.entries(MAP)) {\n  const v = sml[field];\n  if (v !== undefined) out[path] = v;\n}\n\nif (Object.keys(out).length === 0) return null;  // nichts Brauchbares -> keine leere Nachricht\n\nmsg.payload = out;\nreturn msg;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 560,
        "y": 120,
        "wires": [
            [
                "grid_victron",
                "grid_debug"
            ]
        ]
    },
    {
        "id": "grid_victron",
        "type": "victron-virtual",
        "z": "grid_tab_improved",
        "name": "Tasmota",
        "outputs": 1,
        "device": "grid",
        "default_values": true,
        "acload_nrofphases": 1,
        "enable_s2support": false,
        "battery_capacity": 25,
        "include_battery_temperature": false,
        "battery_voltage_preset": "24",
        "battery_voltage_custom": 24,
        "generator_type": "ac",
        "generator_nrofphases": 1,
        "include_engine_hours": false,
        "include_starter_voltage": false,
        "include_history_energy": false,
        "grid_nrofphases": "3",
        "include_motor_temp": false,
        "include_controller_temp": false,
        "include_coolant_temp": false,
        "include_motor_rpm": true,
        "include_motor_direction": true,
        "position": 0,
        "pvinverter_nrofphases": 1,
        "switch_1_type": 1,
        "switch_1_min": 0,
        "switch_1_max": "",
        "switch_1_initial": 0,
        "switch_1_label": "",
        "switch_1_unit": "",
        "switch_1_step": 1,
        "switch_1_customname": "",
        "switch_1_group": "",
        "switch_1_include_measurement": false,
        "switch_1_rgb_color_wheel": false,
        "switch_1_cct_wheel": false,
        "switch_1_rgb_white_dimmer": false,
        "fluid_type": 0,
        "include_tank_battery": false,
        "include_tank_temperature": false,
        "tank_battery_voltage": 3.3,
        "tank_capacity": 0.2,
        "temperature_type": 2,
        "include_humidity": false,
        "include_pressure": false,
        "include_temp_battery": false,
        "temp_battery_voltage": 3.3,
        "x": 830,
        "y": 100,
        "wires": [
            []
        ]
    },
    {
        "id": "grid_debug",
        "type": "debug",
        "z": "grid_tab_improved",
        "name": "debug (im Betrieb aus)",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 840,
        "y": 180,
        "wires": []
    },
    {
        "id": "grid_broker",
        "type": "mqtt-broker",
        "name": "<MQTT Broker Name>",
        "broker": "<IP Adresse des MQTT Brokers>",
        "port": "1883",
        "clientid": "SolarGrid",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": "5",
        "keepalive": "60",
        "cleansession": true,
        "autoUnsubscribe": true,
        "birthTopic": "Solargrid",
        "birthQos": "0",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "willMsg": {},
        "userProps": "",
        "sessionExpiry": "5"
    },
    {
        "id": "grid_globalcfg",
        "type": "global-config",
        "env": [],
        "modules": {
            "@victronenergy/node-red-contrib-victron": "1.6.64"
        }
    }
]
1 „Gefällt mir“

Ich gebe Dir recht, das ist sauber aufgebaut. Chapeau

und wie im Node SML -> dbus (1 Objekt) als Kommentar geschrieben, können die Phase vertauscht werden wenn der Wechselrichter an einer anderen Phase wie L1 hängt

Und ja, ich hatte nur bei den Phase das Mapping zurück genommen. Das war schlampig

Das geht mir zu weit. Du postest Deine Ideen und Umsetzungen. Wir haben alle Erfahrungen und Blickwinkel. Wenn wir dadurch hier gemeinsam bessere Lösungen schaffen, finde ich das eine super Sache.

Darum prinzipiell sehe ich ähnliche Ansätze bei Deiner PV-Inverter Lösung:

  • aktuell 6 Change Nodes vs ich schlage einen Totbandfilter vor
  • aktuell 7 D-Bus Output Nodes vs. ich schlage einen Multi-Value Output vor
  • aktuell 7 writes pro 2 Sekunden vs. ich schlage 1 Batch Write vor nur bei echter Änderung
  • aktuell 1 phasiger PV-Inverter vs. ich schlage 3 phasig vor um die reale Phase auch zuweisen zu können

[
    {
        "id": "09bd67aecf2e3950",
        "type": "shelly-gen2",
        "z": "4eacab1cec7fbe75",
        "g": "ce560e2f782009ef",
        "hostname": "192.168.7.154",
        "description": "shelly1pmminig3",
        "mode": "polling",
        "verbose": false,
        "server": "",
        "outputmode": "event",
        "uploadretryinterval": "",
        "pollinginterval": "5000",
        "pollstatus": true,
        "getstatusoncommand": false,
        "devicetype": "S3SW-001P8EU",
        "devicetypemustmatchexactly": false,
        "captureblutooth": false,
        "outputs": 1,
        "x": 190,
        "y": 180,
        "wires": [
            [
                "a0d6c9289821a447"
            ]
        ]
    },
    {
        "id": "a0d6c9289821a447",
        "type": "function",
        "z": "4eacab1cec7fbe75",
        "g": "ce560e2f782009ef",
        "name": "L2 deadband filter",
        "func": "// ============================================================\n//   L2 Deadband Filter (mit Zero-Crossing-Bypass)\n// ------------------------------------------------------------\n//   Liest Shelly-Werte, vergleicht mit letztem gesendeten\n//   Stand und baut Multi-Value-Payload nur aus Feldern, die\n//   ihre Deadband ueberschritten haben.\n//\n//   Spezialfall: Wechsel zu/von exakt 0 wird IMMER geschrieben,\n//   damit langsam abklingende PV-Leistung sich nicht oberhalb\n//   der Deadband-Schwelle \"festsetzt\" und nachts ein Geist-\n//   Wert im DBus stehen bleibt.\n//\n//   WICHTIG bei mehreren Anlagen: STATE_KEY muss pro Device\n//   eindeutig sein. flow.get/set ist flow(tab)-scoped -- liegen\n//   zwei Devices im selben Tab und teilen sich denselben Key,\n//   ueberschreiben sie gegenseitig ihren Vergleichsstand.\n// ============================================================\n\nconst STATE_KEY = 'lastPvValues_103';   // <-- pro Device eindeutig!\n\nconst DEADBAND = {\n    power:   1,        // W\n    voltage: 3,        // V\n    current: 0.02,     // A\n    energy:  0.001     // kWh\n};\n\nlet s = msg.payload && msg.payload.switch0;\nif (!s) {\n    node.status({ fill: \"yellow\", shape: \"ring\", text: \"no switch0 in payload\" });\n    return null;\n}\n\n// --- Werte extrahieren --------------------------------------\nconst power   = s.apower * -1;            // Vorzeichen-Korrektur (Einbaurichtung!)\nconst voltage = s.voltage;\nconst current = s.current;\nconst energy  = s.aenergy.total / 1000;   // Wh -> kWh\n\n// --- Hilfsfunktion: Zero-Crossing-Erkennung ----------------\nfunction crossesZero(newVal, oldVal) {\n    if (oldVal === undefined) return false;\n    return (newVal === 0 && oldVal !== 0) || (newVal !== 0 && oldVal === 0);\n}\n\n// --- Mit letztem Stand vergleichen --------------------------\nlet last = flow.get(STATE_KEY) || {};\nlet payload = {};\nlet updatedFields = [];\n\nif (last.power === undefined || \n    crossesZero(power, last.power) || \n    Math.abs(power - last.power) >= DEADBAND.power) {\n    payload[\"/Ac/L2/Power\"] = power;\n    payload[\"/Ac/Power\"]    = power;\n    last.power = power;\n    updatedFields.push(\"P\");\n}\n\nif (last.voltage === undefined || Math.abs(voltage - last.voltage) >= DEADBAND.voltage) {\n    payload[\"/Ac/L2/Voltage\"] = voltage;\n    last.voltage = voltage;\n    updatedFields.push(\"U\");\n}\n\nif (last.current === undefined || \n    crossesZero(current, last.current) || \n    Math.abs(current - last.current) >= DEADBAND.current) {\n    payload[\"/Ac/L2/Current\"] = current;\n    last.current = current;\n    updatedFields.push(\"I\");\n}\n\nif (last.energy === undefined || Math.abs(energy - last.energy) >= DEADBAND.energy) {\n    payload[\"/Ac/L2/Energy/Forward\"] = energy;\n    payload[\"/Ac/Energy/Forward\"]    = energy;\n    last.energy = energy;\n    updatedFields.push(\"E\");\n}\n\nif (updatedFields.length === 0) {\n    node.status({\n        fill: \"grey\",\n        shape: \"ring\",\n        text: `idle · ${power.toFixed(1)}W ${voltage.toFixed(0)}V`\n    });\n    return null;\n}\n\nflow.set(STATE_KEY, last);\n\nnode.status({\n    fill: \"green\",\n    shape: \"dot\",\n    text: `→ [${updatedFields.join(\" \")}] · ${power.toFixed(1)}W`\n});\n\nreturn { payload: payload };",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 480,
        "y": 180,
        "wires": [
            [
                "428db88edbdf9b97"
            ]
        ]
    },
    {
        "id": "428db88edbdf9b97",
        "type": "victron-virtual",
        "z": "4eacab1cec7fbe75",
        "g": "131d6a1744361de9",
        "name": "virt. PV Inverter (103)",
        "outputs": 1,
        "device": "pvinverter",
        "default_values": false,
        "enable_s2support": false,
        "battery_capacity": 25,
        "include_battery_temperature": false,
        "battery_voltage_custom": "",
        "include_engine_hours": false,
        "include_starter_voltage": false,
        "include_history_energy": false,
        "grid_nrofphases": 1,
        "include_motor_temp": false,
        "include_controller_temp": false,
        "include_coolant_temp": false,
        "include_motor_rpm": true,
        "include_motor_direction": true,
        "position": "0",
        "pvinverter_nrofphases": "3",
        "fluid_type": 0,
        "include_tank_battery": false,
        "include_tank_temperature": false,
        "tank_battery_voltage": 3.3,
        "tank_capacity": 0.2,
        "temperature_type": 2,
        "include_humidity": false,
        "include_pressure": false,
        "include_temp_battery": false,
        "temp_battery_voltage": 3.3,
        "x": 860,
        "y": 260,
        "wires": [
            []
        ]
    },
    {
        "id": "12489e1066732d54",
        "type": "inject",
        "z": "4eacab1cec7fbe75",
        "g": "344166762fbb145e",
        "name": "auto · once @ startup +15s",
        "props": [
            {
                "p": "trigger",
                "v": "startup",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": "15",
        "topic": "",
        "x": 230,
        "y": 340,
        "wires": [
            [
                "4e3785d470ddaff7"
            ]
        ]
    },
    {
        "id": "51e6d6b68423df11",
        "type": "inject",
        "z": "4eacab1cec7fbe75",
        "g": "344166762fbb145e",
        "name": "manual · re-init defaults",
        "props": [
            {
                "p": "trigger",
                "v": "manual",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": "",
        "topic": "",
        "x": 230,
        "y": 390,
        "wires": [
            [
                "4e3785d470ddaff7"
            ]
        ]
    },
    {
        "id": "4e3785d470ddaff7",
        "type": "function",
        "z": "4eacab1cec7fbe75",
        "g": "344166762fbb145e",
        "name": "L1 & L3 defaults",
        "func": "// ============================================================\n//   L1 & L3 Defaults\n// ------------------------------------------------------------\n//   Shelly haengt einphasig auf L2. Damit das virtual\n//   pvinverter-Device (3-phasig, default_values:false) saubere\n//   Werte fuer alle 3 Phasen liefert, werden L1 und L3 hier\n//   einmalig (startup) oder manuell mit Defaults belegt.\n//\n//   Energy/Forward wird mitgesetzt, damit die Per-Phasen-\n//   Energie im VRM nicht auf \"-\" steht.\n//\n//   Multi-Value-Payload -> 1 DBus-Batch-Write statt 8 einzelne.\n// ============================================================\n\nconst trigger = msg.trigger || \"unknown\";\n\nconst payload = {\n    \"/Ac/L1/Power\":          0,\n    \"/Ac/L1/Voltage\":        230,\n    \"/Ac/L1/Current\":        0,\n    \"/Ac/L1/Energy/Forward\": 0,\n    \"/Ac/L3/Power\":          0,\n    \"/Ac/L3/Voltage\":        230,\n    \"/Ac/L3/Current\":        0,\n    \"/Ac/L3/Energy/Forward\": 0\n};\n\n// Variante kompakt: \"startup · 20.05. 14:23\"\nconst d = new Date();\nconst stamp = `${String(d.getDate()).padStart(2,'0')}.${String(d.getMonth()+1).padStart(2,'0')}. ${d.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'})}`;\n\nnode.status({\n    fill: \"blue\",\n    shape: \"dot\",\n    text: `${trigger} · ${stamp}`\n});\n\nreturn { payload: payload };\n",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 540,
        "y": 365,
        "wires": [
            [
                "428db88edbdf9b97"
            ]
        ]
    },
    {
        "id": "2b264530fab90b80",
        "type": "global-config",
        "env": [],
        "modules": {
            "node-red-contrib-shelly": "11.11.2",
            "@victronenergy/node-red-contrib-victron": "1.6.64"
        }
    }
]