Electronics > Power/Renewable Energy/EV's
Automatic Solar battery charging timings based on forecast weather
IanJ:
Hi all,
I have a 4kW solar & 10kWh battery system, and an Octopus Flux tariff with my energy provider.
2am to 5am the cheap rate electricity tariff kicks in which is nearly 50% off the normal rate so it makes sense to charge the 10kWh battery during that time ready for the next day.
But of course, if it's going to be sunny the next day then don't bother charging the batteries overnight and let the solar panels do it instead.
Some folks log-in to their battery inverter app manually after looking at tomorrows weather online and making a determination of what timings to put on the battery charging.......yeuccchhhh, I couldn't be bothered!
So, I have automated this with a Windows app running on my home server.
At 11:55pm the app goes off to openweathermap.org via it's API and gets the next days irridiance forecast for my lat/long and then via a ruleset works out how much to charge the batteries the coming 2am to 5am cheap rate.....if at all.
The app them goes off via the Solis API and updates the charging settings in the inverter. I don't lift a finger.
Today is day 1 of this now up and running. Tomorrows irridiance figure is 3699 and so my app has determined a half hour charge should be enough.
I'll have to monitor it over the next few weeks under different weather conditions and adjust the ruleset accordingly, but basically my system is completely hands-off now.
Here's some of the VB.NET code from my app........API keys etc removed.
If anyone else can benefit from it................
PS. It's a Solis battery inverter.
Future works:
Spare battery capacity could be exported back to the grid between 4pm and 7pm which is nearly 30% more than grid cost. Hmmmm!
--- Code: ---Imports System.Security.Cryptography
Imports System.Text
Imports Newtonsoft.Json.Linq
Imports Newtonsoft.Json
Imports System.Net.Http
Imports System.Globalization
Imports System.Net.Http.Headers
Imports System.Text.RegularExpressions
Imports System
Imports System.Threading
Imports System.Runtime.InteropServices
Partial Class FormMain
Private Sub ButtonChargeSet_Click(sender As Object, e As EventArgs) Handles ButtonChargeSet.Click
If CheckBox7.Checked And CheckBox8.Checked Then
Call OpenWeatherIrridiance() ' Manually get irridiance figures and then send updated settings back to Solis Inverter (battery charge discharge timings)
End If
End Sub
Private Sub SendToSolis()
Try
' Check user entered data, abort sub with pop-up if wrong
Dim Charge1SetOn As String = TextBoxChargeSetOn.Text
Dim Charge1SetOff As String = TextBoxChargeSetOff.Text
' Define the regex pattern for XX:XX format
Dim timePattern As String = "^\d{2}:\d{2}$"
' Check if the strings match the pattern
If Not Regex.IsMatch(Charge1SetOn, timePattern) OrElse Not Regex.IsMatch(Charge1SetOff, timePattern) Then
' If either string does not match the pattern, exit the sub
MessageBox.Show("Please enter the time in the format XX:XX")
Exit Sub
End If
Dim key As String = "#####################" ' Private key from Solis
Dim keySecret As String = "############################" ' Secret key from Solis
' Create a comma-separated value for cid 103 using the inputs and default values
Dim value As String = $"70,50,{Charge1SetOn},{Charge1SetOff},00:00,00:00,70,50,00:00,00:00,00:00,00:00,70,50,00:00,00:00,00:00,00:00"
' Create the map for the API request
Dim map As New Dictionary(Of String, Object) From {
{"inverterSn", "##############"}, ' Replace with your actual inverter serial number
{"cid", "103"},
{"value", value} ' Set the parameters as a single comma-separated string
}
' Serialize the map to JSON for the API request
Dim body As String = JsonConvert.SerializeObject(map)
Dim ContentMd5 As String = GetDigest(body)
Dim [Date] As String = GetGMTTime()
Dim path As String = "/v2/api/control"
Dim param As String = "POST" & vbLf & ContentMd5 & vbLf & "application/json" & vbLf & [Date] & vbLf & path
Dim sign As String = HmacSHA1Encrypt(param, keySecret)
Dim url As String = "https://www.soliscloud.com:13333" & path ' URL for the control endpoint
Dim client As New HttpClient()
Dim requestBody As HttpContent = New StringContent(body, Encoding.UTF8, "application/json")
' Set Content-Type and Content-MD5
requestBody.Headers.ContentType = New MediaTypeHeaderValue("application/json") With {
.CharSet = "UTF-8"
}
requestBody.Headers.ContentMD5 = Convert.FromBase64String(ContentMd5)
Dim request As New HttpRequestMessage(HttpMethod.Post, url)
request.Headers.Add("Authorization", "API " & key & ":" & sign)
request.Headers.Add("Date", [Date])
request.Content = requestBody
' Send the request
Dim response As HttpResponseMessage = client.SendAsync(request).Result ' Non-async for timing purposes
Dim result As String = response.Content.ReadAsStringAsync().Result
RunningSeq.Text = "Irridiance received, Battery settings updated"
Catch ex As Exception
Console.WriteLine(ex.ToString())
End Try
End Sub
Private Sub CheckBox7_CheckedChanged(sender As Object, e As EventArgs) Handles CheckBox7.CheckedChanged
If CheckBox7.Checked = True Then
ButtonChargeSet.Enabled = True
Else
ButtonChargeSet.Enabled = False
End If
End Sub
Private Sub OpenWeatherIrridiance()
On Error GoTo ErrorHandler
' get solar irridiance from openweathermap.org
' API = ##################################
' appid = API key
If CheckBox8.Checked = True Then
Dim TomorrowDate As String = DateTime.Now.AddDays(1).ToString("yyyy-MM-dd") ' tomorrow date, so run this sub before midnight
Dim IrridianceAPIkey As String = "################################"
' openweathermap ping retry
Dim deviceAddress As String = "openweathermap.org"
Dim maxRetries As Integer = 3
Dim retryDelaySeconds As Double = 0.2
' Call Function
If TryPingDevice(deviceAddress, maxRetries, retryDelaySeconds) Then
' Ping was successful, continue with your specific actions
Dim IrridianceData As String = ""
IrridianceData = New System.Net.WebClient().DownloadString("https://api.openweathermap.org/energy/1.0/solar/data?lat=56.9734&lon=-2.2252&date=" & TomorrowDate & "&" & "appid=" & IrridianceAPIkey)
IsOkIrridiance = True
' Turn on the LED
LEDirridiance.State = OnOffLed.LedState.OnSmallYellow
' Start the timer so the LED will light for 1sec
IndicatorTimer3.Interval = 2000 ' 1 second
IndicatorTimer3.Start()
' Sample return
' {"lat":56.9734,"lon":2.2252,"date":"2024-09-12","tz":"+00:00","sunrise":"2024-09-12T05:16:09","sunset":"2024-09-12T18:16:56","irradiance":{"daily":[{"clear_sky":{"ghi":4454.41,"dni":8160.83,"dhi":974.92},"cloudy_sky":{"ghi":1176.86,"dni":0.0,"dhi":1176.86}}],"hourly":[{"hour":0,"clear_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0},"cloudy_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0}},{"hour":1,"clear_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0},"cloudy_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0}},{"hour":2,"clear_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0},"cloudy_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0}},{"hour":3,"clear_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0},"cloudy_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0}},{"hour":4,"clear_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0},"cloudy_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0}},{"hour":5,"clear_sky":{"ghi":13.3,"dni":92.91,"dhi":13.27},"cloudy_sky":{"ghi":3.83,"dni":0.0,"dhi":3.83}},{"hour":6,"clear_sky":{"ghi":112.37,"dni":436.99,"dhi":48.97},"cloudy_sky":{"ghi":31.06,"dni":0.0,"dhi":31.06}},{"hour":7,"clear_sky":{"ghi":244.8,"dni":617.18,"dhi":70.05},"cloudy_sky":{"ghi":61.2,"dni":0.0,"dhi":61.2}},{"hour":8,"clear_sky":{"ghi":372.29,"dni":715.88,"dhi":83.97},"cloudy_sky":{"ghi":98.83,"dni":0.0,"dhi":98.83}},{"hour":9,"clear_sky":{"ghi":478.25,"dni":774.19,"dhi":93.27},"cloudy_sky":{"ghi":136.31,"dni":0.0,"dhi":136.31}},{"hour":10,"clear_sky":{"ghi":551.52,"dni":806.91,"dhi":98.91},"cloudy_sky":{"ghi":153.17,"dni":0.0,"dhi":153.17}},{"hour":11,"clear_sky":{"ghi":584.97,"dni":820.27,"dhi":101.33},"cloudy_sky":{"ghi":160.0,"dni":0.0,"dhi":160.0}},{"hour":12,"clear_sky":{"ghi":575.45,"dni":816.47,"dhi":100.67},"cloudy_sky":{"ghi":151.91,"dni":0.0,"dhi":151.91}},{"hour":13,"clear_sky":{"ghi":523.81,"dni":794.88,"dhi":96.9},"cloudy_sky":{"ghi":130.95,"dni":0.0,"dhi":130.95}},{"hour":14,"clear_sky":{"ghi":434.94,"dni":751.78,"dhi":89.75},"cloudy_sky":{"ghi":108.77,"dni":0.0,"dhi":108.77}},{"hour":15,"clear_sky":{"ghi":317.69,"dni":678.03,"dhi":78.58},"cloudy_sky":{"ghi":79.5,"dni":0.0,"dhi":79.5}},{"hour":16,"clear_sky":{"ghi":185.27,"dni":550.99,"dhi":61.93},"cloudy_sky":{"ghi":46.36,"dni":0.0,"dhi":46.36}},{"hour":17,"clear_sky":{"ghi":59.13,"dni":298.83,"dhi":35.37},"cloudy_sky":{"ghi":14.8,"dni":0.0,"dhi":14.8}},{"hour":18,"clear_sky":{"ghi":0.62,"dni":5.51,"dhi":1.96},"cloudy_sky":{"ghi":0.15,"dni":0.0,"dhi":0.15}},{"hour":19,"clear_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0},"cloudy_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0}},{"hour":20,"clear_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0},"cloudy_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0}},{"hour":21,"clear_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0},"cloudy_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0}},{"hour":22,"clear_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0},"cloudy_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0}},{"hour":23,"clear_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0},"cloudy_sky":{"ghi":0.0,"dni":0.0,"dhi":0.0}}]}}
' Pull total irridiance value from returned string
If (IrridianceData.Length() > 2000) Then ' usually 2885 approx.
' Your JSON string
Dim json As String = IrridianceData
' Parse the JSON string
Dim data As JObject = JObject.Parse(json)
' Get the hourly irradiance array
Dim hourlyIrradiance As JArray = data("irradiance")("hourly")
' Initialize variables to store the total irradiance for both clear and cloudy skies
Dim totalClearSkyIrradiance As Double = 0
Dim totalCloudySkyIrradiance As Double = 0
' Loop through each hourly object and sum the "ghi" (global horizontal irradiance) for both clear and cloudy skies
For Each hourData As JObject In hourlyIrradiance
Dim clearSkyGhi As Double = hourData("clear_sky")("ghi")
Dim cloudySkyGhi As Double = hourData("cloudy_sky")("ghi")
totalClearSkyIrradiance += clearSkyGhi
totalCloudySkyIrradiance += cloudySkyGhi
Next
' Output the total summed irradiance for both clear and cloudy skies
Console.WriteLine("Total summed irradiance for the day (clear sky): " & totalClearSkyIrradiance.ToString())
Console.WriteLine("Total summed irradiance for the day (cloudy sky): " & totalCloudySkyIrradiance.ToString())
' You can use either of the summed values or calculate an average for decision making
Dim overallIrradiance As Double = (totalClearSkyIrradiance + totalCloudySkyIrradiance) / 2
Irridiance.Text = overallIrradiance
' Output the overall irradiance
Console.WriteLine("Overall summed irradiance: " & overallIrradiance.ToString())
' now determine if batteries should charge. Figures below from ChatGPT
' Overcast days: 200-500 W/m²
' Cloudy days: 500-1000 W/m²
' Cloudy/sunny days: 1000-2000 W/m²
' Sunny winter days: 2000-3000 W/m²
' Sunny summer days: 5000-7000 W/m²
If overallIrradiance <= 500 Then
TextBoxChargeSetOn.Text = "02:00"
TextBoxChargeSetOff.Text = "05:00"
End If
If overallIrradiance > 500 And overallIrradiance <= 1000 Then
TextBoxChargeSetOn.Text = "02:00"
TextBoxChargeSetOff.Text = "04:30"
End If
If overallIrradiance > 1000 And overallIrradiance <= 2000 Then
TextBoxChargeSetOn.Text = "02:00"
TextBoxChargeSetOff.Text = "04:00"
End If
If overallIrradiance > 2000 And overallIrradiance <= 3000 Then
TextBoxChargeSetOn.Text = "02:00"
TextBoxChargeSetOff.Text = "03:30"
End If
If overallIrradiance > 3000 And overallIrradiance <= 5000 Then
TextBoxChargeSetOn.Text = "02:00"
TextBoxChargeSetOff.Text = "02:30"
End If
If overallIrradiance > 5000 And overallIrradiance <= 7000 Then
TextBoxChargeSetOn.Text = "00:00"
TextBoxChargeSetOff.Text = "00:00"
End If
My.Settings.data40 = TextBoxChargeSetOn.Text
My.Settings.data41 = TextBoxChargeSetOff.Text
My.Settings.Save()
Call SendToSolis() ' Got the irridiance value so can now send the required settings to Solis
' Turn off the LED
LEDirridiance.State = OnOffLed.LedState.OffSmallBlack
End If
Else
Dim currentDateAndTime As DateTime = DateTime.Now
Dim formattedDateTime As String = currentDateAndTime.ToString("dd-MM-yyyy HH:mm", CultureInfo.InvariantCulture)
ErrorCode.Text = formattedDateTime & " " & "openweathermap.org ping fail" 'ToErrorString(Err) ' display error status
IsOkIrridiance = False
LEDirridiance.State = OnOffLed.LedState.OffSmall ' fail RED led
Exit Sub
End If
End If
ErrorHandler:
End Sub
Private Async Sub Battery()
Dim batteryPower As Double = 0
If CheckBox4.Checked Then ' Solar read must have been done successfully before OB418 read can take place
' Async/Await: The HttpClient operations are now asynchronous, preventing the UI thread from being blocked.
Try
Dim key As String = "######################" ' Private key from Solis
Dim keySecret As String = "################################" ' Secret key from Solis
Dim map As New Dictionary(Of String, Object) From {
{"pageNo", 1},
{"pageSize", 10}
}
Dim body As String = JsonConvert.SerializeObject(map)
Dim ContentMd5 As String = GetDigest(body)
Dim [Date] As String = GetGMTTime()
Dim path As String = "/v1/api/inverterList"
Dim param As String = "POST" & vbLf & ContentMd5 & vbLf & "application/json" & vbLf & [Date] & vbLf & path
Dim sign As String = HmacSHA1Encrypt(param, keySecret)
Dim url As String = "https://www.soliscloud.com:13333" & path ' Url from Solis
Dim client As New HttpClient()
Dim requestBody As HttpContent = New StringContent(body, Encoding.UTF8, "application/json")
' Set Content-Type and Content-MD5 directly when creating StringContent
requestBody.Headers.ContentType = New MediaTypeHeaderValue("application/json") With {
.CharSet = "UTF-8"
}
requestBody.Headers.ContentMD5 = Convert.FromBase64String(ContentMd5)
Dim request As New HttpRequestMessage(HttpMethod.Post, url)
request.Headers.Add("Authorization", "API " & key & ":" & sign)
request.Headers.Add("Date", [Date])
request.Content = requestBody
Dim response As HttpResponseMessage = client.SendAsync(request).Result ' non-asynchronous - Sub will wait for response, stopwatch records properly
Dim result As String = response.Content.ReadAsStringAsync().Result
' Now pull data from JSON string
Dim jsonObject As JObject = JObject.Parse(result) ' Parse the JSON string
' Access the "records" array within the "page" property
Dim recordsArray As JArray = jsonObject.SelectToken("data.page.records")
' Check if the "records" array is not null and contains at least one item
If recordsArray IsNot Nothing AndAlso recordsArray.Any() Then
' Access the first item in the "records" array
Dim firstRecord As JObject = recordsArray.First
' Access the "batteryCapacitySoc" property within the first record
Dim batteryCapacitySoc As Double = 0
If firstRecord.TryGetValue("batteryCapacitySoc", batteryCapacitySoc) Then
IsOkBattery = True
LEDbattery.State = OnOffLed.LedState.OnSmall
BatteryCapacity.Text = Math.Round(batteryCapacitySoc, 0).ToString() ' 0dp
Else
IsOkBattery = False
LEDbattery.State = OnOffLed.LedState.OffSmall
End If
' Access the "batterypower" property within the first record, also charging status
If DataGlitch = False Then
If firstRecord.TryGetValue("batteryPower", batteryPower) Then
IsOkBattery = True
LEDbattery.State = OnOffLed.LedState.OnSmall
If batteryPower > 0 Then
BattStatus.Text = "Charging"
ElseIf batteryPower < 0 Then
BattStatus.Text = "Discharging"
Else
BattStatus.Text = "Static"
End If
batteryPower *= 1000 ' kW to W
BattChg.Text = Math.Round(batteryPower, 0).ToString() ' 0dp
Else
IsOkBattery = False
LEDbattery.State = OnOffLed.LedState.OffSmall
End If
End If
' House Consumption
If DataGlitch = False Then
Dim solarWValue As Double = Val(SolarW.Text)
Dim consumptionValue As Double = solarWValue + OB418DataMeterPwr - batteryPower
Consumption.Text = FormatNumber(consumptionValue, 1).Replace(",", "")
End If
DataGlitch = False ' reset glitch flag
RunningSeq.Text = "Received Solis battery data"
Else
' Handle the case where "records" array is empty or null
IsOkBattery = False
LEDbattery.State = OnOffLed.LedState.OffSmall
End If
Catch ex As Exception
Console.WriteLine(ex.ToString())
End Try
End If
End Sub
Function HmacSHA1Encrypt(encryptText As String, KeySecret As String) As String
Dim data As Byte() = Encoding.UTF8.GetBytes(KeySecret)
Dim secretKey As New HMACSHA1(data)
Dim text As Byte() = Encoding.UTF8.GetBytes(encryptText)
Dim result As Byte() = secretKey.ComputeHash(text)
Return Convert.ToBase64String(result)
End Function
Function GetGMTTime() As String
Return DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture)
End Function
Function GetDigest(test As String) As String
Dim result As String = ""
Try
Using md5 As System.Security.Cryptography.MD5 = System.Security.Cryptography.MD5.Create()
Dim data As Byte() = md5.ComputeHash(Encoding.UTF8.GetBytes(test))
result = Convert.ToBase64String(data)
End Using
Catch ex As Exception
Console.WriteLine(ex.ToString())
End Try
Return result
End Function
End Class
--- End code ---
Siwastaja:
Interesting to hear!
I mean, we do this as a product/service. It's a physical box which connects to the battery inverter through RS485 or modbus TCP (Solis hybrid being one of the supported models) and to the Internet to get prices/weather forecasts/configuration/user bypass controls from our cloud service. So far it hasn't taken weather forecasts into account for battery optimization, and people have had to fine-tune optimization algorithm settings to get good results and that has sucked to the point of causing some well deserved complaints.
We made it the simple way by having a control for maximum SoC during night/early morning charging, instead of controlling it by the length of charge time. This way, if there is still excess solar from previous day, nothing is put into the battery, but if it is empty, a little bit charging is done to supply the morning peak use before the generation really exceeds consumption. But even then this requires manual tuning of this "charge up to SoC%" setting; in winter you want to increase it, maybe up to nearly 100%. And if you really want good results, even daily increase it for cloudy days. Sucks.
As such now I'm in progress of writing a more comprehensive battery optimization algorithm and can share some ideas, like:
Take consumption history into account automatically, too. I'm now doing this by tracking energy meter net sums which are negative on export, positive on import. Track hourly minimums over a few weeks of history and they represent what would be exported on a fully sunny day (well assuming there was at least one sunny day within the period). Net forecast is better than production forecast because maybe there is some load which often/always comes on at a certain time.
When hourly net forecasts show export, multiple them by hourly solar level estimates, sum them up for the day and now you know more exactly how much room you have to reserve into the battery.
I have this tendency that when it comes to designing an algorithm with complex interactions, at some point I just resort to some brute-forcing. So create a model (input with price, weather, energy consumption/production forecasts which are constant over iterations) which runs with not too much CPU time and loop over thousands of different combinations of controls, outputting money made for each set of parameters. Then pick the winner and possibly finetune it.
IanJ:
I did talk to Solis back in January when I first had this idea, and they were receptive but that's as far as it went. I was actually hoping Solis would integrate this functionality into their inverters.....more sales!
I already had an app running monitoring my energy usage, Solar PV output etc.......it made sense to extend the app. I know some folks are using Home Assistant for something similar.
Thanks for the tips, I'm quite eager to see how things go.......I'll probably extend my app to record irridiance forecast values against battery capacity to see where my ruleset needs tweaked......summer and winter.
Ian.
IanJ:
Here's my app.......It doubles as an Aircon control app for my workshop.
It's quite a dense form, the server display is only 1024x768 and runs on a touchscreen on my workshop wall.
The app also writes a webpage containing most of the stats and graphs and pops it onto my webserver so I can access from anywhere.
mag_therm:
Ian, that looks good.
I have been developing/testing a similar system since Jan 2024 on the mini solar battery system here used to power the ham radio station.
With two NOS panels,in parallel, there is a nominal 3 kW.h 42 V bus and a 1 kW.h 13.8V bus each controlled by two 300 W home brew DC converters.
The outer loop of the converters is controlled, along with a 14 point datalogger, by python via tty on linux.
I use Pyowm API to obtain next day UV Index. Based on UV Index, a calculation of battery ullage is made at 8:00pm local.(0 UTC)
Fo example, UV Index of 10 will switch off the Utility earlier in pm than a UVI of 4.
https://app.box.com/s/a3pfju14qjllbnp3qdwjj258gs603atp
It basically works but there are problems to overcome. The main one is intermittent clouds that occur in this area through summer.
The pyowm is inadequately documented for me, and does not seem to provide an enumeration of forecast cloud cover (as used in aviation for example)
A further problem is that the batteries are floating, and can not take all the solar power available.
I am thinking of adding a third "raw" bus with Li battery and a separate bidirectional controller to take the presently unusable energy.
I will look at openweathermap.org when I do next round of python updates ( next winter).
Navigation
[0] Message Index
[#] Next page
Go to full version