Project 027 - Solis Cloud API - VB.NET sample code
DISCLAIMER: This design is experimental, so if you decide to build one yourself then you are on your own, I can't be held responsible for any problems/issues/damage/injury that may occur if you decide to follow this build and make one yourself.
NOTICE: In 2024 I retired this code and method of controlling my Solis Inverter, I migrated the functionality over to Home Assistant and at the same time incorporated ModBus comms in order to talk directly to the inverter whilst also still having the SolisCloud comms active. I'll post this in a new article at some point.
I have a Solis Inverter (non-hybrid) connected to 10.65kWh Lithium-Iron-Phosphate batteries in the house.
I have a VB app running on a web server in the house that I use to monitor and control a whol;e host of energy related hardware including my workshop aircon, and wanted to extend it's functionality to monitor my new Solis Inverter.
I also pick up weather forecast data for the next day via OpenWeatherMap.org API and determine what charging should happen overnight (Octopus Flux Tarif, cheap rate 2-5am), and update the Solis inverter charging timings.
I don't offer much of an explanation of how the code works, I just quote it below to give anyone else some ideas since the Solis API is quite a feat to understand when you also consider the API is a 122 page document.
Important: You cannot access the API data unless you have activated it via Solis. You will need to sign a document. You also need an OpenWeatherMap.org API account.
Here's snippets of my VB.net code, note the real Key and Secret Key are omitted for obvious reasons. Hopefully, this code will give you some ideas on developing your own system.
1Imports System.Security.Cryptography2Imports System.Text3Imports Newtonsoft.Json.Linq4Imports Newtonsoft.Json5Imports System.Net.Http6Imports System.Globalization7Imports System.Net.Http.Headers8Imports System.Text.RegularExpressions9Imports System10Imports System.Threading11Imports System.Runtime.InteropServices12 13Partial Class FormMain14 15 Private Sub ButtonChargeSet_Click(sender As Object, e As EventArgs) Handles ButtonChargeSet.Click16 If CheckBox7.Checked And CheckBox8.Checked Then17 Call OpenWeatherIrridiance() ' Manually get irridiance figures and then send updated settings back to Solis Inverter (battery charge discharge timings)18 End If19 End Sub20 21 Private Sub SendToSolis()22 Try23 ' Check user entered data, abort sub with pop-up if wrong24 Dim Charge1SetOn As String = TextBoxChargeSetOn.Text25 Dim Charge1SetOff As String = TextBoxChargeSetOff.Text26 ' Define the regex pattern for XX:XX format27 Dim timePattern As String = "^\d{2}:\d{2}$"28 ' Check if the strings match the pattern29 If Not Regex.IsMatch(Charge1SetOn, timePattern) OrElse Not Regex.IsMatch(Charge1SetOff, timePattern) Then30 ' If either string does not match the pattern, exit the sub31 MessageBox.Show("Please enter the time in the format XX:XX")32 Exit Sub33 End If34 35 Dim key As String = "#####################" ' Private key from Solis36 Dim keySecret As String = "############################" ' Secret key from Solis37 38 ' Create a comma-separated value for cid 103 using the inputs and default values39 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"40 41 ' Create the map for the API request42 Dim map As New Dictionary(Of String, Object) From {43 {"inverterSn", "##############"}, ' Replace with your actual inverter serial number44 {"cid", "103"},45 {"value", value} ' Set the parameters as a single comma-separated string46 }47 48 ' Serialize the map to JSON for the API request49 Dim body As String = JsonConvert.SerializeObject(map)50 Dim ContentMd5 As String = GetDigest(body)51 Dim [Date] As String = GetGMTTime()52 Dim path As String = "/v2/api/control"53 Dim param As String = "POST" & vbLf & ContentMd5 & vbLf & "application/json" & vbLf & [Date] & vbLf & path54 Dim sign As String = HmacSHA1Encrypt(param, keySecret)55 Dim url As String = "https://www.soliscloud.com:13333" & path ' URL for the control endpoint56 Dim client As New HttpClient()57 Dim requestBody As HttpContent = New StringContent(body, Encoding.UTF8, "application/json")58 59 ' Set Content-Type and Content-MD560 requestBody.Headers.ContentType = New MediaTypeHeaderValue("application/json") With {61 .CharSet = "UTF-8"62 }63 requestBody.Headers.ContentMD5 = Convert.FromBase64String(ContentMd5)64 65 Dim request As New HttpRequestMessage(HttpMethod.Post, url)66 request.Headers.Add("Authorization", "API " & key & ":" & sign)67 request.Headers.Add("Date", [Date])68 request.Content = requestBody69 70 ' Send the request71 Dim response As HttpResponseMessage = client.SendAsync(request).Result ' Non-async for timing purposes72 Dim result As String = response.Content.ReadAsStringAsync().Result73 74 RunningSeq.Text = "Irridiance received, Battery settings updated"75 76 Catch ex As Exception77 Console.WriteLine(ex.ToString())78 End Try79 80 End Sub81 82 Private Sub CheckBox7_CheckedChanged(sender As Object, e As EventArgs) Handles CheckBox7.CheckedChanged83 If CheckBox7.Checked = True Then84 ButtonChargeSet.Enabled = True85 Else86 ButtonChargeSet.Enabled = False87 End If88 End Sub89 90 Private Sub OpenWeatherIrridiance()91 On Error GoTo ErrorHandler92 93 ' get solar irridiance from openweathermap.org94 ' API = ##################################95 ' appid = API key96 97 If CheckBox8.Checked = True Then98 99 Dim TomorrowDate As String = DateTime.Now.AddDays(1).ToString("yyyy-MM-dd") ' tomorrow date, so run this sub before midnight100 Dim IrridianceAPIkey As String = "################################"101 ' openweathermap ping retry102 Dim deviceAddress As String = "openweathermap.org"103 Dim maxRetries As Integer = 3104 Dim retryDelaySeconds As Double = 0.2105 106 ' Call Function107 If TryPingDevice(deviceAddress, maxRetries, retryDelaySeconds) Then108 ' Ping was successful, continue with your specific actions109 110 Dim IrridianceData As String = ""111 112 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)113 IsOkIrridiance = True114 115 ' Turn on the LED116 LEDirridiance.State = OnOffLed.LedState.OnSmallYellow117 ' Start the timer so the LED will light for 1sec118 IndicatorTimer3.Interval = 2000 ' 1 second119 IndicatorTimer3.Start()120 121 ' Sample return122 ' {"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}}]}}123 124 ' Pull total irridiance value from returned string125 If (IrridianceData.Length() > 2000) Then ' usually 2885 approx.126 127 ' Your JSON string128 Dim json As String = IrridianceData129 130 ' Parse the JSON string131 Dim data As JObject = JObject.Parse(json)132 133 ' Get the hourly irradiance array134 Dim hourlyIrradiance As JArray = data("irradiance")("hourly")135 136 ' Initialize variables to store the total irradiance for both clear and cloudy skies137 Dim totalClearSkyIrradiance As Double = 0138 Dim totalCloudySkyIrradiance As Double = 0139 140 ' Loop through each hourly object and sum the "ghi" (global horizontal irradiance) for both clear and cloudy skies141 For Each hourData As JObject In hourlyIrradiance142 Dim clearSkyGhi As Double = hourData("clear_sky")("ghi")143 Dim cloudySkyGhi As Double = hourData("cloudy_sky")("ghi")144 145 totalClearSkyIrradiance += clearSkyGhi146 totalCloudySkyIrradiance += cloudySkyGhi147 Next148 149 ' Output the total summed irradiance for both clear and cloudy skies150 Console.WriteLine("Total summed irradiance for the day (clear sky): " & totalClearSkyIrradiance.ToString())151 Console.WriteLine("Total summed irradiance for the day (cloudy sky): " & totalCloudySkyIrradiance.ToString())152 153 ' You can use either of the summed values or calculate an average for decision making154 Dim overallIrradiance As Double = (totalClearSkyIrradiance + totalCloudySkyIrradiance) / 2155 156 Irridiance.Text = overallIrradiance157 158 ' Output the overall irradiance159 Console.WriteLine("Overall summed irradiance: " & overallIrradiance.ToString())160 161 ' now determine if batteries should charge. Figures below from ChatGPT162 ' Overcast days: 200-500 W/m²163 ' Cloudy days: 500-1000 W/m²164 ' Cloudy/sunny days: 1000-2000 W/m²165 ' Sunny winter days: 2000-3000 W/m²166 ' Sunny summer days: 5000-7000 W/m²167 168 If overallIrradiance <= 500 Then169 TextBoxChargeSetOn.Text = "02:00"170 TextBoxChargeSetOff.Text = "05:00"171 End If172 If overallIrradiance > 500 And overallIrradiance <= 1000 Then173 TextBoxChargeSetOn.Text = "02:00"174 TextBoxChargeSetOff.Text = "04:30"175 End If176 If overallIrradiance > 1000 And overallIrradiance <= 2000 Then177 TextBoxChargeSetOn.Text = "02:00"178 TextBoxChargeSetOff.Text = "04:00"179 End If180 If overallIrradiance > 2000 And overallIrradiance <= 3000 Then181 TextBoxChargeSetOn.Text = "02:00"182 TextBoxChargeSetOff.Text = "03:30"183 End If184 If overallIrradiance > 3000 And overallIrradiance <= 5000 Then185 TextBoxChargeSetOn.Text = "02:00"186 TextBoxChargeSetOff.Text = "02:30"187 End If188 If overallIrradiance > 5000 And overallIrradiance <= 7000 Then189 TextBoxChargeSetOn.Text = "00:00"190 TextBoxChargeSetOff.Text = "00:00"191 End If192 193 My.Settings.data40 = TextBoxChargeSetOn.Text194 My.Settings.data41 = TextBoxChargeSetOff.Text195 My.Settings.Save()196 197 Call SendToSolis() ' Got the irridiance value so can now send the required settings to Solis198 199 ' Turn off the LED200 LEDirridiance.State = OnOffLed.LedState.OffSmallBlack201 End If202 203 Else204 Dim currentDateAndTime As DateTime = DateTime.Now205 Dim formattedDateTime As String = currentDateAndTime.ToString("dd-MM-yyyy HH:mm", CultureInfo.InvariantCulture)206 ErrorCode.Text = formattedDateTime & " " & "openweathermap.org ping fail" 'ToErrorString(Err) ' display error status207 IsOkIrridiance = False208 LEDirridiance.State = OnOffLed.LedState.OffSmall ' fail RED led209 Exit Sub210 End If211 212 End If213 214ErrorHandler:215 End Sub216 217 Private Async Sub Battery()218 Dim batteryPower As Double = 0219 If CheckBox4.Checked Then ' Solar read must have been done successfully before OB418 read can take place220 ' Async/Await: The HttpClient operations are now asynchronous, preventing the UI thread from being blocked.221 Try222 Dim key As String = "######################" ' Private key from Solis223 Dim keySecret As String = "################################" ' Secret key from Solis224 225 Dim map As New Dictionary(Of String, Object) From {226 {"pageNo", 1},227 {"pageSize", 10}228 }229 Dim body As String = JsonConvert.SerializeObject(map)230 Dim ContentMd5 As String = GetDigest(body)231 Dim [Date] As String = GetGMTTime()232 Dim path As String = "/v1/api/inverterList"233 Dim param As String = "POST" & vbLf & ContentMd5 & vbLf & "application/json" & vbLf & [Date] & vbLf & path234 Dim sign As String = HmacSHA1Encrypt(param, keySecret)235 Dim url As String = "https://www.soliscloud.com:13333" & path ' Url from Solis236 Dim client As New HttpClient()237 Dim requestBody As HttpContent = New StringContent(body, Encoding.UTF8, "application/json")238 239 ' Set Content-Type and Content-MD5 directly when creating StringContent240 requestBody.Headers.ContentType = New MediaTypeHeaderValue("application/json") With {241 .CharSet = "UTF-8"242 }243 requestBody.Headers.ContentMD5 = Convert.FromBase64String(ContentMd5)244 245 Dim request As New HttpRequestMessage(HttpMethod.Post, url)246 request.Headers.Add("Authorization", "API " & key & ":" & sign)247 request.Headers.Add("Date", [Date])248 request.Content = requestBody249 250 Dim response As HttpResponseMessage = client.SendAsync(request).Result ' non-asynchronous - Sub will wait for response, stopwatch records properly251 Dim result As String = response.Content.ReadAsStringAsync().Result252 253 ' Now pull data from JSON string254 Dim jsonObject As JObject = JObject.Parse(result) ' Parse the JSON string255 256 ' Access the "records" array within the "page" property257 Dim recordsArray As JArray = jsonObject.SelectToken("data.page.records")258 259 ' Check if the "records" array is not null and contains at least one item260 If recordsArray IsNot Nothing AndAlso recordsArray.Any() Then261 ' Access the first item in the "records" array262 Dim firstRecord As JObject = recordsArray.First263 264 ' Access the "batteryCapacitySoc" property within the first record265 Dim batteryCapacitySoc As Double = 0266 If firstRecord.TryGetValue("batteryCapacitySoc", batteryCapacitySoc) Then267 IsOkBattery = True268 LEDbattery.State = OnOffLed.LedState.OnSmall269 270 BatteryCapacity.Text = Math.Round(batteryCapacitySoc, 0).ToString() ' 0dp271 272 Else273 IsOkBattery = False274 LEDbattery.State = OnOffLed.LedState.OffSmall275 End If276 277 ' Access the "batterypower" property within the first record, also charging status278 If DataGlitch = False Then279 If firstRecord.TryGetValue("batteryPower", batteryPower) Then280 281 IsOkBattery = True282 LEDbattery.State = OnOffLed.LedState.OnSmall283 284 If batteryPower > 0 Then285 BattStatus.Text = "Charging"286 ElseIf batteryPower < 0 Then287 BattStatus.Text = "Discharging"288 Else289 BattStatus.Text = "Static"290 End If291 292 batteryPower *= 1000 ' kW to W293 BattChg.Text = Math.Round(batteryPower, 0).ToString() ' 0dp294 Else295 IsOkBattery = False296 LEDbattery.State = OnOffLed.LedState.OffSmall297 End If298 End If299 300 ' House Consumption301 If DataGlitch = False Then302 Dim solarWValue As Double = Val(SolarW.Text)303 Dim consumptionValue As Double = solarWValue + OB418DataMeterPwr - batteryPower304 305 Consumption.Text = FormatNumber(consumptionValue, 1).Replace(",", "")306 End If307 308 DataGlitch = False ' reset glitch flag309 RunningSeq.Text = "Received Solis battery data"310 311 Else312 ' Handle the case where "records" array is empty or null313 IsOkBattery = False314 LEDbattery.State = OnOffLed.LedState.OffSmall315 End If316 317 Catch ex As Exception318 Console.WriteLine(ex.ToString())319 End Try320 321 End If322 End Sub323 324 Function HmacSHA1Encrypt(encryptText As String, KeySecret As String) As String325 Dim data As Byte() = Encoding.UTF8.GetBytes(KeySecret)326 Dim secretKey As New HMACSHA1(data)327 Dim text As Byte() = Encoding.UTF8.GetBytes(encryptText)328 Dim result As Byte() = secretKey.ComputeHash(text)329 Return Convert.ToBase64String(result)330 End Function331 332 Function GetGMTTime() As String333 Return DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture)334 End Function335 336 Function GetDigest(test As String) As String337 Dim result As String = ""338 Try339 Using md5 As System.Security.Cryptography.MD5 = System.Security.Cryptography.MD5.Create()340 Dim data As Byte() = md5.ComputeHash(Encoding.UTF8.GetBytes(test))341 result = Convert.ToBase64String(data)342 End Using343 Catch ex As Exception344 Console.WriteLine(ex.ToString())345 End Try346 Return result347 End Function348 349End ClassCodequote by Ian Johnston

