Electronics > Power/Renewable Energy/EV's

Automatic Solar battery charging timings based on forecast weather

(1/8) > >>

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

There was an error while thanking
Thanking...
Go to full version
Powered by SMFPacks Advanced Attachments Uploader Mod