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!
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