Skip to content

Instantly share code, notes, and snippets.

@zoernert
Created August 21, 2022 23:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zoernert/647b9ac533783c9d329b14a16214474e to your computer and use it in GitHub Desktop.
Save zoernert/647b9ac533783c9d329b14a16214474e to your computer and use it in GitHub Desktop.
Use PV Forecast for EV Charging or Heatpump scheduling

**Electric vehicles and heat pumps are consumers which can very easily be used for demand-side management. In order to maximize use of local electricity generated by photovoltaics and minimize the amount of electricity from the grid, a scheduler plans and organizes time slots. **

This flow shows the concepts and introduces the actual scheduling function. It is designed as an easy to adopt prototype for intermidiate users of Node-RED. In order to get this scheduler working, a free RapidAPI-Key for the PV forecast system is required. You could get it here and register for the SolarEnergyPrediction API.

Setup / Configuration

Open the SEP-API Call node and its configuration. Create a new RapidAPI Credentials with an API-key to use with the SolarEnergyPrediction service.

Configure your PV-generation plant in the PV Settings node.

msg.payload = {
    "lat": 49.3418836,  // latitude of geolocation
    "lon": 8.8006813,   // longitude of geolocation
    "deg": 35,     // tilt degrees of pv panels (0=horiziontal)
    "az": 45, // azimuth (0=south, 90 = west, -90 = east)
    "wp": 5060 // WattPeak of generator
}
return msg;

Usage

Trigger the flow using the inject node Retrieve Forecast. After a few seconds the scheduler calculates the optimum times and stores them in a context-data object of the flow called prediction.

For easy re-use you might inject Retrieve Schedule which will display a formal schedule with the on/off switches for the two pre-configured devices.

Additional devices might be added using the same schema. There is no UI!

Questions?

Do not hesitate to contact dev@stromdao.com.

