Beginner help - home battery charge PID control loop

Hi,

Please help me Obi-Wan!

So I have a Frankenstein solar system, it's evolved into a nightmare but I've invested so much into it that have to keep it. I'm not math or programming minded (so Google searches haven't been helpful) that's why node-red has been amazing. I'm doing bits and bobs for the solar system in node-red already.

Can someone help me with a loop that I can implement that takes a target SoC and time to get to that SoC and outputs a signal to either charge (target SoC higher than current SoC) or discharge (target SoC lower than current SoC) and how many watts (max charge rate is 60A, roughly 3000W and max discharge rate is 100A, roughly 5000W, so there can be situations where I can't charge fast enough to reach the SoC in the requested time)?

For charge no doubt I can layer some logic onto the loop to either stop charging when it hits the target or to maintain the target by continuously charging to offset the house load.

My battery is 48v nominal and has a capacity of 19.2kwh, all the charge smarts are handled by the BMS so I don't have to worry about safety or slowing the charge rate when it's nearly full.

I already have live measurements for battery SoC, current, voltage, draw in/out (any many other measurements) in node-red.

I should say that the battery can also be charged by solar, a different (off-grid) inverter than the one I want to control.

There is a cost equation as well but again I can no doubt layer some logic onto the loop to either stop charging when it's too expensive or to start discharging if the SoC is OK and the export price is good.

Honestly at this point I would happily spend a bit of money to pay for help.

Thanks.
Richard

Yep i already do this - based on a few variables - you do not need a PID loop but do need the following (where are you by the way ?)

IN my case i have a whole of house energy monitor so i can measure the following

Consumption (of the house)
Solar output
Grid draw or feed
Battery Charge/Discharge

In my case my POCO in Australia provides a variable rate for the supply of electricity (it changes every 1/2 hour) - between certain hours we are charged to feed into the grid - but generally on a reasonable weather day we can be assured of low power rates between 10am - 3PM

Here is my code below - feel free to ask any questions

First block is the exported part of the flow which may be a bit overwhelming

2nd part is just the Function node that does the heavy lifting
Note the following

I have 3 battereis with a total capacity of 39.9Kwh
They have their own BMS so i do not have to worry about overcharging etc - once they hit 90% charge they taper down the acceptable charge rate

Our POCO charges us a lot extra for power used after 3PM so the goal is to have the batteries fully chargeg by then and then put them into auto mode so they service the house needs and keep the draw from the grid to Zero.

[{"id":"74bf64010f768b60","type":"function","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"ForceGridChargeFunction10-2","func":"//Modified by CraigC on 1/7/24 to reflect new Demand tarriff and end of AUgrid Trial Tarriff\n\n//Created by CraigC on 25/3 to start automating charging on Trial Ausgrid two way Tarriff\n//This will run every 5 minutes between 10am and 3PM \n// It will monitor the SOC of the batteries and aim to finish charging by 3PM each day\n//It will also monitor the SOC of each battery and ensure they are balanced as they charge\n\n//Need to take into account if the weather is sunny then we will max production between 11 and 2PM \n//1st Hour we should limit rate into battery - can do this crudely to begin with to see results\n\n//Note that once batteries get to 90% SOC they slow down to max 6.5Kw charge\n//and typically we will produce most power from 11AM - so should ramp slowly and cater for\n//Tapering at end of cycle (eventually) \n\n//Setup our Variables\n\nlet NumHoursRemaining = 0;\nlet StartChargeTime = 0;\nlet EndChargeTime = \"15:00\"; //End Charging time - peak hour starts at 2PM\nlet RemainingTimeToCharge = 0; //THis will hold a decimal representation of how many hours left to achieve full batteries \nlet SetChargeRate = 0;\nlet DesiredBatteryChargeRate = 0;\nlet SetChargeRatePerInverter = 0;\nlet AmountToCharge = 0;\nlet AverageSOC = global.get(\"Battery.AverageSOC\");\nlet Fivemin_GridPower = global.get(\"Power.5min_GridPower\");\nlet Onemin_GridPower = global.get(\"Power.1min_GridPower\");\nlet Weather = global.get(\"Weather.Today.Description\");\nlet ManualChargeRamp = global.get(\"Battery.ManualChargeRamp\");\n\n// A function to Round (Correctly) to any given number of digits\nNumber.prototype.round = function (places) {\n    return +(Math.round(this + \"e+\" + places) + \"e-\" + places);\n}\n\n//Get 3PM today as a JS Date object\nlet d = new Date();\nlet ThreePMDay = new Date();\nThreePMDay.setHours(15);\nThreePMDay.setMinutes(0);\nThreePMDay.setSeconds(0);\n\n//In case it spans a day - code to be safe\nif (d > ThreePMDay) {\n    ThreePMDay.setDate(ThreePMDay.getDate() + 1)\n};\n\n//Get the difference between current time and 3PM\nvar diff = ((ThreePMDay.getTime() - d.getTime()) / 1000) / 60; // Return the value in minutes\nnode.warn(\"Difference till 3PM \" + (diff));\n\n//Round this into Hours to 2 decimal places\n\nif (diff > 30) {\nRemainingTimeToCharge = (diff - 30).round(2);// Set an offset to account for battery slow charge at top SOC - take 1/2 hour off the time calculated\n}\nelse RemainingTimeToCharge = diff;\n\n\nnode.warn(RemainingTimeToCharge);\n\nmsg.RemainingTimeToCharge = RemainingTimeToCharge;\n\n\nAmountToCharge = (100 - AverageSOC) / 100 * 39900; //Set how much we need from the Grid - essentially in Kw\nnode.warn(\"This is the amount to charge with no offset \" + AmountToCharge);\n\n\nSetChargeRate = ((AmountToCharge / 3) / RemainingTimeToCharge) * 60; //Work out the charge rate per inverter\nnode.warn(\"Set Charge Rate \" + SetChargeRate + \" Amount to Charge \" + AmountToCharge + \" Remaining Time to Charge \" + RemainingTimeToCharge);\n\nSetChargeRate = SetChargeRate.round(2);\nSetChargeRatePerInverter = (SetChargeRate / 50) + 6; //Convert this into %Value and add 6 to ensure we are full at the end \nSetChargeRatePerInverter = SetChargeRatePerInverter.round(0);\nnode.warn(\"Set Charge Rate \" + SetChargeRate);\nnode.warn(\"Set Charge Rate Per Inverter \" + SetChargeRatePerInverter);\n\n\n//At the end of the charge cycle - do not stop charging too early or we will end up running over 3PM\n//This is crude and could really key in better on the Extended Battery info such as Max Charge rate and \n\n//Slowdown charging in first 1 hours to maximize use of solar\nif ((diff > 180 && Weather.match(/Sunny./i)) || (diff > 180 && (ManualChargeRamp == \"Manual\"))) {\n    SetChargeRate = SetChargeRate * 0.60;\n    SetChargeRatePerInverter = SetChargeRatePerInverter * 0.60; \n}\n\n//Slowdown charging in next 1/2 hours to maximize use of solar\nif ((diff > 150 && diff < 180 && Weather.match(/Sunny./i)) || (diff > 150 && diff < 180 && (ManualChargeRamp == \"Manual\"))) {\n    SetChargeRate = SetChargeRate * 0.75;\n    SetChargeRatePerInverter = SetChargeRatePerInverter * 0.75; \n}\n\n\nnode.warn(\"After Slowdown code - Set Charge Rate \" + SetChargeRate + \" Amount to Charge \" + AmountToCharge + \" Remaining Time to Charge \" + RemainingTimeToCharge);\n\n\n//Try and use as much solar as possible for the charging - particularly if negative feedin.\n//This should be in a 1 minute loop so should track reasonably well.\nif (Onemin_GridPower < - 200) {\n    SetChargeRate = SetChargeRate + Math.abs(Onemin_GridPower)\n    SetChargeRatePerInverter = SetChargeRatePerInverter + Math.abs(Onemin_GridPower / 50); //The Value is a Percentage \n    msg.ExcessSolarSinking = \"Yes\"\n    \n    if (SetChargeRate > 4600){\n        SetChargeRate = 4600\n        SetChargeRatePerInverter = 100\n     }\n} \n\n//The Battery will slow down charging once it hits 90% SOC\nif (AverageSOC > 90) {\n    SetChargeRate = 2500;\n    SetChargeRatePerInverter = 50;\n};\n\n//Values to pass on in messages\nmsg.RemainingTimeToCharge = RemainingTimeToCharge;\nmsg.SetChargeRate = SetChargeRate;\nmsg.SetChargeRatePercent = SetChargeRate;\nmsg.SetChargePerInverter = SetChargeRatePerInverter;\nmsg.SetChargePerInverterPercent = SetChargeRatePerInverter;\nmsg.BatteryMode = \"Charge\";\nmsg.Source=\"FromForceGridCharge10-3Function\";\nmsg.BatteryChargeRate=\"Percentage\";\nmsg.Weather=Weather;\nmsg.diff=diff;\n\nreturn msg;\n\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[{"var":"moment","module":"moment"}],"x":590,"y":1940,"wires":[["41a549279178f72f","71be7807d7acf84b"]]},{"id":"41a549279178f72f","type":"debug","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"debug 30","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":820,"y":1900,"wires":[]},{"id":"71be7807d7acf84b","type":"change","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Set Global Variable for Battery Charging from the Grid","rules":[{"t":"set","p":"Battery.Mode","pt":"global","to":"Manual-Charge","tot":"str"},{"t":"set","p":"Battery.Arbitrage.TimedCharge","pt":"global","to":"Auto","tot":"str"},{"t":"set","p":"Battery.ChargingRate","pt":"global","to":"SetChargePerInverter","tot":"msg"},{"t":"set","p":"Battery.ChargingRatePercent","pt":"global","to":"SetChargePerInverterPercent","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":960,"y":1940,"wires":[["7805d44be736c9f8","09d9d56335d3724b"]]},{"id":"488bd1fababddc3e","type":"time-inject","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Kick the charging process every 15 Seconds","nameInt":"10:00 - 14:59 = timestamp ↻15s","positionConfig":"e2e4f670.cd19a8","props":[{"p":"","pt":"msgPayload","v":"","vt":"date","o":"","oT":"none","oM":"60000","f":0,"fS":0,"fT":"UNIX timestamp (ms)","fI":"0","next":true,"days":"*","months":"*","onlyOddDays":false,"onlyEvenDays":false,"onlyOddWeeks":false,"onlyEvenWeeks":false},{"p":"","pt":"msgTopic","v":"","vt":"str","o":"","oT":"none","oM":"60000","f":0,"fS":0,"fT":"UNIX timestamp (ms)","fI":"0","next":false,"days":"*","months":"*","onlyOddDays":false,"onlyEvenDays":false,"onlyOddWeeks":false,"onlyEvenWeeks":false}],"injectTypeSelect":"interval-time","intervalCount":"15","intervalCountType":"num","intervalCountMultiplier":1000,"time":"10:00","timeType":"entered","offset":0,"offsetType":"none","offsetMultiplier":60000,"timeEnd":"14:59","timeEndType":"entered","timeEndOffset":0,"timeEndOffsetType":"none","timeEndOffsetMultiplier":60000,"timeDays":"*","timeOnlyOddDays":false,"timeOnlyEvenDays":false,"timeOnlyOddWeeks":false,"timeOnlyEvenWeeks":false,"timeMonths":"*","timedatestart":"","timedateend":"","property":"","propertyType":"none","propertyCompare":"true","propertyThreshold":"","propertyThresholdType":"num","timeAlt":"","timeAltType":"entered","timeAltDays":"*","timeAltOnlyOddDays":false,"timeAltOnlyEvenDays":false,"timeAltOnlyOddWeeks":false,"timeAltOnlyEvenWeeks":false,"timeAltMonths":"*","timeAltOffset":0,"timeAltOffsetType":"none","timeAltOffsetMultiplier":60000,"once":false,"onceDelay":0.1,"recalcTime":2,"x":250,"y":1720,"wires":[["9e6aded9aeca4653"]]},{"id":"ec2fae3179085a08","type":"flogger","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Log GLobal Battery Context and messages","logfile":"Arbitrage Code for Daytime Battery Charging","inputchoice":"fullmsg","inputobject":"payload","inputobjectType":"msg","inputmoustache":"Recieved payload {{payload}} and topic {{topic}}","loglevel":"INFO","logconfig":"bc7a715d.1d2f2","sendpane":"","x":1650,"y":1880,"wires":[[]]},{"id":"7805d44be736c9f8","type":"change","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Test Dumping Globals","rules":[{"t":"set","p":"BatteryGlobal","pt":"msg","to":"Battery","tot":"global","dc":true}],"action":"","property":"","from":"","to":"","reg":false,"x":1240,"y":1880,"wires":[["ec2fae3179085a08"]]},{"id":"df55f222f3f5147e","type":"time-inject","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Start Dumping to the grid","nameInt":"16:30 - 20:59 = timestamp ↻1min","positionConfig":"e2e4f670.cd19a8","props":[{"p":"","pt":"msgPayload","v":"","vt":"date","o":"","oT":"none","oM":"60000","f":0,"fS":0,"fT":"UNIX timestamp (ms)","fI":"0","next":true,"days":"*","months":"*","onlyOddDays":false,"onlyEvenDays":false,"onlyOddWeeks":false,"onlyEvenWeeks":false},{"p":"","pt":"msgTopic","v":"","vt":"str","o":"","oT":"none","oM":"60000","f":0,"fS":0,"fT":"UNIX timestamp (ms)","fI":"0","next":false,"days":"*","months":"*","onlyOddDays":false,"onlyEvenDays":false,"onlyOddWeeks":false,"onlyEvenWeeks":false}],"injectTypeSelect":"interval-time","intervalCount":1,"intervalCountType":"num","intervalCountMultiplier":60000,"time":"16:30","timeType":"entered","offset":0,"offsetType":"none","offsetMultiplier":60000,"timeEnd":"20:59","timeEndType":"entered","timeEndOffset":0,"timeEndOffsetType":"none","timeEndOffsetMultiplier":60000,"timeDays":"*","timeOnlyOddDays":false,"timeOnlyEvenDays":false,"timeOnlyOddWeeks":false,"timeOnlyEvenWeeks":false,"timeMonths":"*","timedatestart":"","timedateend":"","property":"","propertyType":"none","propertyCompare":"true","propertyThreshold":"","propertyThresholdType":"num","timeAlt":"","timeAltType":"entered","timeAltDays":"*","timeAltOnlyOddDays":false,"timeAltOnlyEvenDays":false,"timeAltOnlyOddWeeks":false,"timeAltOnlyEvenWeeks":false,"timeAltMonths":"*","timeAltOffset":0,"timeAltOffsetType":"none","timeAltOffsetMultiplier":60000,"once":false,"onceDelay":0.1,"recalcTime":2,"x":210,"y":2020,"wires":[["47d1233143244a39"]]},{"id":"31ab2478a165bee2","type":"change","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"dump to the grid","rules":[{"t":"set","p":"Battery.Mode","pt":"global","to":"Manual-Discharge","tot":"str"},{"t":"set","p":"Battery.Status","pt":"global","to":"Discharging","tot":"str"},{"t":"set","p":"BatteryMode","pt":"msg","to":"Discharge","tot":"str"},{"t":"set","p":"Battery.ChargingRate","pt":"global","to":"Battery.Arbitrage.DischargeRate","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":1440,"y":2120,"wires":[["09d9d56335d3724b","9f44b938f2a126f7"]]},{"id":"879d769be0504f14","type":"comment","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"This Group is used for the Ausgrid two way tarriff trial ","info":"# ","x":1010,"y":1740,"wires":[]},{"id":"2a13dd65b4c13a8a","type":"ui_multistate_switch","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Fixed Arbitrage Start Mode","group":"89382e25.9b177","order":3,"width":"0","height":"0","label":"Fixed Arbitrage Start time Mode","stateField":"payload","enableField":"enable","passthroughField":"passthrough","inputMsgField":"passthrough","rounded":true,"useThemeColors":true,"hideSelectedLabel":false,"multilineLabel":true,"passThrough":"never","inputMsg":"all","userInput":"enabled_show","options":[{"label":"Auto - 5:30","value":"Auto","valueType":"str","color":"#009933"},{"label":"Manual","value":"Manual","valueType":"str","color":"#999999"},{"label":"Disable","value":"Disable","valueType":"str","color":"#ff6666"}],"topic":"","x":200,"y":2340,"wires":[["8352bc7452515e6b"]]},{"id":"ba83c30fe1f08533","type":"change","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Set to Auto Mode - 5:30PM","rules":[{"t":"set","p":"Battery.Arbitrage.StartTimeMode","pt":"global","to":"Auto","tot":"str"},{"t":"set","p":"Battery.Arbitrage.StartTime","pt":"global","to":"530","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":900,"y":2320,"wires":[[]]},{"id":"8352bc7452515e6b","type":"switch","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Switch Arbitrage Timed Modes","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"Auto","vt":"str"},{"t":"eq","v":"Manual","vt":"str"},{"t":"eq","v":"Disable","vt":"str"}],"checkall":"true","repair":false,"outputs":3,"x":570,"y":2340,"wires":[["ba83c30fe1f08533"],["2acc983c3446bad8"],["d8923a98168f8bf9"]]},{"id":"2acc983c3446bad8","type":"change","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Set to Manual Mode","rules":[{"t":"set","p":"Battery.Arbitrage.StartTimeMode","pt":"global","to":"Manual","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":880,"y":2380,"wires":[[]]},{"id":"d8923a98168f8bf9","type":"change","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Set to Disable Auto Arbitrage","rules":[{"t":"set","p":"Battery.Arbitrage.StartTimeMode","pt":"global","to":"Disable","tot":"str"},{"t":"set","p":"Battery.Arbitrage.StartTime","pt":"global","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":900,"y":2440,"wires":[[]]},{"id":"d8ebc732d6eebe05","type":"ui_multistate_switch","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Fixed Arbitrage Start Time","group":"89382e25.9b177","order":4,"width":"0","height":"0","label":"Fixed Arbitrage Start time","stateField":"payload","enableField":"enable","passthroughField":"passthrough","inputMsgField":"passthrough","rounded":true,"useThemeColors":true,"hideSelectedLabel":false,"multilineLabel":true,"passThrough":"never","inputMsg":"all","userInput":"enabled_show","options":[{"label":"4:30PM","value":"430","valueType":"num","color":"#009933"},{"label":"5:30PM","value":"530","valueType":"num","color":"#999999"},{"label":"6:30PM","value":"630","valueType":"num","color":"#ff6666"}],"topic":"","x":190,"y":2620,"wires":[["044c17b39a505f54"]]},{"id":"fb531812f6308d61","type":"switch","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Check the Time to Start","property":"Battery.Arbitrage.StartTime","propertyType":"global","rules":[{"t":"eq","v":"430","vt":"num"},{"t":"eq","v":"530","vt":"num"},{"t":"eq","v":"630","vt":"num"}],"checkall":"true","repair":false,"outputs":3,"x":770,"y":2160,"wires":[["c6dbe8aef605085c"],["df96f0c1059aad6c"],["ead19b51f829dec4"]]},{"id":"ce66701c0ee02366","type":"change","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Set time to start to 4:30PM","rules":[{"t":"set","p":"Battery.Arbitrage.StartTime","pt":"global","to":"430","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":900,"y":2520,"wires":[[]]},{"id":"044c17b39a505f54","type":"switch","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Switch Arbitrage Start Times","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"430","vt":"num"},{"t":"eq","v":"530","vt":"num"},{"t":"eq","v":"630","vt":"num"}],"checkall":"true","repair":false,"outputs":3,"x":520,"y":2620,"wires":[["ce66701c0ee02366"],["a16e937327f296cd"],["8798f61bdd91a4b1"]]},{"id":"a16e937327f296cd","type":"change","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Set to 5:30PM - Same as Auto","rules":[{"t":"set","p":"Battery.Arbitrage.StartTime","pt":"global","to":"530","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":910,"y":2580,"wires":[[]]},{"id":"8798f61bdd91a4b1","type":"change","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Set to 6:30PM","rules":[{"t":"set","p":"Battery.Arbitrage.StartTime","pt":"global","to":"630","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":860,"y":2640,"wires":[[]]},{"id":"c6dbe8aef605085c","type":"within-time-switch","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Time Between 4:30 and 5:30","nameInt":"","positionConfig":"e2e4f670.cd19a8","startTime":"16:30","startTimeType":"entered","startOffset":0,"startOffsetType":"none","startOffsetMultiplier":60000,"endTime":"17:29","endTimeType":"entered","endOffset":0,"endOffsetType":"none","endOffsetMultiplier":60000,"timeRestrictions":0,"timeRestrictionsType":"none","timeDays":"*","timeOnlyOddDays":false,"timeOnlyEvenDays":false,"timeOnlyOddWeeks":false,"timeOnlyEvenWeeks":false,"timeMonths":"*","timedatestart":"","timedateend":"","propertyStart":"","propertyStartType":"none","propertyStartCompare":"true","propertyStartThreshold":"","propertyStartThresholdType":"num","startTimeAlt":"","startTimeAltType":"entered","startOffsetAlt":0,"startOffsetAltType":"none","startOffsetAltMultiplier":60000,"propertyEnd":"","propertyEndType":"none","propertyEndCompare":"true","propertyEndThreshold":"","propertyEndThresholdType":"num","endTimeAlt":"","endTimeAltType":"entered","endOffsetAlt":0,"endOffsetAltType":"none","endOffsetAltMultiplier":60000,"withinTimeValue":"true","withinTimeValueType":"msgInput","outOfTimeValue":"false","outOfTimeValueType":"msgInput","tsCompare":"0","x":1120,"y":2140,"wires":[["31ab2478a165bee2"],["df96f0c1059aad6c"]]},{"id":"47d1233143244a39","type":"switch","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Check what Mode we are in","property":"Battery.Arbitrage.StartTimeMode","propertyType":"global","rules":[{"t":"eq","v":"Auto","vt":"str"},{"t":"eq","v":"Manual","vt":"str"},{"t":"eq","v":"Disable","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":4,"x":440,"y":2100,"wires":[["94737275b2ebd6b9"],["fb531812f6308d61"],["ef5db92f3e8b2225"],[]]},{"id":"df96f0c1059aad6c","type":"within-time-switch","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Time Between 5:30 and 6:30","nameInt":"","positionConfig":"e2e4f670.cd19a8","startTime":"17:30","startTimeType":"entered","startOffset":0,"startOffsetType":"none","startOffsetMultiplier":60000,"endTime":"18:29","endTimeType":"entered","endOffset":0,"endOffsetType":"none","endOffsetMultiplier":60000,"timeRestrictions":0,"timeRestrictionsType":"none","timeDays":"*","timeOnlyOddDays":false,"timeOnlyEvenDays":false,"timeOnlyOddWeeks":false,"timeOnlyEvenWeeks":false,"timeMonths":"*","timedatestart":"","timedateend":"","propertyStart":"","propertyStartType":"none","propertyStartCompare":"true","propertyStartThreshold":"","propertyStartThresholdType":"num","startTimeAlt":"","startTimeAltType":"entered","startOffsetAlt":0,"startOffsetAltType":"none","startOffsetAltMultiplier":60000,"propertyEnd":"","propertyEndType":"none","propertyEndCompare":"true","propertyEndThreshold":"","propertyEndThresholdType":"num","endTimeAlt":"","endTimeAltType":"entered","endOffsetAlt":0,"endOffsetAltType":"none","endOffsetAltMultiplier":60000,"withinTimeValue":"true","withinTimeValueType":"msgInput","outOfTimeValue":"false","outOfTimeValueType":"msgInput","tsCompare":"0","x":1120,"y":2180,"wires":[["31ab2478a165bee2"],["ead19b51f829dec4"]]},{"id":"ead19b51f829dec4","type":"within-time-switch","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Time after 6:30","nameInt":"","positionConfig":"e2e4f670.cd19a8","startTime":"18:30","startTimeType":"entered","startOffset":0,"startOffsetType":"none","startOffsetMultiplier":60000,"endTime":"19:59","endTimeType":"entered","endOffset":0,"endOffsetType":"none","endOffsetMultiplier":60000,"timeRestrictions":0,"timeRestrictionsType":"none","timeDays":"*","timeOnlyOddDays":false,"timeOnlyEvenDays":false,"timeOnlyOddWeeks":false,"timeOnlyEvenWeeks":false,"timeMonths":"*","timedatestart":"","timedateend":"","propertyStart":"","propertyStartType":"none","propertyStartCompare":"true","propertyStartThreshold":"","propertyStartThresholdType":"num","startTimeAlt":"","startTimeAltType":"entered","startOffsetAlt":0,"startOffsetAltType":"none","startOffsetAltMultiplier":60000,"propertyEnd":"","propertyEndType":"none","propertyEndCompare":"true","propertyEndThreshold":"","propertyEndThresholdType":"num","endTimeAlt":"","endTimeAltType":"entered","endOffsetAlt":0,"endOffsetAltType":"none","endOffsetAltMultiplier":60000,"withinTimeValue":"true","withinTimeValueType":"msgInput","outOfTimeValue":"false","outOfTimeValueType":"msgInput","tsCompare":"0","x":1080,"y":2220,"wires":[["31ab2478a165bee2"],[]]},{"id":"94737275b2ebd6b9","type":"within-time-switch","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"","nameInt":"","positionConfig":"e2e4f670.cd19a8","startTime":"17:30","startTimeType":"entered","startOffset":0,"startOffsetType":"none","startOffsetMultiplier":60000,"endTime":"19:59","endTimeType":"entered","endOffset":0,"endOffsetType":"none","endOffsetMultiplier":60000,"timeRestrictions":0,"timeRestrictionsType":"none","timeDays":"*","timeOnlyOddDays":false,"timeOnlyEvenDays":false,"timeOnlyOddWeeks":false,"timeOnlyEvenWeeks":false,"timeMonths":"*","timedatestart":"","timedateend":"","propertyStart":"","propertyStartType":"none","propertyStartCompare":"true","propertyStartThreshold":"","propertyStartThresholdType":"num","startTimeAlt":"","startTimeAltType":"entered","startOffsetAlt":0,"startOffsetAltType":"none","startOffsetAltMultiplier":60000,"propertyEnd":"","propertyEndType":"none","propertyEndCompare":"true","propertyEndThreshold":"","propertyEndThresholdType":"num","endTimeAlt":"","endTimeAltType":"entered","endOffsetAlt":0,"endOffsetAltType":"none","endOffsetAltMultiplier":60000,"withinTimeValue":"true","withinTimeValueType":"msgInput","outOfTimeValue":"false","outOfTimeValueType":"msgInput","tsCompare":"0","x":1070,"y":2100,"wires":[["31ab2478a165bee2"],[]]},{"id":"09d9d56335d3724b","type":"link out","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Link from Arbitrage Page to Centralised Battery Control","mode":"link","links":["4196122611db1d32"],"x":1385,"y":2020,"wires":[]},{"id":"9f44b938f2a126f7","type":"debug","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"debug 38","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1720,"y":2120,"wires":[]},{"id":"9e6aded9aeca4653","type":"within-time-switch","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Tell the Function What Solar is looking like","nameInt":"","positionConfig":"e2e4f670.cd19a8","startTime":"00:00","startTimeType":"entered","startOffset":0,"startOffsetType":"none","startOffsetMultiplier":60000,"endTime":"23:59","endTimeType":"entered","endOffset":0,"endOffsetType":"none","endOffsetMultiplier":60000,"timeRestrictions":0,"timeRestrictionsType":"none","timeDays":"*","timeOnlyOddDays":false,"timeOnlyEvenDays":false,"timeOnlyOddWeeks":false,"timeOnlyEvenWeeks":false,"timeMonths":"*","timedatestart":"","timedateend":"","propertyStart":"","propertyStartType":"none","propertyStartCompare":"true","propertyStartThreshold":"","propertyStartThresholdType":"num","startTimeAlt":"","startTimeAltType":"entered","startOffsetAlt":0,"startOffsetAltType":"none","startOffsetAltMultiplier":60000,"propertyEnd":"","propertyEndType":"none","propertyEndCompare":"true","propertyEndThreshold":"","propertyEndThresholdType":"num","endTimeAlt":"","endTimeAltType":"entered","endOffsetAlt":0,"endOffsetAltType":"none","endOffsetAltMultiplier":60000,"withinTimeValue":"SolarPeak","withinTimeValueType":"str","outOfTimeValue":"SolarDoldrums","outOfTimeValueType":"str","tsCompare":"0","x":240,"y":1780,"wires":[["59fce55f2f251798"],["59fce55f2f251798"]]},{"id":"b624539385d7d498","type":"switch","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Check for Reaasonable Rate to charge","property":"Amber.30MinPrice.Now","propertyType":"global","rules":[{"t":"lt","v":"45","vt":"num"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":230,"y":1940,"wires":[["74bf64010f768b60"],[]]},{"id":"124f47fbc35024eb","type":"ui_multistate_switch","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Set Manual Ramp Control","group":"89382e25.9b177","order":6,"width":"0","height":"0","label":"Control Manual Ramping of Battery Charge","stateField":"payload","enableField":"enable","passthroughField":"passthrough","inputMsgField":"passthrough","rounded":true,"useThemeColors":true,"hideSelectedLabel":false,"multilineLabel":true,"passThrough":"change","inputMsg":"all","userInput":"enabled_show","options":[{"label":"Manual","value":"Manual","valueType":"str","color":"#009933"},{"label":"Auto","value":"Auto","valueType":"str","color":"#999999"}],"topic":"","x":1410,"y":2280,"wires":[["e90725822a0f448e"]]},{"id":"e90725822a0f448e","type":"change","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Set Global Variable for Battery Charging from the Grid","rules":[{"t":"set","p":"Battery.ManualChargeRamp","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1480,"y":2360,"wires":[[]]},{"id":"97721712fdb62b8a","type":"inject","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Reset Charge Ramp to Auto","props":[{"p":"payload"}],"repeat":"","crontab":"00 15 * * *","once":true,"onceDelay":"5","topic":"","payload":"Auto","payloadType":"str","x":1440,"y":2220,"wires":[["124f47fbc35024eb"]]},{"id":"e787db5506b9de1c","type":"change","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Stop Battery Dumping","rules":[{"t":"set","p":"Battery.Mode","pt":"global","to":"Manual-Charge","tot":"str"},{"t":"set","p":"Battery.Status","pt":"global","to":"Charging","tot":"str"},{"t":"set","p":"BatteryMode","pt":"msg","to":"Charge","tot":"str"},{"t":"set","p":"Battery.ChargingRate","pt":"global","to":"1","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":580,"y":2040,"wires":[["09d9d56335d3724b","59f4b3340f09ab41"]]},{"id":"59f4b3340f09ab41","type":"debug","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"debug 312","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":810,"y":2000,"wires":[]},{"id":"6bc370d5f15b405a","type":"ui_multistate_switch","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Arbitrage Discharge Rate","group":"89382e25.9b177","order":5,"width":"0","height":"0","label":"Variable Discharge Rate","stateField":"payload","enableField":"enable","passthroughField":"passthrough","inputMsgField":"passthrough","rounded":true,"useThemeColors":true,"hideSelectedLabel":false,"multilineLabel":true,"passThrough":"never","inputMsg":"all","userInput":"enabled_show","options":[{"label":"40%","value":"40","valueType":"num","color":"#009933"},{"label":"50%","value":"50","valueType":"num","color":"#999999"},{"label":"60%","value":"60","valueType":"num","color":"#ff6666"},{"label":"70%","value":"70","valueType":"num","color":"#009999"}],"topic":"","x":1250,"y":2440,"wires":[["3d18a26374c160b4"]]},{"id":"3d18a26374c160b4","type":"change","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Set Discharge rate to Global","rules":[{"t":"set","p":"Battery.Arbitrage.DischargeRate","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1540,"y":2440,"wires":[[]]},{"id":"ef5db92f3e8b2225","type":"change","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Set Battery Mode to Auto (No Arbitrage)","rules":[{"t":"set","p":"Battery.Mode","pt":"global","to":"Auto","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":460,"y":2220,"wires":[[]]},{"id":"59fce55f2f251798","type":"switch","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Battery Daytime Charge Mode","property":"Battery.Daytime.Chargemode","propertyType":"global","rules":[{"t":"eq","v":"Auto","vt":"str"},{"t":"eq","v":"Disabled","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":3,"x":210,"y":1860,"wires":[["b624539385d7d498"],["7b739738ad610454"],[]]},{"id":"7b739738ad610454","type":"change","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Set Battery Mode to Auto (No Grid Charge)","rules":[{"t":"set","p":"Battery.Mode","pt":"global","to":"Auto","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":650,"y":1840,"wires":[[]]},{"id":"8dd50d2f699f7b23","type":"ui_multistate_switch","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Daytime Charge from Grid ","group":"89382e25.9b177","order":5,"width":"0","height":"0","label":"Daytime Charge from Grid","stateField":"payload","enableField":"enable","passthroughField":"passthrough","inputMsgField":"passthrough","rounded":true,"useThemeColors":true,"hideSelectedLabel":false,"multilineLabel":true,"passThrough":"change","inputMsg":"all","userInput":"enabled_show","options":[{"label":"Auto","value":"Auto","valueType":"str","color":"#009933"},{"label":"Disabled","value":"Disabled","valueType":"str","color":"#999999"}],"topic":"","x":1250,"y":2520,"wires":[["e168c75ac6db26fc"]]},{"id":"e168c75ac6db26fc","type":"change","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"Enable/Disable Daytime Charging","rules":[{"t":"set","p":"Battery.Daytime.Chargemode","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":2520,"wires":[[]]},{"id":"419fbdb3c6974643","type":"inject","z":"469ddd52.4a9aa4","g":"f09f37e36f955ec3","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"5","topic":"","payload":"Auto","payloadType":"str","x":1190,"y":2620,"wires":[["8dd50d2f699f7b23"]]},{"id":"e2e4f670.cd19a8","type":"position-config","name":"Sydney - Castle Cove","isValide":"true","longitude":"0","latitude":"0","angleType":"deg","timeZoneOffset":99,"timeZoneDST":0,"stateTimeFormat":"3","stateDateFormat":"12"},{"id":"bc7a715d.1d2f2","type":"config-log","logname":"Default with rotation and compression","logdir":"logs","stamp":"local","logstyle":"plain","logrotate":true,"logcompress":true,"logrotatecount":"5","logsize":"1000"},{"id":"89382e25.9b177","type":"ui_group","name":"Battery Mode Control","tab":"db89d5cd.28b158","order":4,"disp":true,"width":"8","collapse":false,"className":""},{"id":"db89d5cd.28b158","type":"ui_tab","name":"Battery Control","icon":"fa-battery-full","order":7,"disabled":false,"hidden":false}]

Craig

And the rest (could not fit in a single message)

This is my main function node code

//Modified by CraigC on 1/7/24 to reflect new Demand tarriff and end of AUgrid Trial Tarriff

//Created by CraigC on 25/3 to start automating charging on Trial Ausgrid two way Tarriff
//This will run every 5 minutes between 10am and 3PM 
// It will monitor the SOC of the batteries and aim to finish charging by 3PM each day
//It will also monitor the SOC of each battery and ensure they are balanced as they charge

//Need to take into account if the weather is sunny then we will max production between 11 and 2PM 
//1st Hour we should limit rate into battery - can do this crudely to begin with to see results

//Note that once batteries get to 90% SOC they slow down to max 6.5Kw charge
//and typically we will produce most power from 11AM - so should ramp slowly and cater for
//Tapering at end of cycle (eventually) 

//Setup our Variables

let NumHoursRemaining = 0;
let StartChargeTime = 0;
let EndChargeTime = "15:00"; //End Charging time - peak hour starts at 2PM
let RemainingTimeToCharge = 0; //THis will hold a decimal representation of how many hours left to achieve full batteries 
let SetChargeRate = 0;
let DesiredBatteryChargeRate = 0;
let SetChargeRatePerInverter = 0;
let AmountToCharge = 0;
let AverageSOC = global.get("Battery.AverageSOC");
let Fivemin_GridPower = global.get("Power.5min_GridPower");
let Onemin_GridPower = global.get("Power.1min_GridPower");
let Weather = global.get("Weather.Today.Description");
let ManualChargeRamp = global.get("Battery.ManualChargeRamp");

// A function to Round (Correctly) to any given number of digits
Number.prototype.round = function (places) {
    return +(Math.round(this + "e+" + places) + "e-" + places);
}

//Get 3PM today as a JS Date object
let d = new Date();
let ThreePMDay = new Date();
ThreePMDay.setHours(15);
ThreePMDay.setMinutes(0);
ThreePMDay.setSeconds(0);

//In case it spans a day - code to be safe
if (d > ThreePMDay) {
    ThreePMDay.setDate(ThreePMDay.getDate() + 1)
};

//Get the difference between current time and 3PM
var diff = ((ThreePMDay.getTime() - d.getTime()) / 1000) / 60; // Return the value in minutes
node.warn("Difference till 3PM " + (diff));

//Round this into Hours to 2 decimal places

if (diff > 30) {
RemainingTimeToCharge = (diff - 30).round(2);// Set an offset to account for battery slow charge at top SOC - take 1/2 hour off the time calculated
}
else RemainingTimeToCharge = diff;


node.warn(RemainingTimeToCharge);

msg.RemainingTimeToCharge = RemainingTimeToCharge;


AmountToCharge = (100 - AverageSOC) / 100 * 39900; //Set how much we need from the Grid - essentially in Kw
node.warn("This is the amount to charge with no offset " + AmountToCharge);


SetChargeRate = ((AmountToCharge / 3) / RemainingTimeToCharge) * 60; //Work out the charge rate per inverter
node.warn("Set Charge Rate " + SetChargeRate + " Amount to Charge " + AmountToCharge + " Remaining Time to Charge " + RemainingTimeToCharge);

SetChargeRate = SetChargeRate.round(2);
SetChargeRatePerInverter = (SetChargeRate / 50) + 6; //Convert this into %Value and add 6 to ensure we are full at the end 
SetChargeRatePerInverter = SetChargeRatePerInverter.round(0);
node.warn("Set Charge Rate " + SetChargeRate);
node.warn("Set Charge Rate Per Inverter " + SetChargeRatePerInverter);


//At the end of the charge cycle - do not stop charging too early or we will end up running over 3PM
//This is crude and could really key in better on the Extended Battery info such as Max Charge rate and 

//Slowdown charging in first 1 hours to maximize use of solar
if ((diff > 180 && Weather.match(/Sunny./i)) || (diff > 180 && (ManualChargeRamp == "Manual"))) {
    SetChargeRate = SetChargeRate * 0.60;
    SetChargeRatePerInverter = SetChargeRatePerInverter * 0.60; 
}

//Slowdown charging in next 1/2 hours to maximize use of solar
if ((diff > 150 && diff < 180 && Weather.match(/Sunny./i)) || (diff > 150 && diff < 180 && (ManualChargeRamp == "Manual"))) {
    SetChargeRate = SetChargeRate * 0.75;
    SetChargeRatePerInverter = SetChargeRatePerInverter * 0.75; 
}


node.warn("After Slowdown code - Set Charge Rate " + SetChargeRate + " Amount to Charge " + AmountToCharge + " Remaining Time to Charge " + RemainingTimeToCharge);


//Try and use as much solar as possible for the charging - particularly if negative feedin.
//This should be in a 1 minute loop so should track reasonably well.
if (Onemin_GridPower < - 200) {
    SetChargeRate = SetChargeRate + Math.abs(Onemin_GridPower)
    SetChargeRatePerInverter = SetChargeRatePerInverter + Math.abs(Onemin_GridPower / 50); //The Value is a Percentage 
    msg.ExcessSolarSinking = "Yes"
    
    if (SetChargeRate > 4600){
        SetChargeRate = 4600
        SetChargeRatePerInverter = 100
     }
} 

//The Battery will slow down charging once it hits 90% SOC
if (AverageSOC > 90) {
    SetChargeRate = 2500;
    SetChargeRatePerInverter = 50;
};

//Values to pass on in messages
msg.RemainingTimeToCharge = RemainingTimeToCharge;
msg.SetChargeRate = SetChargeRate;
msg.SetChargeRatePercent = SetChargeRate;
msg.SetChargePerInverter = SetChargeRatePerInverter;
msg.SetChargePerInverterPercent = SetChargeRatePerInverter;
msg.BatteryMode = "Charge";
msg.Source="FromForceGridCharge10-3Function";
msg.BatteryChargeRate="Percentage";
msg.Weather=Weather;
msg.diff=diff;

return msg;

Hey Craig,

Thanks, it's "you know who" in Sydney, you've helped me heaps in the past, getting my SBP modbus to work.

I should also have added I have have TimeToGo and TimeTo100 coming from my Victron BMV also, although the format is a bit funky "DD days HH:MM:SS" so right now its reading TimeTo100 = "00 days 00:47:34" in MQTT.

Not really understanding why I wouldn't need some kind of loop to handle "manual" grid AC battery "top-ups", I just use the charge and discharge (+ rate in watts) SBP functions that you helped me find and I manually start and stop at the moment.

I basically want to be able to say that I want a certain SoC by a certain time, it seems like most people use the SBP built in PID (with the GoodWe CT clamp which I'm not using) to maintain a certain SoC?

Cheers
Richard

Yeah you do not really want to rely on something external telling you how much time to go

Instead just key in on the current SOC and take it away from the target SOC (presumably 100% but could be calculated elsewhere to just get you through the Peak hour/demand window)

I did not even check the name of who posted - but you should have all my code already and be able to adapt that function node to do what you want - using other processes to store the relevant information as Globals

Craig

Thanks, I don't recall seeing this code but basically I was blinkered by just wanting to get modbus working.

I'm not sure I'm even going to bother with the demand window stuff.

I'm also undecided on arbitrage tbh.

My thinking atm is to code something to make sure my battery is at 45% SoC by 0600 and 85% SoC by 1500. For the overnight charging I'd like to charge cheap in 5 minute bursts (aka LocalVolts 5 minute settlement) at 3kw, overnight charging is only really necessary if for some reason I'm not at >65% SoC at say 9pm.

Thanks for the code, unfortunately it doesn't make much sense, I was hoping I could do something graphically in a flow to help me understand it all.

Not sure why I wouldn't trust the Victron time to charge / discharge it's very accurate?

I'm skimming through your code, I don't see any corrections to the AC charge rate for the amount of watts coming in through PV?

If 20kw of solar comes down before sunset then I'd rather not use any grid at all. A good example is today, I imported 3w from the grid and the rest came from solar, my (nearly) 20kw battery is 91% full now and I probably won't bother exporting anything today because imho exporting post EA959 (Ausgrid 26.7c rebate per kwh between 1400-2000hrs) is not appealing to me anymore.

So I guess my question is more around a more manual setup, or semi-automatic, more around off-grid usage and less about arbitrage or exporting.

I'll probably only make an effort to export in the shoulder when there is an AEMO notice of a NSW LOR condition.

For example I envisage enabling charge at 2100, set it to be 45% SoC by 0600, let's say the current SoC is 50%, it would be cool if it waited to after around 0100 when its cheapest and pulled in power as long as the cost was under a certain threshold (lets say 22c) for that 5 minutes.

Yes so my code does a couple of things

  1. If the weather report is for Sunny - then it ramps the charge rate - so basically for the first 1.5 hours slows it down to allow it to fit within the solar window - but my overriding goal is to have the batery full by 3PM - not so much or grid arbitrage any more - although i still push to the grid if the overnight rates are low enough to make it worthwhile - otherwise i just put the battery into my Auto mode and it Zeros out grid use

  2. If you can get the SOC from the BMS you are better off doing that than having an estimate from the Victron - if you have not got that working yet - then your Shunt is your only choice.

  3. Obviously if you want to rely on Forecast.io or one of the other solar forecasters for how much solar you are going to get then you can - my system does not really track well as i have 6 orientations of panels - i have a Python library that is very good - and will allow me to dial in shade etc - but have not had time to delve into that properly yet.

You can setup something simple with graphical nodes but you are going to have to go to function nodes for the maths in terms of charge rates and the like.

Have you got the localvolts api working yet ? I have some code from a FB user (i have not tested it yet - that uses NR to pull the values and then publishes them through MQTT to HA.

You can write a lot of the logic using graphical nodes (which is what my flow uses) and just drops in the charging routines during the 10-3PM window where it then uses Javascript.

Craig

Thanks!

On your point 1 & 2, unfortunately my battery doesn't have an Auto mode, its ten individual LTO batteries with a simple internal BMS with no comms interface.

On point 3, forecasting, maybe later, my head hurts enough without adding that in and forecasting just seems so fricking unreliable.

Atm I don't think I need a set and forget system like yours (unless I'm asleep), I'm home nearly all the time and have easy remote access to control the goodwe through wireguard.

I haven't shifted to LV yet, I'm waiting for Amber to fix my tariffs on August 1st. I'm not anticipating any issues on the API?

I'm working this out in my head as I type...

So, if I focus on the overnight charge, I'm still sort of thinking I need a loop, every 5 minutes read the SoC, target SoC, remaining time...

Say its:

SoC: 35%
Target SoC: 45%
Remaining time: 540 minutes

So the battery being 19.2kw, my charge rate being 3kw and my load being say 0.4kw

I need 3.6kw to maintain and a further 1.92kw to charge the extra 10% so call it 5kw.

So I have 108 dispatch windows and I need 20 blocks of 5 minutes at 3kw to reach the target and the price should be under 22c/kwh (NEM forecasting seems to not be that great really, it's potentially pointless using it to try and save a few cents).

It would be cool to have logic that maybe charged at 1kw or 2w if the price is higher and time is running out.

Night is simpler but to my mind it makes sense to have a loop that can handle day or night as well as be able to adapt.

Anyway just thinking out loud.

Yeah - where it gets harder is to put all of that in a loop that accounts for the following

  1. Spike in the middle of your charging blocks - do you discharge ? Would only know if you look far enough ahead to see how long the spike will last and your battery SOC and work out can you get through the monring peak

  2. Look ahead to the morning peak and decide if procies are high enough to make it worthwhile chargign overnight or leave the battery to run down or what SOC to get to ?

  3. Morning - not so much winter but spring/autumn - what time do you start charging your battery (and how much left over from the night) so that you do not end up in the middle of the day pumping out wasted solar at negative prices.

I already have the logic to take the next X number of amber 30 minutes, put them into an array, sort that array from lowest cost to highest - but then you need to do a further set of optimisations - take the cheapest set of prices and then sort them by time so you know when to start charging - but yeah do you want to charge if a price is over the threshold amount (whatever you set it to ) or do you wnat to skip that block and ramp up the charging speed (that what the function code block does - the less time left to charge the faster it ramps the charge rate up

Craig

Thanks, about a spike in the middle of the night, (with Localvolts) can you just treat every 5 minutes as a separate charge window so you potentially start/stop a lot:

0100 - 0105 charge at 3000w as price is 21c
0105 - 0110 price 23c no charge
0110 - 0140 charge at 3000w as price is 20c
0140 - 0200 price 24c no charge

so in 1 hour you got 7 of the 20 required blocks, with 48 blocks until 6am. Rinse, repeat until 6am.

Or is that silly, not sure?

I don't have an Auto mode (I assume that would just be use power to get straight to target SoC and then maintain by pulling the house load from the grid) so for 5 minute settlement my idea above "feels" better, snatch what you need at a price your happy with, better the devil you know.

You could, in my example have a target SoC lookup table based on price, so under 22c it is 45%, 25c is 40%...

I really don't want to flip to grid ever (happens when I go under <44v even for a second, so at 44.5v the toaster and kettle will dip it below 44v), because my MPP off-grid inverters will keep me on grid until the voltage gets to 48v... So if I flip to grid at 6am I'll be pulling power during the morning spike and 44-48v is a lot of power, nearly 10kw, its a stupid design (probably to protect SLA batteries) but I'm stuck with it.

To handle daytime with its extra variables perhaps recalculating every 5 minutes the number of 3kw full power 5m blocks required.

I essentially need to create an "Auto" mode for my battery in node-red.