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"
}
]