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

Flow Info

Created 1 year, 10 months ago
Rating: not yet rated

Owner

Actions

Rate:

Node Types

Core
  • comment (x3)
  • debug (x1)
  • function (x7)
  • inject (x2)
Other

Tags

  • evcharging
  • scheduler
  • pvforecast
  • heatpump
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option