Using the Forecast for charging batteries in advance

Hello All. I ahve an idea on trying to limit my consumption during the high-price hours.

I have Multiplus II system, with 20Kw battery storage.

Im’ looking on how I can use in NodeRED the Forecast.

the idea is: When at midnight (roughly), the system should check what the Forecast for solar will be the next day. When this is more then 20KW, then do nothing.

When the forecasts is between 10 & 19KW, then I want to charge my batteries from the grid op to certain SoC of 50%

When the forecasts is between 0 & 10KW, then I want to charge my batteries from the grid op to certain SoC of 750%

I don’t know how to use the ‘foracast’.

but also, I have no idea how I can activate the ‘charger’ function in mu MP II devices.

I now have the charger ‘scheduled’ via the GX remote device, but how can I start and stop the MP II charger?

In a nutshell, if you are using ESS like I do:
I get the data I need with the VRM API node at 22:00.
Next I change the Grid Setpoint so the battery starts charging.
When the SoC is reached, I disable charging.
At 07:00 I set the Grid Setpoint at zero again and allow charging.

Here you can see my Node-Red in action.

Red line is the grid. Blue line household and the green line the battery.

At 22:00 I start charging the battery.
At 23:00 the dishwasher starts running. Power comes from the grid and the battery.
As soon as the load drops the battery starts charging again. But the grid remains a flat line.
When SoC is reached I keep feeding my houseload from the grid until 07:00.

To which gridpoint value?

Daytime -50, at night 2100.

OK, thanks. :slightly_smiling_face:
This high gridpoint value forces the MPs to go into charge mode.

In the past for special purposes I have set the “Minimum SOC” to higher values than the current battery SOC to trigger the charging. Good to know: A high gridpoint makes the same.

Hi Diederick, sorry for my late reply (holidays have been holding me vack :slight_smile: )

could you share me how to configur this in Node-Red. I have the ideas for optimalising my production/usage, but I have practically no knowledge of Node-RED.

in worst case, but this is to bold, I could setup a connection so you could access it :slight_smile:

so, indeed, when just before midnight (23:30 for example), the ‘forecast’ of next day is:

lower then 10KW, then charge battery from grid up to SoC 75%

when between 10-20KW, then charge battery to 50%,

when more then 20KW, then charge to SoC 30%

I have no idea how the Node-Red syntax and info is to be configured.

Anyway, thank you very much in advance already.

I mainly use the function node which is basically Javascript.

My approach is different.
Suppose a dark day without any yield from the panels is coming, then I will need 80% battery charge at 7:00 AM.
At 10:00 PM I get my predictions and calculate what I need.

I pull data from VRM for the days ahead, as well as track performance during the day.
The forecast gets more accurate as the day goes on.
I am not using this currently to action anything, but intend to use the remaining PV forecast and current SOC to trigger manual charging so a minimum level is reached by sundown.



this is kind of what I also want.

I never charge my battery from the grid, but since I have 5.4 kWp solar installed and have a single 1phase MP2-3000 with a maximum inverter power of 2400 W, I need to use my battery as a buffer to feed in all excess solar energy to the grid. I use a Node Red flow to do this. You can probably modify this flow according to your needs.

This Node Red flow uses forecast for today, and tomorrow, and the forecast from now to the end of the day, and the current SOC to modify the grid setpoint every hour (and differently for each month of the year). This flow also changes the minSOC value every month between 20% in summer and 40% in winter.

You need to set the global variable “global.vrmSiteId” to your individual VRM Site Id. I do this in an extra flow.

