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.


Visual Studio VB.NET
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