cmorewood:
I would like to see the same in the SOC chart in VRM this would enable, should everything stay pretty much the same, the user to adjust discharge to arrive at a specified SOC at a specific time. The units should be perhaps 1, 2, 3, and 4 hours or perhaps ther could be a dropdown menu for up to say 6 hours?
This can easily be done via Node Red. I use a Node Red flow to set the “grid setpoint” according to current SOC, minSOC and solar yield forecast. The solar forecast can be easily queried via the VRM-API node.
I have 5.4 kWp installed solar power, and have just a MP2-3000 which can deliver not more than 2.4 kW. To be able to feed in all excess solar power to the grid I use my 15 kWh battery as a buffer and feed in from battery over night.
Edit: The flow uses a global variable “global.vrmSiteId” to hold your individual VRM Site Id. This Id should not be published. Use an extra flow to set this global variable according to your individual valie.
[
{
"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"
}
]