Skip to content

Instantly share code, notes, and snippets.

@tathamoddie
Created July 30, 2022 07:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tathamoddie/5df503ce176a4b024dc545da2b065f70 to your computer and use it in GitHub Desktop.
Save tathamoddie/5df503ce176a4b024dc545da2b065f70 to your computer and use it in GitHub Desktop.
Run a solar and battery simulation using real historical data
<#
.SYNOPSIS
Run a solar and battery simulation using real historical data
.DESCRIPTION
Combines real usage data from Powershop's export format, in 30-minute buckets,
with real regional solar performance data from https://pv-map.apvi.org.au/live,
in 15-minute buckets, to simulate the effectiveness of a solar and battery system.
.EXAMPLE
.\Run-Simulation.ps1 `
-PeakGeneration 5.46 `
-BatteryCapacity 9.59 `
-FirstTwoDigitsOfPostcode '30' `
-PowershopUsageExportPath .\meter_usage_data.csv `
| ConvertTo-Csv `
| Out-File result.csv
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, HelpMessage = "Peak solar generation in kW")]
[double]
$PeakGeneration,
[Parameter(Mandatory = $true, HelpMessage = "Battery capacity in kWh")]
[double]
$BatteryCapacity,
[Parameter(Mandatory = $true, HelpMessage = "First two digits of site's postcode, aligned to https://pv-map.apvi.org.au/live")]
[string]
$FirstTwoDigitsOfPostcode,
[Parameter(Mandatory = $true, HelpMessage = "Path to Powershop usage export CSV")]
[ValidateNotNullOrEmpty()]
[string]
$PowershopUsageExportPath
)
$ErrorActionPreference = 'Stop'
$powershopUsageData = Import-Csv -Path $PowershopUsageExportPath
$currentBatteryStorage = 0
$daysProcessed = 0;
foreach ($usageDay in $powershopUsageData) {
Write-Progress -Activity "Running simulation" -Status $usageDay.DATE -PercentComplete ($daysProcessed / $powershopUsageData.Length * 100)
$daysProcessed++;
$batteryStorageAtStartOfDay = $currentBatteryStorage
$gridImportToday = 0
$gridExportToday = 0
$totalProducedToday = 0
$totalConsumedToday = 0
$totalConsumedFromSolarToday = 0
$totalConsumedFromBatteryToday = 0
$totalConsumedFromGridToday = 0
$date = [DateTime]::Parse($usageDay.DATE)
# Load generation data for that day
$generationData = Invoke-WebRequest -Method POST -Uri https://pv-map.apvi.org.au/data -Body @{ day = $date.ToString("yyyy-MM-dd") } |
ConvertFrom-Json |
Select-Object -ExpandProperty postcode |
Select-Object -Property `
@{ label='LocalTime'; expression = { $_.ts.ToLocalTime() } },
@{ label='ProductionPercentage'; expression = { $_.$FirstTwoDigitsOfPostcode } }
# Simulate each 30 minute interval of the day
for ($hour = 0; $hour -lt 24 ; $hour++) {
for ($minute = 0; $minute -lt 60; $minute += 30) {
$startTime = [TimeSpan]::new($hour, $minute, 0)
$endTime = $startTime + [TimeSpan]::FromMinutes(30)
$usagePeriod = '{0:hh}:{0:mm} - {1:hh}:{1:mm}' -f $startTime, $endTime
# Combine the two 15-minute generation intervals that correlate to this 30-minute usage interval
$averageProductionPercentage = $generationData |
Where-Object { $_.LocalTime.TimeOfDay -ge $startTime -and $_.LocalTime.TimeOfDay -lt $endTime } |
Measure-Object -Property ProductionPercentage -Average |
Select-Object -ExpandProperty Average
if ($null -eq $averageProductionPercentage) {
$averageProductionPercentage = 0
}
$producedInPeriod = $PeakGeneration * $averageProductionPercentage / 100 * ($endTime - $startTime).TotalHours
$consumedInPeriod = [Double]::Parse($usageDay.$usagePeriod)
$netInPeriod = $producedInPeriod - $consumedInPeriod
Write-Debug ('{0} {1}: produced {2:n2} kWh from {3,6:p2}, consumed {4:n2} kWh, net {5:n2} kWh' -f `
$date,
$usagePeriod,
$producedInPeriod,
($averageProductionPercentage / 100),
$consumedInPeriod,
$netInPeriod
)
$totalConsumedToday += $consumedInPeriod
$totalProducedToday += $producedInPeriod
$spaceInBattery = $BatteryCapacity - $currentBatteryStorage
if ($producedInPeriod -gt $consumedInPeriod) {
# We made power
$totalConsumedFromSolarToday += $consumedInPeriod
$excessProductionInPeriod = $producedInPeriod - $consumedInPeriod
$amountThatCanGoInToBattery = [Math]::Min($spaceInBattery, $excessProductionInPeriod)
$amountThatDoesNotFitInBattery = $excessProductionInPeriod - $amountThatCanGoInToBattery
$currentBatteryStorage += $amountThatCanGoInToBattery
$gridExportToday += $amountThatDoesNotFitInBattery
}
elseif ($consumedInPeriod -gt $producedInPeriod) {
# We used power
$totalConsumedFromSolarToday += $producedInPeriod
$excessConsumptionInPeriod = $consumedInPeriod - $producedInPeriod
$amountThatCouldComeFromBattery = [Math]::Min($currentBatteryStorage, $excessConsumptionInPeriod)
$amountThatCouldNotComeFromBattery = $excessConsumptionInPeriod - $amountThatCouldComeFromBattery
$currentBatteryStorage -= $amountThatCouldComeFromBattery
$totalConsumedFromBatteryToday += $amountThatCouldComeFromBattery
$gridImportToday += $amountThatCouldNotComeFromBattery
$totalConsumedFromGridToday += $amountThatCouldNotComeFromBattery
}
else {
# Balanced
$totalConsumedFromSolarToday += $producedInPeriod
}
$consumptionTrackingError = $totalConsumedToday - $totalConsumedFromSolarToday - $totalConsumedFromBatteryToday - $totalConsumedFromGridToday
if ($consumptionTrackingError -gt 0.1) {
throw "Something didn't balance"
}
}
}
$batteryStorageAtEndOfDay = $currentBatteryStorage
[PSCustomObject]@{
Date = $date.ToString("yyyy-MM-dd");
BatteryStorageAtStartOfDay = $batteryStorageAtStartOfDay;
GridImportToday = $gridImportToday;
GridExportToday = $gridExportToday;
TotalProducedToday = $totalProducedToday;
TotalConsumedToday = $totalConsumedToday;
TotalConsumedFromSolarToday = $totalConsumedFromSolarToday;
TotalConsumedFromBatteryToday = $totalConsumedFromBatteryToday;
TotalConsumedFromGridToday = $totalConsumedFromGridToday;
BatteryStorageAtEndOfDay = $batteryStorageAtEndOfDay;
BatteryStorageAtEndOfDayPercentage = '{0:p}' -f ($batteryStorageAtEndOfDay / $BatteryCapacity);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment