Hello,
First, the concept:
Basic Architecture (final)
┌─────────────────────────┐
│ Cerbo GX │
│ │
│ Node-RED │
│ ├─ Power logic │
│ ├─ Consumer logic │
│ ├─ Grid logic (later) │
│ │
│ Relay 1 → Grid relay │ (later)
│ Relay 2 → Consumers │
└─────────┬───────────────┘
│ MQTT
┌─────────────┴─────────────┐
│ │
┌───────▼────────┐ ┌────────▼─────────┐
│ Shelly 2PM G3 │ │ Shelly 1 Gen3 │
│ (PV inverter │ │ Consumer 2 │
│ power) │ │ │
└───────┬────────┘ └────────┬─────────┘
│ │
2× changeover relays Consumer
(utility / Fronius)
Node-RED flow (configuration & logic)
[
{
"id": "config_globals",
"type": "function",
"name": "CONFIG – System & Annahmen",
"func": "// ======================================================\n// ZENTRALE SYSTEMKONFIGURATION\n// Alles hier ändern – nirgendwo sonst!\n// ======================================================\n\n// ------------------------------\n// MQTT / SYSTEM-ANNAHMEN\n// ------------------------------\n// MQTT-Broker läuft lokal auf dem Cerbo GX\nflow.set('MQTT_BROKER', '127.0.0.1');\nflow.set('MQTT_PORT', 1883);\n\n// Victron Standard MQTT Topics (Venus OS)\n// Falls Victron ein Update macht oder sich Topics ändern,\n// sind sie hier zentral angepasst\nflow.set('TOPIC_SOC', 'N/+/system/0/Dc/Battery/Soc');\nflow.set('TOPIC_AC_LOAD', 'N/+/system/0/Ac/Consumption/Total/Power');\nflow.set('TOPIC_GRID_POWER', 'N/+/system/0/Ac/Grid/Total/Power');\nflow.set('TOPIC_GRID_CONNECTED', 'N/+/system/0/Ac/ActiveIn/Connected');\n\n// Shelly MQTT Topics (Gen3)\nflow.set('TOPIC_SHELLY_PV_R1', 'shellies/shelly-2pm-gen3/relay/0/command');\nflow.set('TOPIC_SHELLY_PV_R2', 'shellies/shelly-2pm-gen3/relay/1/command');\nflow.set('TOPIC_SHELLY_CONSUMER', 'shellies/shelly-1-gen3/relay/0/command');\n\n// ------------------------------\n// BATTERIE / SOC\n// ------------------------------\n// Unterhalb dieses SOC wird die PV-Leistung komplett gesperrt\nflow.set('SOC_MIN', 20);\n\n// SOC, bei dem der PV-Wechselrichter grundsätzlich gedrosselt werden soll\n// (nahe voll, um Laden/Entladen zu vermeiden)\nflow.set('SOC_STOP_PV', 95);\n\n// Sicherheitsabstand für Verbraucherfreigaben\n// Beispiel: STOP_PV 95 % – 5 % = Freigabe ab 90 %\nflow.set('SOC_RELEASE_OFFSET', 5);\n\n// ------------------------------\n// NETZ\n// ------------------------------\n// Erlaubter Netzbezug in Watt\n// 0 = absolut kein Bezug\n// 50–200 W sinnvoll bei großen Verbrauchern\nflow.set('GRID_IMPORT_LIMIT_W', 50);\n\n// SOC, bei dem später (optional) das Netz wieder zugeschaltet wird\n// (für Sommer-Inselbetrieb, aktuell nur dokumentiert)\nflow.set('GRID_RECONNECT_SOC', 15);\n\n// ------------------------------\n// LASTSCHWELLEN (hausintern)\n// ------------------------------\n// Unterhalb: keine PV-Freigabe\nflow.set('LOAD_LOW_W', 1000);\n\n// Ab hier: erste Leistungsstufe (30 %)\nflow.set('LOAD_MEDIUM_W', 3000);\n\n// Ab hier: zweite Leistungsstufe (60 %)\nflow.set('LOAD_HIGH_W', 5000);\n\n// ------------------------------\n// HYSTERESE / TAKTVERMEIDUNG\n// ------------------------------\n// SOC-Hysterese in Prozentpunkten\nflow.set('SOC_HYST', 2);\n\n// Leistungshysterese in Watt\nflow.set('POWER_HYST', 200);\n\n// Mindestzeit zwischen Schaltvorgängen\nflow.set('MIN_SWITCH_TIME_SEC', 60);\n\n// ------------------------------\n// VERBRAUCHERFREIGABEN\n// ------------------------------\n// Verbraucher 1: GX Relais 2 (näher an Grundlast)\nflow.set('CONSUMER1_SOC', flow.get('SOC_STOP_PV') - 5);\n\n// Verbraucher 2: Shelly 1 (größer / optional)\nflow.set('CONSUMER2_SOC', flow.get('SOC_STOP_PV') - 8);\n\n// ======================================================\n// ENDE DER KONFIGURATION\n// ======================================================\nreturn null;",
"x": 300,
"y": 80,
"wires": []
},
{
"id": "mqtt_soc",
"type": "mqtt in",
"name": "SOC",
"topic": "N/+/system/0/Dc/Battery/Soc",
"broker": "broker",
"x": 140,
"y": 160,
"wires": [["store_soc"]]
},
{
"id": "store_soc",
"type": "function",
"name": "Store SOC",
"func": "flow.set('soc', Number(msg.payload));\nreturn null;",
"x": 320,
"y": 160,
"wires": []
},
{
"id": "mqtt_load",
"type": "mqtt in",
"name": "AC Load",
"topic": "N/+/system/0/Ac/Consumption/Total/Power",
"broker": "broker",
"x": 140,
"y": 220,
"wires": [["store_load"]]
},
{
"id": "store_load",
"type": "function",
"name": "Store Load",
"func": "flow.set('load', Number(msg.payload));\nreturn null;",
"x": 320,
"y": 220,
"wires": []
},
{
"id": "mqtt_grid",
"type": "mqtt in",
"name": "Grid Power",
"topic": "N/+/system/0/Ac/Grid/Total/Power",
"broker": "broker",
"x": 140,
"y": 280,
"wires": [["store_grid"]]
},
{
"id": "store_grid",
"type": "function",
"name": "Store Grid",
"func": "flow.set('grid', Number(msg.payload));\nreturn null;",
"x": 320,
"y": 280,
"wires": []
},
{
"id": "mqtt_grid_state",
"type": "mqtt in",
"name": "Grid Connected",
"topic": "N/+/system/0/Ac/ActiveIn/Connected",
"broker": "broker",
"x": 160,
"y": 340,
"wires": [["store_grid_state"]]
},
{
"id": "store_grid_state",
"type": "function",
"name": "Store Grid State",
"func": "flow.set('grid_connected', msg.payload === 1);\nreturn null;",
"x": 350,
"y": 340,
"wires": []
},
{
"id": "tick_pv",
"type": "inject",
"name": "PV Logic Tick (10s)",
"repeat": "10",
"once": true,
"x": 160,
"y": 420,
"wires": [["pv_logic"]]
},
{
"id": "pv_logic",
"type": "function",
"name": "PV Leistungslogik",
"func": "// Liest nur gespeicherte Werte\nconst soc = flow.get('soc') || 0;\nconst load = flow.get('load') || 0;\nconst gridConnected = flow.get('grid_connected');\n\n// Offline → keine EVU-Regelung (Frequenzregelung übernimmt)\nif (!gridConnected) {\n return [{ payload: 'off' }, { payload: 'off' }];\n}\n\n// Akku schützen\nif (soc < flow.get('SOC_MIN')) {\n return [{ payload: 'off' }, { payload: 'off' }];\n}\n\n// Lastgeführte Leistungsstufen\nif (load >= flow.get('LOAD_HIGH_W')) {\n return [{ payload: 'on' }, { payload: 'off' }]; // 60 %\n}\n\nif (load >= flow.get('LOAD_MEDIUM_W')) {\n return [{ payload: 'off' }, { payload: 'on' }]; // 30 %\n}\n\n// Default: keine PV-Freigabe\nreturn [{ payload: 'off' }, { payload: 'off' }];",
"x": 360,
"y": 420,
"wires": [["shelly_pv_r1"], ["shelly_pv_r2"]]
},
{
"id": "shelly_pv_r1",
"type": "mqtt out",
"name": "Shelly 2PM – Relay 1",
"topic": "shellies/shelly-2pm-gen3/relay/0/command",
"broker": "broker",
"x": 640,
"y": 400,
"wires": []
},
{
"id": "shelly_pv_r2",
"type": "mqtt out",
"name": "Shelly 2PM – Relay 2",
"topic": "shellies/shelly-2pm-gen3/relay/1/command",
"broker": "broker",
"x": 640,
"y": 440,
"wires": []
},
{
"id": "tick_consumer",
"type": "inject",
"name": "Consumer Logic Tick (15s)",
"repeat": "15",
"once": true,
"x": 160,
"y": 520,
"wires": [["consumer_logic"]]
},
{
"id": "consumer_logic",
"type": "function",
"name": "Verbraucherfreigabe Logik",
"func": "const soc = flow.get('soc') || 0;\nconst grid = flow.get('grid') || 0;\n\nconst c1 = (soc >= flow.get('CONSUMER1_SOC') && grid <= flow.get('GRID_IMPORT_LIMIT_W')) ? 1 : 0;\nconst c2 = (soc >= flow.get('CONSUMER2_SOC') && grid <= flow.get('GRID_IMPORT_LIMIT_W')) ? 1 : 0;\n\nreturn [{ payload: c1 }, { payload: c2 }];",
"x": 380,
"y": 520,
"wires": [["gx_relay_2"], ["shelly_consumer"]]
},
{
"id": "gx_relay_2",
"type": "victron-output-relay",
"name": "GX Relais 2 – Verbraucher",
"relay": "2",
"x": 640,
"y": 500,
"wires": []
},
{
"id": "shelly_consumer",
"type": "mqtt out",
"name": "Shelly 1 – Verbraucher",
"topic": "shellies/shelly-1-gen3/relay/0/command",
"broker": "broker",
"x": 640,
"y": 540,
"wires": []
},
{
"id": "broker",
"type": "mqtt-broker",
"name": "Cerbo MQTT",
"broker": "127.0.0.1",
"port": "1883"
}
]
Context
System setup
-
3× Victron Multiplus 5000
-
Cerbo GX
-
25 kWh battery
-
Charging via AC-Out using a Fronius IG Plus 100V-3 with Power control card
If the Multiplus 5000 units can reliably supply the required power in all operating states, I will install a grid disconnection relay.
This would allow continuous power control via frequency shifting, which is gentler on the batteries.
However, this will only become clear after about one year of operation.
For winter operation and special cases, the relay-based solution will definitely still be required.
Online operation (grid-connected)
-
Fronius is enabled via contact when:
-
there is load demand, or
-
the battery requires charging
-
-
No feed-in to the public grid
Offline operation (island mode)
-
Fronius fully enabled via contact
-
Power reduction via frequency control
Fronius configuration
-
Control card installed for power steps
-
Intermediate relays installed to enable instead of block
-
Shelly output OFF = inverter OFF
-
Frequency-based power control active
Victron configuration
In VEConfigure:
Grid Code / Assistants
-
Frequency Shift Power Control enabled -
Start at e.g. 50.2 Hz
-
Full curtailment at 52 Hz
Configuration notes:
-
All devices configured identically
-
Master unit on phase L1
Important points:
-
ESS enabled
-
Grid setpoint = 0 W (or slightly positive)
-
Frequency Shift enabled (see above)
-
No feed-in to the grid