[
    {
        "id": "cecfa760836ea314",
        "type": "inject",
        "z": "1459d4d7551ca112",
        "name": "execute_every_1h",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "3600",
        "crontab": "",
        "once": true,
        "onceDelay": "0.5",
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 170,
        "y": 140,
        "wires": [
            [
                "83e1731df2cb4a4f",
                "4509958d36211e97",
                "58bf60b68439ec53"
            ]
        ]
    },
    {
        "id": "83e1731df2cb4a4f",
        "type": "vrm-api",
        "z": "1459d4d7551ca112",
        "vrm": "cca5d0a225796a2b",
        "name": "forecast_today",
        "api_type": "installations",
        "idUser": "",
        "users": "",
        "idSite": "{{global.vrmSiteId}}",
        "installations": "stats",
        "attribute": "solar_yield_forecast",
        "stats_interval": "15mins",
        "show_instance": false,
        "stats_start": "bod",
        "stats_end": "eod",
        "use_utc": false,
        "gps_start": "",
        "gps_end": "",
        "widgets": "",
        "instance": "",
        "vrm_id": "",
        "country": "",
        "b_max": "",
        "tb_max": "",
        "fb_max": "",
        "tg_max": "",
        "fg_max": "",
        "b_cycle_cost": "",
        "buy_price_formula": "",
        "sell_price_formula": "",
        "green_mode_on": "",
        "feed_in_possible": "",
        "feed_in_control_on": "",
        "b_goal_hour": "",
        "b_goal_SOC": "",
        "store_in_global_context": false,
        "verbose": false,
        "x": 380,
        "y": 80,
        "wires": [
            [
                "b6d55740f5a71d7d"
            ]
        ]
    },
    {
        "id": "9a0e3d6e3a04fda2",
        "type": "change",
        "z": "1459d4d7551ca112",
        "name": "Korrekturfaktor",
        "rules": [
            {
                "t": "set",
                "p": "forecast_tomorrow",
                "pt": "flow",
                "to": "0.9*payload.totals.solar_yield_forecast",
                "tot": "jsonata"
            },
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "forecast_tomorrow",
                "tot": "flow"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 600,
        "y": 200,
        "wires": [
            [
                "bc15c2dd4db42c81"
            ]
        ]
    },
    {
        "id": "b6d55740f5a71d7d",
        "type": "change",
        "z": "1459d4d7551ca112",
        "name": "Korrekturfaktor",
        "rules": [
            {
                "t": "set",
                "p": "forecast_today",
                "pt": "flow",
                "to": "0.9*payload.totals.solar_yield_forecast",
                "tot": "jsonata"
            },
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "forecast_today",
                "tot": "flow"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 600,
        "y": 80,
        "wires": [
            [
                "ce10e89d55ef11a6"
            ]
        ]
    },
    {
        "id": "53ec77049a4d0973",
        "type": "victron-input-battery",
        "z": "1459d4d7551ca112",
        "service": "com.victronenergy.battery/512",
        "path": "/Soc",
        "serviceObj": {
            "service": "com.victronenergy.battery/512",
            "name": "Batterie"
        },
        "pathObj": {
            "path": "/Soc",
            "type": "float",
            "name": "State of charge (%)"
        },
        "name": "currentSOC",
        "onlyChanges": true,
        "roundValues": "0",
        "x": 190,
        "y": 300,
        "wires": [
            [
                "e2356f404424d7e5"
            ]
        ]
    },
    {
        "id": "f2a2c02c8c22031a",
        "type": "inject",
        "z": "1459d4d7551ca112",
        "name": "execute-oncedaily",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "11 00 * * *",
        "once": true,
        "onceDelay": "0.6",
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 170,
        "y": 400,
        "wires": [
            [
                "07f9999728e60555"
            ]
        ]
    },
    {
        "id": "4509958d36211e97",
        "type": "vrm-api",
        "z": "1459d4d7551ca112",
        "vrm": "cca5d0a225796a2b",
        "name": "forecast_tomorrow",
        "api_type": "installations",
        "idUser": "",
        "users": "",
        "idSite": "{{global.vrmSiteId}}",
        "installations": "stats",
        "attribute": "solar_yield_forecast",
        "stats_interval": "15mins",
        "show_instance": false,
        "stats_start": "bot",
        "stats_end": "86400",
        "use_utc": false,
        "gps_start": "",
        "gps_end": "",
        "widgets": "",
        "instance": "",
        "vrm_id": "",
        "country": "",
        "b_max": "",
        "tb_max": "",
        "fb_max": "",
        "tg_max": "",
        "fg_max": "",
        "b_cycle_cost": "",
        "buy_price_formula": "",
        "sell_price_formula": "",
        "green_mode_on": "",
        "feed_in_possible": "",
        "feed_in_control_on": "",
        "b_goal_hour": "",
        "b_goal_SOC": "",
        "store_in_global_context": false,
        "verbose": false,
        "x": 390,
        "y": 200,
        "wires": [
            [
                "9a0e3d6e3a04fda2"
            ]
        ]
    },
    {
        "id": "07f9999728e60555",
        "type": "function",
        "z": "1459d4d7551ca112",
        "name": "minSOC_saison",
        "func": "// Aktuelles Datum abrufen\n// const d = new Date();\n// let cmonth = d.getMonth(); // 0-11\n// Monat um 1 erhöhen, um 1 bis 12 zu erhalten:\n// cmonth++;\n// let currentdate = flow.get('cdatearray');\n// let cmonth = currentdate[0];\n// let cmonth = flow.get('cmonth');\n\nlet d = new Date();\nlet cmonth = 1 + d.getMonth();\nlet cday = d.getDate();\n\n// Berechnung minSOC nach Jahreszeit (Nordhalbkugel)\n// Winter 45%: November (11), Dezember (12), Januar (1), Februar (2)\n// Frühling, Herbst 30%: März (3), April (4), September (9), Oktober (10)\n// Sommer 20%: Mai (5), Juni (6), Juli (6), August (8)\n\nswitch (cmonth) {\n  case 1:\n      minSOC = 40;\n      break;\n  case 2:\n      minSOC = 40;\n      break;\n  case 3:\n      if (cday < 15) {\n          minSOC = 35;\n      } else if (true) {\n          minSOC = 30;\n      }\n      break;\n  case 4:\n      minSOC = 25;\n      break;\n  case 5:\n      minSOC = 20;\n      break;\n  case 6:\n      minSOC = 20;\n      break;\n  case 7:\n      minSOC = 20;\n      break;\n  case 8:\n      minSOC = 20;\n      break;\n  case 9:\n      minSOC = 25;\n      break;\n  case 10:\n      if (cday < 15) {\n          minSOC = 30;\n      } else if (true) {\n          minSOC = 35;\n      }\n      break;\n  case 11:\n      minSOC = 40;\n      break;\n  case 12:\n      minSOC = 40;\n      break;\n  default:\n      minSOC = 40;\n}\n\n// als flow-variablen setzen, um sie weiterhin nutzen zu können\nflow.set ('minSOC' , minSOC);\n\n// Nachricht setzen\nmsg.payload = minSOC;\n// Optional: minSOC auch als Payload für Schalter senden\n// msg.minSOC = minSOC; \n\nreturn msg;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 380,
        "y": 400,
        "wires": [
            [
                "9b375ebef613ee7c"
            ]
        ]
    },
    {
        "id": "ce10e89d55ef11a6",
        "type": "debug",
        "z": "1459d4d7551ca112",
        "name": "forecast_today",
        "active": true,
        "tosidebar": false,
        "console": false,
        "tostatus": true,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "payload",
        "statusType": "auto",
        "x": 800,
        "y": 80,
        "wires": []
    },
    {
        "id": "bc15c2dd4db42c81",
        "type": "debug",
        "z": "1459d4d7551ca112",
        "name": "forecast_tomorrow",
        "active": true,
        "tosidebar": false,
        "console": false,
        "tostatus": true,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "payload",
        "statusType": "auto",
        "x": 810,
        "y": 200,
        "wires": []
    },
    {
        "id": "9b375ebef613ee7c",
        "type": "victron-output-ess",
        "z": "1459d4d7551ca112",
        "service": "com.victronenergy.settings",
        "path": "/Settings/CGwacs/BatteryLife/MinimumSocLimit",
        "serviceObj": {
            "service": "com.victronenergy.settings",
            "name": "Venus settings"
        },
        "pathObj": {
            "path": "/Settings/CGwacs/BatteryLife/MinimumSocLimit",
            "type": "integer",
            "name": "Minimum Discharge SOC (%)",
            "mode": "both"
        },
        "initial": 20,
        "name": "set-MinSOC",
        "onlyChanges": false,
        "x": 590,
        "y": 400,
        "wires": []
    },
    {
        "id": "58bf60b68439ec53",
        "type": "vrm-api",
        "z": "1459d4d7551ca112",
        "vrm": "cca5d0a225796a2b",
        "name": "forecast_restofday",
        "api_type": "installations",
        "idUser": "",
        "users": "",
        "idSite": "{{global.vrmSiteId}}",
        "installations": "stats",
        "attribute": "solar_yield_forecast",
        "stats_interval": "15mins",
        "show_instance": false,
        "stats_start": "0",
        "stats_end": "eod",
        "use_utc": false,
        "gps_start": "",
        "gps_end": "",
        "widgets": "",
        "instance": "",
        "vrm_id": "",
        "country": "",
        "b_max": "",
        "tb_max": "",
        "fb_max": "",
        "tg_max": "",
        "fg_max": "",
        "b_cycle_cost": "",
        "buy_price_formula": "",
        "sell_price_formula": "",
        "green_mode_on": "",
        "feed_in_possible": "",
        "feed_in_control_on": "",
        "b_goal_hour": "",
        "b_goal_SOC": "",
        "store_in_global_context": false,
        "verbose": false,
        "x": 390,
        "y": 140,
        "wires": [
            [
                "4e3e0b44fd6a8109"
            ]
        ]
    },
    {
        "id": "4e3e0b44fd6a8109",
        "type": "change",
        "z": "1459d4d7551ca112",
        "name": "Korrekturfaktor",
        "rules": [
            {
                "t": "set",
                "p": "forecast_restofday",
                "pt": "flow",
                "to": "0.9*payload.totals.solar_yield_forecast",
                "tot": "jsonata"
            },
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "forecast_restofday",
                "tot": "flow"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 600,
        "y": 140,
        "wires": [
            [
                "4d003d0b786480b7"
            ]
        ]
    },
    {
        "id": "4d003d0b786480b7",
        "type": "debug",
        "z": "1459d4d7551ca112",
        "name": "forecast_restofday",
        "active": true,
        "tosidebar": false,
        "console": false,
        "tostatus": true,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "payload",
        "statusType": "auto",
        "x": 810,
        "y": 140,
        "wires": []
    },
    {
        "id": "2cc09b109a7328c4",
        "type": "inject",
        "z": "1459d4d7551ca112",
        "name": "execute-everyhour",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "3600",
        "crontab": "",
        "once": true,
        "onceDelay": "0.6",
        "topic": "",
        "payload": "20",
        "payloadType": "num",
        "x": 180,
        "y": 480,
        "wires": [
            [
                "5096be16a5dae81c"
            ]
        ]
    },
    {
        "id": "5096be16a5dae81c",
        "type": "function",
        "z": "1459d4d7551ca112",
        "name": "calc_gridsetpoint",
        "func": "let forecast_today = flow.get ('forecast_today');\nlet forecast_tomorrow = flow.get ('forecast_tomorrow');\nlet currentsoc = flow.get ('currentsoc');\nlet minSOC = flow.get ('minSOC') || 20;\nlet missingload = flow.get ('missingload') || 0;\nlet fcrest = flow.get ('flow.forecast_restofday');\n\nlet d = new Date();\nlet cmonth = 1 + d.getMonth();\nlet cday = d.getDate();\nlet chour = d.getHours();\nlet cminute = d.getMinutes();\n\nlet gridsetpoint = 20;\n\nif (cmonth === 5 || cmonth === 6 || cmonth === 7 || cmonth === 8) {\n    if ((17 <= chour) && (chour < 20)) { // zwischen 17 und 20 Uhr voll laden\n        gridsetpoint = -20;\n    } else if ((20 <= chour) && (45 < currentsoc) && (8000 < forecast_tomorrow)) {\n        gridsetpoint = -1000;\n    } else if ((chour <= 7) && (35 < currentsoc) && (8000 < forecast_today)) {\n        gridsetpoint = -1000;\n    } else if ((7 < chour) && (chour < 14) && (60 < currentsoc) && (6000 < fcrest)) {\n        gridsetpoint = -1400;\n    } else if ((14 <= chour) && (chour < 17) && (80 < currentsoc)  && (6000 < fcrest)) {\n        gridsetpoint = -1800;\n    } else {\n        gridsetpoint = -20;\n    }\n} else if (cmonth === 4 || cmonth === 9) {\n    if ((16 <= chour) && (chour < 20)) { // zwischen 16 und 20 Uhr voll laden\n        gridsetpoint = -20;\n    } else if ((20 <= chour) && (55 < currentsoc) && (8000 < forecast_tomorrow)) {\n        gridsetpoint = -600;\n    } else if ((chour < 7) && (50 < currentsoc) && (8000 < forecast_today)) {\n        gridsetpoint = -600;\n    } else if ((7 <= chour) && (chour < 12) && (50 < currentsoc) && (8000 < fcrest)) {\n        gridsetpoint = -800;\n    } else if ((12 <= chour) && (chour < 16) && (80 < currentsoc) && (6000 < fcrest)) {\n        gridsetpoint = -1000;\n    } else {\n        gridsetpoint = -20;\n    }\n} else if (cmonth === 3 || cmonth === 10) {\n    if ((16 <= chour) && (chour < 20)) { // zwischen 16 und 20 Uhr voll laden\n        gridsetpoint = -20;\n    } else if ((20 <= chour) && (60 < currentsoc) && (10000 < forecast_tomorrow)) {\n        gridsetpoint = -600;\n    } else if ((chour <= 7) && (55 < currentsoc) && (10000 < forecast_today)) {\n        gridsetpoint = -600;\n    } else if ((7 < chour) && (chour < 12) && (60 < currentsoc) && (10000 < fcrest)) {\n        gridsetpoint = -800;\n    } else if ((12 <= chour) && (chour < 16) && (80 < currentsoc) && (6000 < fcrest)) {\n        gridsetpoint = -1000;\n    } else {\n        gridsetpoint = -20;\n    }\n} else if (true) {\n        gridsetpoint = 20;\n    }\n\nflow.set ('gridsetpoint' , gridsetpoint);\n\nmsg.payload = gridsetpoint;\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 390,
        "y": 480,
        "wires": [
            [
                "62cff6b823712e53"
            ]
        ]
    },
    {
        "id": "62cff6b823712e53",
        "type": "victron-output-ess",
        "z": "1459d4d7551ca112",
        "service": "com.victronenergy.settings",
        "path": "/Settings/CGwacs/AcPowerSetPoint",
        "serviceObj": {
            "service": "com.victronenergy.settings",
            "name": "Venus settings"
        },
        "pathObj": {
            "path": "/Settings/CGwacs/AcPowerSetPoint",
            "type": "integer",
            "name": "Grid set-point (W)",
            "mode": "both"
        },
        "initial": 20,
        "name": "set-GridSetpoint",
        "onlyChanges": false,
        "x": 600,
        "y": 480,
        "wires": []
    },
    {
        "id": "e2356f404424d7e5",
        "type": "function",
        "z": "1459d4d7551ca112",
        "name": "set_flowSOC",
        "func": "let currentSOC = msg.payload;\nlet minSOC = flow.get ('minSOC') || 40;\nlet diffSOC = 100 - currentSOC;\nconst fullbattery = 14000;\nlet missingload = diffSOC * fullbattery / 100;\n\nflow.set ('missingload', missingload);\nflow.set ('currentSOC', currentSOC);\n\nmsg.payload = currentSOC;\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 370,
        "y": 300,
        "wires": [
            []
        ]
    },
    {
        "id": "7e94cf2671a9b2dd",
        "type": "function",
        "z": "1459d4d7551ca112",
        "name": "calc_gridsetpoint_demo",
        "func": "let forecast_today = 11000; //= flow.get ('forecast_today');\nlet forecast_tomorrow = 11000; // = flow.get ('forecast_tomorrow');\nlet currentsoc = 65; // = flow.get ('currentsoc');\nlet minSOC = 20; // flow.get ('minSOC') || 20;\nlet missingload = 8000; //flow.get ('missingload') || 0;\nlet fcrest = 11000; // flow.get ('flow.forecast_restofday');\n\n// let d = new Date();\nlet cmonth = 3; // 1 + d.getMonth();\n// let cday = d.getDate();\nlet chour = 6; // = d.getHours();\n// let cminute = d.getMinutes();\n\nlet gridset = 19;\n\nif (cmonth === 5 || cmonth === 6 || cmonth === 7 || cmonth === 8) {\n    if ((17 <= chour) && (chour < 20)) { // zwischen 17 und 20 Uhr voll laden\n        gridsetpoint = -20;\n    } else if ((20 <= chour) && (45 < currentsoc) && (8000 < forecast_tomorrow)) {\n        gridsetpoint = -1000;\n    } else if ((chour <= 7) && (35 < currentsoc) && (8000 < forecast_today)) {\n        gridsetpoint = -1000;\n    } else if ((7 < chour) && (chour < 14) && (60 < currentsoc) && (6000 < fcrest)) {\n        gridsetpoint = -1400;\n    } else if ((14 <= chour) && (chour < 17) && (80 < currentsoc)  && (6000 < fcrest)) {\n        gridsetpoint = -1800;\n    } else {\n        gridsetpoint = -20;\n    }\n} else if (cmonth === 4 || cmonth === 9) {\n    if ((16 <= chour) && (chour < 20)) { // zwischen 16 und 20 Uhr voll laden\n        gridsetpoint = -20;\n    } else if ((20 <= chour) && (55 < currentsoc) && (8000 < forecast_tomorrow)) {\n        gridsetpoint = -600;\n    } else if ((chour < 7) && (50 < currentsoc) && (8000 < forecast_today)) {\n        gridsetpoint = -600;\n    } else if ((7 <= chour) && (chour < 12) && (50 < currentsoc) && (8000 < fcrest)) {\n        gridsetpoint = -800;\n    } else if ((12 <= chour) && (chour < 16) && (80 < currentsoc) && (6000 < fcrest)) {\n        gridsetpoint = -1000;\n    } else {\n        gridsetpoint = -20;\n    }\n} else if (cmonth === 3 || cmonth === 10) {\n    if ((16 <= chour) && (chour < 20)) { // zwischen 16 und 20 Uhr voll laden\n        gridsetpoint = -20;\n    } else if ((20 <= chour) && (60 < currentsoc) && (10000 < forecast_tomorrow)) {\n        gridsetpoint = -600;\n    } else if ((chour <= 7) && (55 < currentsoc) && (10000 < forecast_today)) {\n        gridsetpoint = -600;\n    } else if ((7 < chour) && (chour < 12) && (60 < currentsoc) && (10000 < fcrest)) {\n        gridsetpoint = -800;\n    } else if ((12 <= chour) && (chour < 16) && (80 < currentsoc) && (6000 < fcrest)) {\n        gridsetpoint = -1000;\n    } else {\n        gridsetpoint = -20;\n    }\n} else if (true) {\n        gridsetpoint = 20;\n    }\n\n\nflow.set ('gridsetpoint' , gridsetpoint);\n\nmsg.payload = gridsetpoint;\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 550,
        "y": 560,
        "wires": [
            [
                "315af0a55c88cad9"
            ]
        ]
    },
    {
        "id": "67156c8207c78082",
        "type": "inject",
        "z": "1459d4d7551ca112",
        "name": "Demo",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 350,
        "y": 560,
        "wires": [
            [
                "7e94cf2671a9b2dd"
            ]
        ]
    },
    {
        "id": "315af0a55c88cad9",
        "type": "debug",
        "z": "1459d4d7551ca112",
        "name": "demo-gridsetpoint",
        "active": true,
        "tosidebar": false,
        "console": false,
        "tostatus": true,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "payload",
        "statusType": "auto",
        "x": 810,
        "y": 560,
        "wires": []
    },
    {
        "id": "2f5522e00a078a17",
        "type": "comment",
        "z": "1459d4d7551ca112",
        "name": "Description",
        "info": "Global variable:\n\nvrmSiteId = the individual VRM site id\n            I set this global variable in an extra flow\n            Never publish your VRM site id!\n\nFlow variables:\n\nUpdated every hour:\nforecast_today = VRM forecast for whole today\nforecast_tomorrow = VRM forecast for whole day tomorrow\nforecast_restofday = VRM forecast from now to end of day\n\nPermanently updated:\ncurrentSOC = the current SOC \nmissingload = load (in Wh) necessary for 100% SOC\n\nUpdated once daily:\nminSOC = minimal SOC for ESS, different for each month of the year\n\nUpdated every hour:\ngridsetpoint = Grid Setpoint, to enable grid-feed-in when more solar yield is expected than inverter power\n\nThe gridsetpoint is modified according to month (seasonal difference), hour (discharging full battery over night etc), current SOC and forecast_restofday\n\nThe calc_gridsetpoint_demo can be used to evaluate different settings according to a different system. The variables cmonth, cday, forecast_today, forecast_tomorrow, forecast_restofday, currentSOC, missingload can be set to the values to be tested. Just click on the inject node.",
        "x": 140,
        "y": 40,
        "wires": []
    },
    {
        "id": "cca5d0a225796a2b",
        "type": "config-vrm-api",
        "name": "SolarVorhersage"
    }
]

Hi Tom,

thank you for this info. I’ll be testing it this week. I think this kind of fullfills my desires.

I’ll keep you informed after evaluation :slight_smile:

There are still some bugs in the Node Red flow above. Change all variable names “currentsoc” to “currentSOC”, and change the line
“let fcrest = flow.get (‘flow.forecast_restofday’);”
to
“let fcrest = flow.get (‘forecast_restofday’);”

I have been messing around and using ChatGPT also. This is what I currently am testing.

[
{
“id”: “feb0e52bc873eae9”,
“type”: “tab”,
“label”: “ESS Forecast Charge Controller”
},
{
“id”: “ab2e6c126cc45acc”,
“type”: “inject”,
“z”: “feb0e52bc873eae9”,
“name”: “Run forecast check 23:50”,
“props”: ,
“repeat”: “”,
“crontab”: “50 23 * * *”,
“once”: false,
“onceDelay”: “”,
“topic”: “”,
“x”: 180,
“y”: 100,
“wires”: [
[
“0d9c3c96c432b18b”
]
]
},
{
“id”: “0d9c3c96c432b18b”,
“type”: “vrm-api”,
“z”: “feb0e52bc873eae9”,
“vrm”: “vrm_config”,
“name”: “VRM forecast tomorrow”,
“api_type”: “installations”,
“idUser”: “”,
“idSite”: “233814”,
“installations”: “stats”,
“attribute”: “solar_yield_forecast”,
“stats_interval”: “15mins”,
“show_instance”: false,
“stats_start”: “bot”,
“stats_end”: “86400”,
“use_utc”: false,
“gps_start”: “”,
“gps_end”: “”,
“instance”: “”,
“store_in_global_context”: false,
“verbose”: false,
“transform_price_schedule”: false,
“outputs”: 1,
“x”: 420,
“y”: 100,
“wires”: [
[
“e1180073c5bbc5c7”
]
]
},
{
“id”: “e1180073c5bbc5c7”,
“type”: “function”,
“z”: “feb0e52bc873eae9”,
“name”: “Extract forecast + convert kWh→Wh”,
“func”: “let forecast = msg.payload?.totals?.solar_yield_forecast;\n\nforecast = Number(forecast) || 0;\n\n// VRM gives kWh → convert to Wh\nforecast = Math.floor(forecast / 1000);\n\nflow.set(‘forecast_tomorrow’, forecast);\n\nnode.status({text: ‘Forecast stored: ’ + forecast + ’ Wh’});\n\nmsg.payload = forecast;\nreturn msg;”,
“outputs”: 1,
“timeout”: “”,
“noerr”: 0,
“initialize”: “”,
“finalize”: “”,
“libs”: ,
“x”: 710,
“y”: 100,
“wires”: [
[
“7ba06b73cec005a1”
]
]
},
{
“id”: “7ba06b73cec005a1”,
“type”: “function”,
“z”: “feb0e52bc873eae9”,
“name”: “Determine target SoC”,
“func”: “let forecast = flow.get(‘forecast_tomorrow’) || 0;\n\nlet targetSOC = 75;\n\nif (forecast >= 10000 && forecast < 15000)\n targetSOC = 65;\nelse if (forecast >= 15000 && forecast < 20000)\n targetSOC = 50;\nelse if (forecast >= 20000)\n targetSOC = 30;\n\nflow.set(‘targetSOC’, targetSOC);\nflow.set(‘chargeEnabled’, true);\n\nnode.status({text: 'Forecast ’ + forecast + ’ → Target ’ + targetSOC + ‘%’});\n\nmsg.payload = targetSOC;\nreturn msg;”,
“outputs”: 1,
“timeout”: “”,
“noerr”: 0,
“initialize”: “”,
“finalize”: “”,
“libs”: ,
“x”: 1040,
“y”: 100,
“wires”: [
[
“2ae851086fc9736d”
]
]
},
{
“id”: “2ae851086fc9736d”,
“type”: “debug”,
“z”: “feb0e52bc873eae9”,
“name”: “Target SoC %”,
“active”: true,
“complete”: “payload”,
“x”: 1260,
“y”: 100,
“wires”:
},
{
“id”: “vrm_config”,
“type”: “config-vrm-api”,
“name”: “Bjorn_Home”,
“forceIpv4”: false
},
{
“id”: “9ae9c3104e71f3d8”,
“type”: “global-config”,
“env”: ,
“modules”: {
“victron-vrm-api”: “0.3.11”
}
}
]

Let’s see what this brings.

What is the purpose: When forecast of next day is lower then certain value or between 2 values, the system has to start charging the batteries to certain SoC. When forecast is lower then 10KW, then charge 75% SoC; when between 10 and 15KW, hten charge to 65%,…

@bjorn.aenspeck

Have you tried DESS with „sell to grid restriction“?
I tried it to be grid friendly.
I discharge batteries during morning high price, charge with OV during lunch time and discharge during evening peak again.

I use high fixed buy price and variable low sell price.

You should use original buy price and sell price equals zero.

Than the system should plan optimal buy price considering PV and consumption forecast, system efficiency and remaining battery.

There have been some issue with bigger batteries based on 1% precision SOC, which shall be fixed with current Venus OS 3.70.