This Node-RED flow can be used to control a Victron EV Charging Station (EVCS) from solar surplus using Node-RED on Venus OS. The flow keeps the EVCS in Manual mode and adjusts charge current continuously based on grid meter readings, keeping a configurable buffer flowing to the grid to guarantee no grid power is used for charging.
An override allows forcing full-speed charging from the grid at any time, triggered either by the mode button on the EVCS display or by inject buttons in the Node-RED GUI.
- Victron Cerbo GX (or other GX device) with Venus OS Large installed
- Victron EVCS (EV Charging Station NS or with display). The screen version is preferred, as then the switching to full grid power is easier.
- Grid meter visible to the GX device (tested with HomeWizard Energy P1 via
com.victronenergy.grid) - Node-RED enabled on the GX device (Settings → Venus OS Large → Node-RED)
node-red-contrib-victroninstalled (included with Venus OS Large)
These registers need to be written once and survive reboots. Use the a Modbus TCP tool to write them to the EVCS at its IP address, port 502.
| Register | Name | Value | Note |
|---|---|---|---|
5009 |
Charge mode | 1 (Auto) |
Node-RED overwrites this to 0 (Manual) on every deploy |
5055 |
Manual mode phase | 0 |
0 = 3-phase in the multiple relay charging context. Already correct |
5056 |
Auto mode phase | 2 |
2 = auto 1P/3P switching. Used if EVCS falls back to auto |
5057 |
Power for 3-phase | 3500 W | Phase switching threshold (auto and manual) |
5058 |
Power for single phase | 4200 W | Phase switching threshold |
5059 |
Phase switching timeout | 60 s | Time between phase switches |
5062 |
Min current | 6 A | Hardware minimum, fixed |
5068 |
Backup battery emulation | 1 | If you don't have a battery, the EVCS needs this to auto charge |
5083 |
Available sun power threshold for start (Battery powered) | 1380 W | Safety net: if Node-RED is down and EVCS reverts to auto mode, it won't start below 6 A (6 A × 230 V = 1380 W) of solar |
5100 |
Multiple relay charging | 1 (enabled) |
Required for 3P switching in manual mode |
5101 |
Multiple relay charging relays switching delay | 60 s | Already set correctly |
5102 |
Allow battery/grid power for auto mode | 0 (disabled) |
Prevents grid draw if EVCS falls back to auto |
5103 |
Battery/grid power timeout | 5 min | Auto mode timeout, irrelevant in manual mode |
- Open Node-RED on the Cerbo:
https://venus.local:1881 - Hamburger menu (≡) → Import
- Upload or paste
flows.json - Click Import
- Click Deploy
On deploy, the initialisation group fires 0.5 s after startup and writes Mode = 0 (Manual)
to the EVCS via D-Bus. Verify with a Modbus TCP tool: register 5009 should read 0.
After import, open each Victron input/output node and confirm the correct device is selected. The flow is pre-configured for the devices below; if your service instances differ, update them.
| Node | Device | D-Bus service |
|---|---|---|
Grid /Ac/Power, /Ac/L1-L3/Voltage |
HomeWizard Energy P1 | com.victronenergy.grid/40 |
EVCS /Ac/Power, /Ac/L2/Power, /Status, /Mode, /MaxCurrent |
EVCS | com.victronenergy.evcharger/40 |
EVCS output /Mode, /SetCurrent, /StartStop |
EVCS | com.victronenergy.evcharger/40 |
Constants are stored as environment variables on the flow tab. To edit:
- Double-click the
EVCS Solar Surplustab name - Click the list icon (≡) in the top-right of the dialog
- Edit values under Environment Variables
- Click Done → Deploy to activate the new settings
| Variable | Default | Unit | Description |
|---|---|---|---|
EVCS_BUFFER_W |
200 |
W | Solar surplus kept flowing to grid. Raise if you still see grid draw during charging |
EVCS_MAX_A |
16 |
A | Fallback max current if /MaxCurrent is unavailable. Normally /MaxCurrent is read live from the EVCS |
EVCS_STOP_DELAY_S |
30 |
s | How long surplus must be below 6 A before charging stops |
EVCS_START_DELAY_S |
60 |
s | How long surplus must be above 6 A before charging starts |
EVCS_PHASE_DETECT_W |
50 |
W | EVCS L2 power above this threshold → 3-phase mode detected |
On every D-Bus update from the grid meter and EVCS, the flow calculates:
surplus_W = -(grid_power_W) + evcs_power_W - BUFFER_W
grid_power_Wis negative when exporting → negating gives a positive surplus- Adding back
evcs_power_Wavoids a feedback loop: the EVCS draw is already reflected in the grid reading, so without this correction the flow would under-estimate available solar BUFFER_Wis subtracted to guarantee the grid export never drops to zero
The available charge current is then:
available_A = surplus_W / voltage_sum
Where voltage_sum is V_L1 (1-phase) or V_L1 + V_L2 + V_L3 (3-phase),
read live from the grid meter to get a more correct calculation, instead of assuming a fixed 230V.
3-phase mode is detected from the EVCS L2 power reading:
phases = (evcs_L2_power > EVCS_PHASE_DETECT_W) ? 3 : 1
This is more reliable than a static register read because it reflects what the EVCS
is actually doing right now. The EVCS handles 1P/3P switching autonomously based on
its own power thresholds (registers 5057 and 5058).
To prevent rapid start/stop cycling near the 6 A threshold:
- Stop: surplus must be below 6 A for
EVCS_STOP_DELAY_Sseconds continuously beforeStartStop = 0is sent. While counting down, current is held at 6 A. - Start: surplus must be above 6 A for
EVCS_START_DELAY_Sseconds continuously beforeStartStop = 1is sent.
The controlling code looks like this:
// ── Constants — edit via tab Environment variables ──────────
//
// How to reach them:
// 1. Double-click the "EVCS Solar Surplus" tab name
// 2. Click the list icon (≡) in the top-right of the dialog
// 3. Edit values under "Environment Variables"
// 4. Click Done → Deploy
//
const BUFFER_W = Number(env.get('EVCS_BUFFER_W')) || 200;
const MIN_A = 6; // hardware minimum, fixed (reg 5062)
const MAX_A = Number(env.get('EVCS_MAX_A')) || 16; // fallback if /MaxCurrent unavailable
const STOP_DELAY_MS = (Number(env.get('EVCS_STOP_DELAY_S')) || 30) * 1000;
const START_DELAY_MS = (Number(env.get('EVCS_START_DELAY_S')) || 60) * 1000;
const PHASE_DETECT_W = Number(env.get('EVCS_PHASE_DETECT_W')) || 50; // W above this → 3-phase
// ── Inputs from join ────────────────────────────────────────
const p = msg.payload;
const needed = ['grid_power','v_L1','v_L2','v_L3','evcs_power','evcs_L2_power','evcs_status'];
for (const k of needed) { if (p[k] === undefined) return null; }
const gridW = p.grid_power;
const vL1 = p.v_L1;
const vL2 = p.v_L2;
const vL3 = p.v_L3;
const evcsW = p.evcs_power !== null ? p.evcs_power : 0;
const evcsL2W = p.evcs_L2_power !== null ? p.evcs_L2_power : 0;
const status = p.evcs_status;
const maxCurrent = (p.evcs_max_current !== null && p.evcs_max_current !== undefined)
? p.evcs_max_current : MAX_A;
// ── Mode toggle: detect display button press ─────────────────
// Node-RED always owns Mode=0 (Manual). The EVCS display button
// cycles: Manual(0) → Auto(1) → Scheduled(2) → Manual(0) → ...
// After Node-RED reverts to 0, the next press may land on 1 OR 2
// depending on where the internal cycle counter is.
// Fix: treat ANY non-zero Mode value (not just 1) as the toggle signal.
const modeReading = Number(p.evcs_mode !== null && p.evcs_mode !== undefined ? p.evcs_mode : 0);
const prevMode = context.get('prevMode') !== undefined ? context.get('prevMode') : 0;
let override = flow.get('evcs_override') !== undefined ? flow.get('evcs_override') : false;
let modeRevert = false;
if (modeReading !== 0 && prevMode === 0) {
// Display switched away from Manual — treat as toggle regardless of which mode
override = !override;
flow.set('evcs_override', override);
modeRevert = true;
node.warn('EVCS mode button pressed — Mode=' + modeReading +
' (' + (modeReading === 1 ? 'Auto' : 'Scheduled') + ')' +
' → override ' + (override ? 'ON ⚡' : 'OFF ☀'));
}
context.set('prevMode', modeReading);
// ── Grid charge override ─────────────────────────────────────
if (override) {
const debugOvr = {
mode: '⚡ GRID OVERRIDE — charging at ' + maxCurrent.toFixed(1) + ' A',
maxCurrent: maxCurrent.toFixed(1) + ' A',
status: status,
tip: 'Press mode button on EVCS display, or click ☀ Resume solar inject node to cancel',
};
return [
{ payload: maxCurrent },
{ payload: 1 },
{ payload: debugOvr },
modeRevert ? { payload: 0 } : null,
];
}
// ── No car / fully charged: reset state and exit ────────────
if (status === 0 || status === 3) {
context.set('isStopped', true);
context.set('stopPendingAt', null);
context.set('startPendingAt', null);
return [null, null, null, modeRevert ? { payload: 0 } : null];
}
// ── Phase detection from EVCS L2 power ──────────────────────
const phases = (evcsL2W > PHASE_DETECT_W) ? 3 : 1;
const voltageSum = (phases === 3) ? (vL1 + vL2 + vL3) : vL1;
// ── Surplus calculation ─────────────────────────────────────
const surplusW = -(gridW) + evcsW - BUFFER_W;
let availableA = surplusW / voltageSum;
availableA = Math.max(0, Math.min(maxCurrent, availableA));
// ── Hysteresis state machine ────────────────────────────────
let isStopped = context.get('isStopped') !== undefined ? context.get('isStopped') : true;
let stopPendingAt = context.get('stopPendingAt') !== undefined ? context.get('stopPendingAt') : null;
let startPendingAt = context.get('startPendingAt') !== undefined ? context.get('startPendingAt') : null;
const now = Date.now();
let newStartStop = isStopped ? 0 : 1;
let newCurrent = MIN_A;
if (!isStopped) {
if (availableA < MIN_A) {
if (stopPendingAt === null) { stopPendingAt = now; }
else if (now - stopPendingAt >= STOP_DELAY_MS) {
isStopped = true; newStartStop = 0; stopPendingAt = null;
}
newCurrent = MIN_A;
} else {
stopPendingAt = null;
newCurrent = availableA;
}
} else {
if (availableA >= MIN_A) {
if (startPendingAt === null) { startPendingAt = now; }
else if (now - startPendingAt >= START_DELAY_MS) {
isStopped = false; newStartStop = 1; newCurrent = availableA; startPendingAt = null;
}
} else {
startPendingAt = null;
}
}
context.set('isStopped', isStopped);
context.set('stopPendingAt', stopPendingAt);
context.set('startPendingAt', startPendingAt);
const debug = {
mode: '☀ SOLAR SURPLUS',
gridW: gridW.toFixed(0) + ' W',
evcsW: evcsW.toFixed(0) + ' W',
evcsL2W: evcsL2W.toFixed(0) + ' W',
phases: phases + 'P',
voltageSum: voltageSum.toFixed(1) + ' V',
surplusW: surplusW.toFixed(0) + ' W',
availableA: availableA.toFixed(2) + ' A',
newCurrent: newCurrent.toFixed(2) + ' A',
newStartStop: newStartStop,
isStopped: isStopped,
maxCurrent: maxCurrent.toFixed(1) + ' A',
stopCountdown: stopPendingAt ? Math.round((STOP_DELAY_MS - (now - stopPendingAt)) / 1000) + 's' : null,
startCountdown: startPendingAt ? Math.round((START_DELAY_MS - (now - startPendingAt)) / 1000) + 's' : null,
};
return [
{ payload: newCurrent },
{ payload: newStartStop },
{ payload: debug },
modeRevert ? { payload: 0 } : null,
];
When you need to charge from the grid regardless of solar surplus, the flow
supports a toggle override. The override charges at the installer-configured maximum
current (read live from /MaxCurrent, register 5017), always in 3-phase mode
since 5055 = 0.
There are two ways to activate it:
Mode button on the EVCS display:
The flow monitors /Mode continuously. Node-RED always owns Mode = 0 (Manual), so
every time the display switches away from Manual — to either Auto (1) or Scheduled (2)
— the flow treats this as a toggle signal, flips the override state, and immediately
writes Mode = 0 back.
The EVCS display button cycles internally: Manual → Auto → Scheduled → Manual. After Node-RED reverts to Manual, the button counter continues from where it was, so the next press may land on Auto or Scheduled depending on the cycle position. The flow handles both correctly.
The display briefly shows "Auto" or "Scheduled" for about a second before reverting — this is expected behaviour.
- 1st press → override on → charges at MaxCurrent from grid
- 2nd press → override off → returns to solar surplus control
Each button press is logged in the Node-RED debug sidebar as a warning:
EVCS mode button pressed — Mode=1 (Auto) → override ON ⚡
EVCS mode button pressed — Mode=2 (Scheduled) → override OFF ☀
Inject buttons in Node-RED: The orange-bordered group at the bottom of the canvas contains two clickable buttons:
⚡ Force grid charge→ activates the override☀ Resume solar→ deactivates the override
Both the display button and the inject buttons share the same override state, stored
in flow context (flow.evcs_override). Either method can cancel what the other started.
| D-Bus path | Written when | Value |
|---|---|---|
/Mode |
Once on deploy; also immediately on display toggle | 0 (Manual) |
/SetCurrent |
On change ≥ 0.5 A | Calculated amps (solar mode) or MaxCurrent (override mode) |
/StartStop |
On change | 1 if charging, 0 if stopped |
Open the debug sidebar in Node-RED while the car is charging. The Control state
debug node outputs on every control cycle.
Solar surplus mode:
{
"mode": "☀ SOLAR SURPLUS",
"gridW": "-1850 W",
"evcsW": "2600 W",
"evcsL2W": "870 W",
"phases": "3P",
"voltageSum": "706.2 V",
"surplusW": "2250 W",
"availableA": "11.33 A",
"newCurrent": "11.33 A",
"newStartStop": 1,
"isStopped": false,
"maxCurrent": "16.0 A",
"stopCountdown": null,
"startCountdown": null
}Grid charge override mode:
{
"mode": "⚡ GRID OVERRIDE — charging at 16.0 A",
"maxCurrent": "16.0 A",
"status": 2,
"tip": "Press mode button on EVCS display, or click ☀ Resume solar inject node to cancel"
}Negative gridW = exporting (good). surplusW after subtracting the buffer should
be positive when charging. phases confirms whether 1P or 3P was detected.
| Symptom | Likely cause | Fix |
|---|---|---|
| Flow produces no debug output | Join waiting for initial 7 topics | Check all core input nodes are connected; open each and verify the device is selected |
| EVCS still draws from grid | EVCS_BUFFER_W too low, or surplus spikes during phase switch |
Raise EVCS_BUFFER_W to 300–400 W |
| Charging never starts | Surplus consistently below 6 A × voltage | Wait for better solar conditions; check surplusW in debug |
| Rapid start/stop cycling | Hysteresis delays too short | Raise EVCS_STOP_DELAY_S and EVCS_START_DELAY_S |
| Phase always shows 1P | EVCS not switching to 3P | Check EVCS_PHASE_DETECT_W; lower if EVCS L2 power is just above 50 W |
| Mode button only works one way | EVCS button cycles Manual→Auto→Scheduled; 2nd press may land on Scheduled (2) not Auto (1) | v6c fixes this — any non-Manual mode triggers the toggle |
| Mode reverts to Auto permanently | Node-RED restarted without deploy | Redeploy; or set 5083 to 1380 W as safety net for auto mode fallback |
availableA looks wrong |
Grid meter not updating | Check debounce setting (2000 ms) on gridmeter nodes |
| Value | Meaning |
|---|---|
| 0 | Disconnected |
| 1 | Connected |
| 2 | Charging |
| 3 | Charged |
| 4 | Waiting for sun |
| 20 | Charging limit |
| 21 | Start charging |
| 22 | Switching to 3 phase |
| 23 | Switching to 1 phase |
| 24 | Stop charging |