[
{
"id": "c034943b78abeaa2",
"type": "tab",
"label": "PV Prediction",
"disabled": false,
"info": "Sample flow to see how to retrieve a solar plant prediction and use it for scheduling of larger devices (like EV charging or heatpump).\r\n",
"env": []
},
{
"id": "33570a6660ac8b9a",
"type": "RapidAPI",
"z": "c034943b78abeaa2",
"name": "SEP-API Call",
"account": "fcd26d2128b2396f",
"url": "https://solarenergyprediction.p.rapidapi.com/v2.0/solar/prediction",
"method": "GET",
"x": 690,
"y": 80,
"wires": [
[
"f5bd6dd8b5d8fd97"
]
]
},
{
"id": "54583f123b9a4d7f",
"type": "inject",
"z": "c034943b78abeaa2",
"name": "Retrieve Forecast",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "3600",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 170,
"y": 80,
"wires": [
[
"29e3f22863574168"
]
]
},
{
"id": "f5bd6dd8b5d8fd97",
"type": "function",
"z": "c034943b78abeaa2",
"name": "Store in Flow",
"func": "await flow.set(\"prediction\",msg.payload.output);\nconst forecast = msg.payload.output;\n\nlet avail = 0;\nfor (let i = 0;\n (i < forecast.length) &&\n (forecast[i].timestamp < new Date().getTime() + 86400000);\n i++) {\n if ((forecast[i].timestamp > new Date().getTime() - 3600000) && (forecast[i].wh > 0)) {\n avail += 1 * forecast[i].wh; \n }\n}\nlet fill = \"green\";\n\nnode.status({ fill: fill, shape: \"ring\", text: avail + \" available.\" });\nflow.set(\"devices\",{});\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 890,
"y": 80,
"wires": [
[
"e4b94d2841fd7d26"
]
]
},
{
"id": "3fcc1f84753c3656",
"type": "comment",
"z": "c034943b78abeaa2",
"name": "Retrieve Generation Forecast",
"info": "",
"x": 180,
"y": 40,
"wires": []
},
{
"id": "9dba8be5fe7326dd",
"type": "comment",
"z": "c034943b78abeaa2",
"name": "Schedule Devices",
"info": "",
"x": 150,
"y": 160,
"wires": []
},
{
"id": "aada8def46d86c4d",
"type": "function",
"z": "c034943b78abeaa2",
"name": "Optimize Schedule",
"func": "const forecast = await flow.get(\"prediction\");\nlet devices = await flow.get(\"devices\");\nif((typeof devices == 'undefined') || (devices == null)) devices = {};\n\nlet relevantHours = [];\nfor(let i=0;\n (i<forecast.length) && \n (forecast[i].timestamp < new Date().getTime() + msg.payload.timeframe);\n i++) {\n if ((forecast[i].timestamp > new Date().getTime() - 3600000) && (forecast[i].wh>0)) {\n relevantHours.push(forecast[i]);\n }\n}\n\nrelevantHours.sort(function (a, b) {\n return b.wh - a.wh;\n});\n\nlet requiredWh = msg.payload.requiredWh * 1;\nlet used = 0;\n\ndevices[msg.payload.device] = requiredWh;\n\nfor(let i=0;(i<relevantHours.length) && (requiredWh > 0);i++) {\n if(requiredWh > 0) {\n let using = msg.payload.avgWatt;\n if (using > requiredWh) using = requiredWh;\n requiredWh -= using;\n used += using;\n relevantHours[i].wh -= using;\n if (typeof relevantHours[i].usedBy == 'undefined') {\n relevantHours[i].usedBy = {};\n }\n relevantHours[i].usedBy[msg.payload.device] = using;\n }\n}\n\nlet remain = 0;\nfor (let i = 0;\n (i < forecast.length);\n i++) {\n \n if(forecast[i].timestamp < new Date().getTime() + msg.payload.timeframe) { \n for(let j=0;j<relevantHours.length;j++) {\n if(relevantHours[j].timestamp == forecast[i].timestamp) {\n node.log(\"rhWh\" + relevantHours[j].wh);\n forecast[i].wh = relevantHours[j].wh;\n remain += forecast[i].wh;\n }\n }\n }\n}\n\nawait flow.set(\"prediction\", forecast);\nlet fill=\"red\";\nif(remain > 0) fill=\"green\";\n\nnode.status({ fill: fill, shape: \"ring\", text: used +\" used / \"+remain+\" unused.\"});\nawait flow.set(\"devices\",devices);\n\nmsg.payload = relevantHours;\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 650,
"y": 220,
"wires": [
[
"20ff9e7579cc6a9f"
]
]
},
{
"id": "20ff9e7579cc6a9f",
"type": "function",
"z": "c034943b78abeaa2",
"name": "Settings: Device 2 - Heatpump",
"func": "msg.payload = {\n \"device\": \"Device_2_heatpump\",\n \"requiredWh\": 5000,\n \"avgWatt\": 2500,\n \"timeframe\": 86400000\n};\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 210,
"y": 300,
"wires": [
[
"e7d53e54f786dd84"
]
]
},
{
"id": "e4b94d2841fd7d26",
"type": "function",
"z": "c034943b78abeaa2",
"name": "Settings: Device 1 Car ",
"func": "msg.payload = {\n \"device\": \"Device_1_car\",\n \"requiredWh\": 18000,\n \"avgWatt\": 2100,\n \"timeframe\": 86400000\n};\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 180,
"y": 220,
"wires": [
[
"aada8def46d86c4d"
]
]
},
{
"id": "29e3f22863574168",
"type": "function",
"z": "c034943b78abeaa2",
"name": "PV Settings",
"func": "msg.payload = {\n \"lat\": 49.3418836,\n \"lon\": 8.8006813,\n \"deg\": 35,\n \"az\": 45,\n \"wp\": 5060\n}\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 410,
"y": 80,
"wires": [
[
"33570a6660ac8b9a"
]
]
},
{
"id": "e7d53e54f786dd84",
"type": "function",
"z": "c034943b78abeaa2",
"name": "Optimize Schedule",
"func": "const forecast = await flow.get(\"prediction\");\nlet devices = await flow.get(\"devices\");\nif ((typeof devices == 'undefined') || (devices == null)) devices = {};\n\nlet relevantHours = [];\nfor (let i = 0;\n (i < forecast.length) &&\n (forecast[i].timestamp < new Date().getTime() + msg.payload.timeframe);\n i++) {\n if ((forecast[i].timestamp > new Date().getTime() - 3600000) && (forecast[i].wh > 0)) {\n relevantHours.push(forecast[i]);\n }\n}\n\nrelevantHours.sort(function (a, b) {\n return b.wh - a.wh;\n});\n\nlet requiredWh = msg.payload.requiredWh * 1;\nlet used = 0;\n\ndevices[msg.payload.device] = requiredWh;\n\nfor (let i = 0; (i < relevantHours.length) && (requiredWh > 0); i++) {\n if (requiredWh > 0) {\n let using = msg.payload.avgWatt;\n if (using > requiredWh) using = requiredWh;\n requiredWh -= using;\n used += using;\n relevantHours[i].wh -= using;\n if (typeof relevantHours[i].usedBy == 'undefined') {\n relevantHours[i].usedBy = {};\n }\n relevantHours[i].usedBy[msg.payload.device] = using;\n }\n}\n\nlet remain = 0;\nfor (let i = 0;\n (i < forecast.length);\n i++) {\n\n if (forecast[i].timestamp < new Date().getTime() + msg.payload.timeframe) {\n for (let j = 0; j < relevantHours.length; j++) {\n if (relevantHours[j].timestamp == forecast[i].timestamp) {\n node.log(\"rhWh\" + relevantHours[j].wh);\n forecast[i].wh = relevantHours[j].wh;\n remain += forecast[i].wh;\n }\n }\n }\n}\n\nawait flow.set(\"prediction\", forecast);\nlet fill = \"red\";\nif (remain > 0) fill = \"green\";\n\nnode.status({ fill: fill, shape: \"ring\", text: used + \" used / \" + remain + \" unused.\" });\nawait flow.set(\"devices\", devices);\n\nmsg.payload = relevantHours;\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 650,
"y": 300,
"wires": [
[]
]
},
{
"id": "ae5eff12cbab70a7",
"type": "function",
"z": "c034943b78abeaa2",
"name": "Format Schedule",
"func": "const forecast = await flow.get(\"prediction\");\nconst devices = await flow.get(\"devices\");\n\nlet rows = [];\n\nfor(let i=0;((i<forecast.length) && (forecast[i].timestamp < new Date().getTime()+86400000));i++) {\n if(forecast[i].timestamp > new Date().getTime()-3600000) {\n let row = {\n timestamp:forecast[i].timestamp,\n unscheduledWh:forecast[i].wh,\n devices: {}\n };\n\n if(typeof forecast[i].usedBy !== 'undefined') {\n for (const [key, value] of Object.entries(devices)) {\n if(typeof forecast[i].usedBy[key] !== 'undefined') {\n row.devices[key] = 1;\n } else {\n row.devices[key] = 0;\n }\n }\n } else {\n for (const [key, value] of Object.entries(devices)) {\n row.devices[key] = 0;\n }\n }\n\n rows.push(row);\n }\n}\n\nmsg.payload = rows;\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 650,
"y": 440,
"wires": [
[
"2a1f1a3fc352e07d"
]
]
},
{
"id": "5b5161cd24d041dc",
"type": "inject",
"z": "c034943b78abeaa2",
"name": "Retrieve Schedule",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 170,
"y": 440,
"wires": [
[
"ae5eff12cbab70a7"
]
]
},
{
"id": "24dee4e6a95cb1c8",
"type": "comment",
"z": "c034943b78abeaa2",
"name": "Show in Console",
"info": "",
"x": 140,
"y": 400,
"wires": []
},
{
"id": "2a1f1a3fc352e07d",
"type": "debug",
"z": "c034943b78abeaa2",
"name": "Debug View",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 870,
"y": 440,
"wires": []
},
{
"id": "fcd26d2128b2396f",
"type": "rapidapi-config",
"name": "SEP-API"
}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment