EVCC charge plan to vrm predicted loads

,

Hey all,

I have heard that there is a way to make the EV charger visible in vrm as seperate load and predicted load. You can actually edit the predicted load which is really cool. I was hoping this will also work for third party EV chargers and i heard that vrm already has an EV charger virtual device in the beta version of Venus OS.

It would be really cool to make a node-RED script that gets the charging plan of the charger via EVCC and MQTT and puts it in as a predicted load. Do you guys think that would be possible? The plan is accesable and the payload looks like this:

plan = [{"start":"2026-07-01T10:45:00+02:00","end":"2026-07-01T11:00:00+02:00","value":0.11101000000000001},{"start":"2026-07-01T11:15:00+02:00","end":"2026-07-01T11:30:00+02:00","value":0.11605}

I hope Victron makes this possible in vrm to overwrite the prediction via node-RED. The node-RED function itself is probably pretty easy.

Just some thoughts, maybe people are also interested in this project and share ideas :slight_smile:

Hi Arjen,

I’m not only interested in this; I’ve already started working on it. The standard assumptions made by VRM do not fully align with reality, which, in my opinion, means that DESS is not operating as optimally as it could.

While setting up the flow, I ran into the issue that simply sending the EV forecast to VRM via the API is not sufficient. This caused several problems, as it also resulted in the base consumption being adjusted.

I now have the following working:

  1. Every 15 minutes, an HTTP request is sent to http://EVCC-ip:7070/api/loadpoints/1/plan. The EVCC response is stored in flow.evccPlan.
  2. Subsequently, the total consumption forecast, EV consumption forecast (VRM), and HP consumption forecast are retrieved from the VRM API and stored as flow.vrmForecast.
  3. The function below converts the EVCC charging plan into the VRM format. The updated EV, HP, and BASE forecasts are then sent back to VRM for the next 48 hours. I have also added a msg.reset for testing purposes.

This approach allows me to update the EV forecast from EVCC while keeping the HP and BASE forecasts unchanged.

If you have any feedback, please let me know

[
    {
        "id": "f930cb0bb5a29883",
        "type": "inject",
        "z": "9b566e92c3b55369",
        "name": "Full reset",
        "props": [
            {
                "p": "reset",
                "v": "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48]",
                "vt": "json"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "x": 1760,
        "y": 600,
        "wires": [
            [
                "4845cf986f1e010d"
            ]
        ]
    },
    {
        "id": "142d0d9667bb2a61",
        "type": "function",
        "z": "9b566e92c3b55369",
        "name": "EVCC naar DESS Forecast",
        "func": "// Haal beide inputs op uit de flow context geheugens\nlet evccRaw = flow.get(\"evccPlan\");\nlet vrmData = flow.get(\"vrmForecast\");\n\n// ---------------------------------------------------------\n// 1. Veiligheidscontrole: Is alle data aanwezig?\n// ---------------------------------------------------------\nif (!evccRaw) {\n    node.error(\"Wachten op evcc data... Nog niet gevonden in flow.evccPlan\", msg);\n    return null;\n}\nif (!vrmData || !vrmData.records) {\n    node.error(\"Wachten op VRM data... Nog niet gevonden in flow.vrmForecast\", msg);\n    return null;\n}\n\n// ---------------------------------------------------------\n// 2. EVCC JSON-string omzetten naar object indien nodig\n// ---------------------------------------------------------\nlet evccData = evccRaw;\nif (typeof evccData === \"string\") {\n    try {\n        evccData = JSON.parse(evccData);\n    } catch (e) {\n        node.error(\"Ongeldige JSON-string in flow.evccPlan\", msg);\n        return null;\n    }\n}\n\nif (!evccData.plan || !Array.isArray(evccData.plan)) {\n    node.error(\"Geen 'plan' array gevonden in evcc data\", msg);\n    return null;\n}\n\n// evcc /plan API bevat meestal geen live power, we pakken 11000W (11kW) als veilige fallback\nconst evPower = Number(evccData.power) || 11000;\n\n// ---------------------------------------------------------\n// 3. VRM Data indexeren op basis van Unix Timestamp (ms)\n// ---------------------------------------------------------\nconst vrmMap = {}; \nconst records = vrmData.records;\n\nif (records.vrm_consumption_fc) {\n    records.vrm_consumption_fc.forEach(item => {\n        const ts = item[0];\n        if (!vrmMap[ts]) vrmMap[ts] = { total: 0, ev: 0, hp: 0 };\n        vrmMap[ts].total = item[1];\n    });\n}\nif (records.vrm_consum_evcs_fc) {\n    records.vrm_consum_evcs_fc.forEach(item => {\n        const ts = item[0];\n        if (!vrmMap[ts]) vrmMap[ts] = { total: 0, ev: 0, hp: 0 };\n        vrmMap[ts].ev = item[1];\n    });\n}\nif (records.vrm_consum_hp_fc) {\n    records.vrm_consum_hp_fc.forEach(item => {\n        const ts = item[0];\n        if (!vrmMap[ts]) vrmMap[ts] = { total: 0, ev: 0, hp: 0 };\n        vrmMap[ts].hp = item[1];\n    });\n}\n\n// ---------------------------------------------------------\n// 4. Huidige tijd bepalen voor offset berekening\n// ---------------------------------------------------------\nconst now = new Date();\nconst currentHour = new Date(now);\ncurrentHour.setMinutes(0, 0, 0);\n\n// ---------------------------------------------------------\n// 5. EVCC Laadplanning koppelen aan VRM Timestamps\n// ---------------------------------------------------------\nconst evccMap = {}; \n\nfor (const slot of evccData.plan) {\n    const start = new Date(slot.start).getTime();\n    const end = new Date(slot.end).getTime();\n\n    if (end <= now.getTime()) {\n        continue;\n    }\n\n    const effectiveStart = Math.max(start, now.getTime());\n\n    for (const tsStr in vrmMap) {\n        const ts = Number(tsStr);\n        const hourStart = ts;\n        const hourEnd = ts + 3600000; \n\n        const overlapStart = Math.max(effectiveStart, hourStart);\n        const overlapEnd = Math.min(end, hourEnd);\n        const minutes = Math.max(0, (overlapEnd - overlapStart) / 60000);\n\n        if (minutes > 0) {\n            const averagePowerInHour = (evPower * minutes) / 60;\n            if (!evccMap[ts]) evccMap[ts] = 0;\n            evccMap[ts] += averagePowerInHour;\n        }\n    }\n}\n\n// ---------------------------------------------------------\n// 6. DESS Output Array opbouwen\n// ---------------------------------------------------------\nconst output = [];\n\nfor (const tsStr in vrmMap) {\n    const ts = Number(tsStr);\n    const dataHour = vrmMap[ts];\n\n    const offset = Math.round((ts - currentHour.getTime()) / 3600000);\n\n    // Alleen toekomstige uren verwerken\n    if (offset >= 1) {\n        // Formule: base = totaal - ev - hp\n        let calculatedBase = dataHour.total - dataHour.ev - dataHour.hp;\n        calculatedBase = Math.max(0, calculatedBase); // Nooit negatief\n\n        // Bepaal de EV waarde op basis van de evcc match\n        const finalEv = evccMap[ts] ? evccMap[ts] : 0;\n\n        output.push({\n            offset: offset,\n            ev: Math.round(finalEv),\n            hp: Math.round(dataHour.hp),\n            base: Math.round(calculatedBase)\n        });\n    }\n}\n\n// Sorteren op netste volgorde van uren\noutput.sort((a, b) => a.offset - b.offset);\n\n// Geef een volledig schoon bericht terug om Node-RED waarschuwingen te voorkomen\nreturn {\n    payload: output\n};\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1720,
        "y": 520,
        "wires": [
            [
                "4845cf986f1e010d"
            ]
        ]
    }
]

Hi Johan,

This is really interesting, thanks for sharing!

We still have 3 months to test before net metering ends :wink:

Was this flow created before or after the update that split the heat pump and EV out in VRM?

I already have two NodeRED flows that pass on the current consumption of the EV and heat pump. I’m hoping that will help VRM make better predictions.

Best,

Arjan

I can already see that it works with the HP, EV and baseload. One thing I was still wondering: why do you calculate the baseload?

let calculatedBase = dataHour.total - dataHour.ev - dataHour.hp;

Isn’t that a property of dataHour?

I’ll test it soon and let you know how it goes.

I think I’ll let VRM run for a while first, since I’ve only just started passing the EV and HP through as energy meters.

It seems to me that it would be good to let it learn on its own for a while first.

I got it working, this is really brilliant!

I haven’t been able to test it extensively yet, but I can already see the prediction appearing in VRM, so that’s quite something :slight_smile:

I’ll keep you posted on what I find.

Hi Arjen,

Thanks for your message. I’m afraid I hadn’t seen it sooner.
In my first version, I only forwarded the EV forecast to the VRM, but that went wrong. The total sum wasn’t adjusted, which affected the baseload. That’s why I calculate the EV-HP and baseload before sending them to the VRM.

Hi Johan,

Ah, I understand now. I also had a look at the smart price limit in evcc, and unfortunately it isn’t published to MQTT.

If I only enable the price limit, Plan continues to return null in MQTT.

I’ve already started a discussion on the evcc GitHub to get this added in the future.

In principle, you could add this in NodeRED, but you’d have to create a completely new eccPlan based on effectiveMaxCurrent, smartCostLimit, vehicleSoc and effectiveLimitSoc, and possibly merge it with the normal plan. That seems a bit cumbersome and prone to errors to me.

Here’s the link to the GitHub discussion: Smart Price Limit plan into a Plan in MQTT · evcc-io/evcc · Discussion #34319 · GitHub

I’ve made one small addition so that only the changed offsets are sent to vrm. I still get the feeling that if you overwrite 48 hours every day, vrm becomes less flexible when forecasts change. I only make a change if EV differs from the forecast.

// Alleen toekomstige uren verwerken
    if (offset >= 1) {

        // EV waarde die momenteel in VRM staat
        const originalEv = Math.round(dataHour.ev);

        // Nieuwe door EVCC berekende EV waarde
        const newEv = evccMap[ts]
            ? Math.round(evccMap[ts])
            : 0;

        // Alleen sturen als EV daadwerkelijk veranderd is
        if (newEv !== originalEv) {

            let calculatedBase =
                dataHour.total -
                dataHour.ev -
                dataHour.hp;

            calculatedBase = Math.max(0, calculatedBase);

            output.push({
                offset: offset,
                ev: newEv,
                hp: Math.round(dataHour.hp),
                base: Math.round(calculatedBase)
            });
        }
    }