499 lines
22 KiB
Plaintext
499 lines
22 KiB
Plaintext
@page "/"
|
|
@using Radzen
|
|
@using Radzen.Blazor
|
|
@using BattSim.Models
|
|
@using BattSim.Services
|
|
|
|
<PageTitle>Thuisbatterij Simulator</PageTitle>
|
|
|
|
<h1 style="color: white">Thuisbatterij Simulator</h1>
|
|
|
|
<div class="pipeline-container">
|
|
<!-- Pipeline with 3 connected boxes -->
|
|
<div class="pipeline-row">
|
|
<div class="pipeline-connector"></div>
|
|
|
|
<!-- Box 1: Data Upload -->
|
|
<div class="pipeline-box @GetPipelineStepClasses(0)">
|
|
<div class="box-header" @onclick="() => ToggleBox(0)">
|
|
<div class="step-number">1</div>
|
|
<div class="step-title">Fluvius Data</div>
|
|
<div class="status-indicator"></div>
|
|
</div>
|
|
<div class="box-content">
|
|
<div class="box-content-inner">
|
|
<p class="box-description">Upload je Fluvius kwartierdata om te beginnen</p>
|
|
|
|
<div class="form-section">
|
|
<div class="file-input-wrapper">
|
|
<InputFile type="file" id="fileUpload" accept=".csv" onchange="async (e) => await OnFileUploaded(e)" />
|
|
<label for="fileUpload" class="file-input-label">
|
|
@if (StepData[0].isProcessing)
|
|
{
|
|
<div class="loading-indicator">
|
|
<div class="spinner"></div>
|
|
<span style="margin-left: 10px;">Data aan het inladen...</span>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
@if (FluviusDataRaw.Length == 0)
|
|
{
|
|
<text>Kies CSV bestand</text>
|
|
}
|
|
else
|
|
{
|
|
<text>OK @FluviusDataRaw.Length kwartieren geladen</text>
|
|
}
|
|
}
|
|
</label>
|
|
@if (FluviusDataRaw.Length > 0 && !StepData[0].isProcessing)
|
|
{
|
|
<p class="file-name">@_uploadedFileName</p>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@if (StepData[0].Completed)
|
|
{
|
|
<div class="success-message">
|
|
Data succesvol ingeladen en verwerkt!
|
|
</div>
|
|
}
|
|
@if (_uploadError != null)
|
|
{
|
|
<div class="error-message">
|
|
@_uploadError
|
|
</div>
|
|
}
|
|
|
|
<div class="info-text">
|
|
<p><strong>Tip:</strong> Download een volledig jaar aan kwartierdata voor de meest accurate resultaten.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Box 2: Battery Simulation -->
|
|
<div class="pipeline-box @GetPipelineStepClasses(1)">
|
|
<div class="box-header" @onclick="() => ToggleBox(1)">
|
|
<div class="step-number">2</div>
|
|
<div class="step-title">Batterij Simulatie</div>
|
|
<div class="status-indicator"></div>
|
|
</div>
|
|
<div class="box-content">
|
|
<div class="box-content-inner">
|
|
<p class="box-description">Test verschillende batterijconfiguraties</p>
|
|
|
|
@if (FluviusDataRaw.Length == 0)
|
|
{
|
|
<div class="info-text">
|
|
<p>Upload eerst je Fluvius data (stap 1) om deze stap te kunnen uitvoeren.</p>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="form-section">
|
|
<div style="display: flex; gap: 1rem;">
|
|
<div class="input-group" style="flex: 1;">
|
|
<label>Batterijcapaciteit (kWh):</label>
|
|
<InputNumber @bind-value="BatteryCapacity" T="double" min="0.1" max="100" step="0.1"/>
|
|
</div>
|
|
<div class="input-group" style="flex: 1;">
|
|
<label>Round-trip Efficiëntie (%):</label>
|
|
<InputNumber @bind-value="Efficiency" T="double" min="0" max="100" step="1"/>
|
|
</div>
|
|
</div>
|
|
<p style="font-size: 0.85rem; color: var(--text-clr); margin-top: 1rem;">
|
|
Efficiëntie is het percentage van opgeslagen energie dat terug kan worden gebruikt.
|
|
</p>
|
|
</div>
|
|
|
|
<div style="display: flex; justify-content: center; margin-top: 1.5rem;">
|
|
<button @onclick="SimulateBattery" @onclick:stopPropagation disabled="@(StepData[1].isProcessing || FluviusDataRaw.Length == 0)">
|
|
@if (StepData[1].isProcessing)
|
|
{
|
|
<text>Simuleren...</text>
|
|
}
|
|
else
|
|
{
|
|
<text>Simuleer Batterij</text>
|
|
}
|
|
</button>
|
|
</div>
|
|
|
|
@if (StepData[1].Completed)
|
|
{
|
|
<div class="success-message">
|
|
Simulatie voltooid! @SimulationData.Length kwartieren gesimuleerd.
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Box 3: Cost Calculation -->
|
|
<div class="pipeline-box @GetPipelineStepClasses(2)">
|
|
<div class="box-header" @onclick="() => ToggleBox(2)">
|
|
<div class="step-number">3</div>
|
|
<div class="step-title">Kosten Berekenen</div>
|
|
<div class="status-indicator"></div>
|
|
</div>
|
|
<div class="box-content">
|
|
<div class="box-content-inner">
|
|
<p class="box-description">Vergelijk kosten met en zonder batterij</p>
|
|
|
|
<!-- Vaste Kosten -->
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<h4 style="color: var(--green-clr); margin-bottom: 0.75rem; border-bottom: 1px solid var(--border-clr); padding-bottom: 0.5rem;">
|
|
Vaste Kosten
|
|
</h4>
|
|
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
|
<div class="input-group" style="flex: 1;">
|
|
<label>Vaste vergoeding (€/jaar):</label>
|
|
<InputNumber @bind-value="calculator.BasePayment" T="double" min="0" step="1"/>
|
|
</div>
|
|
<div class="input-group" style=" flex: 1;">
|
|
<label>Databeheer tarief (€/jaar):</label>
|
|
<InputNumber @bind-value="calculator.DataManagementCost" T="double" min="0" step="0.01"/>
|
|
</div>
|
|
<div class="input-group" style=" flex: 1;">
|
|
<label>Vlaamse energieheffing (€/jaar):</label>
|
|
<InputNumber @bind-value="calculator.FlemishEnergyTariff" T="double" min="0" step="0.01"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Energiekosten -->
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<h4 style="color: var(--green-clr); margin-bottom: 0.75rem; border-bottom: 1px solid var(--border-clr); padding-bottom: 0.5rem;">
|
|
Energiekosten
|
|
</h4>
|
|
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
|
<div class="input-group" style=" flex: 1;">
|
|
<label>Energiekost (c€/kWh):</label>
|
|
<InputNumber @bind-value="calculator.EnergyCost" T="double" min="0" step="0.0001"/>
|
|
</div>
|
|
<div class="input-group" style=" flex: 1;">
|
|
<label>Terugleveringsvergoeding (c€/kWh):</label>
|
|
<InputNumber @bind-value="calculator.ReturnCost" T="double" min="0" step="0.0001"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Heffingen Groene Stroom -->
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<h4 style="color: var(--green-clr); margin-bottom: 0.75rem; border-bottom: 1px solid var(--border-clr); padding-bottom: 0.5rem;">
|
|
Heffingen Groene Stroom
|
|
</h4>
|
|
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
|
<div class="input-group" style=" flex: 1;">
|
|
<label>Groene stroomcertificaten (c€/kWh):</label>
|
|
<InputNumber @bind-value="calculator.GreenCertificateCost" T="double" min="0" step="0.0001"/>
|
|
</div>
|
|
<div class="input-group" style=" flex: 1;">
|
|
<label>Warmtekracht certificaten (c€/kWh):</label>
|
|
<InputNumber @bind-value="calculator.HeatingCertificateCost" T="double" min="0" step="0.0001"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Net- en distributiekosten -->
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<h4 style="color: var(--green-clr); margin-bottom: 0.75rem; border-bottom: 1px solid var(--border-clr); padding-bottom: 0.5rem;">
|
|
Net- en distributiekosten
|
|
</h4>
|
|
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
|
<div class="input-group" style=" flex: 1;">
|
|
<label>Capaciteitstarief (€/kWh/jaar):</label>
|
|
<InputNumber @bind-value="calculator.CapacityCost" T="double" min="0" step="0.01"/>
|
|
</div>
|
|
<div class="input-group" style=" flex: 1;">
|
|
<label>Afnametarief (c€/kWh):</label>
|
|
<InputNumber @bind-value="calculator.UsageTariff" T="double" min="0" step="0.0001"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Heffingen en Toeslagen -->
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<h4 style="color: var(--green-clr); margin-bottom: 0.75rem; border-bottom: 1px solid var(--border-clr); padding-bottom: 0.5rem;">
|
|
Heffingen en Toeslagen
|
|
</h4>
|
|
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
|
<div class="input-group" style=" flex: 1;">
|
|
<label>Energiebijdrage (c€/kWh):</label>
|
|
<InputNumber @bind-value="calculator.EnergyContribution" T="double" min="0" step="0.0001"/>
|
|
</div>
|
|
<div class="input-group" style=" flex: 1;">
|
|
<label>Bijzondere accijns (c€/kWh):</label>
|
|
<InputNumber @bind-value="calculator.SpecialTariffs" T="double" min="0" step="0.0001"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display: flex; justify-content: center; margin-top: 1.5rem;">
|
|
<button @onclick="Calculate" @onclick:stopPropagation disabled="@(StepData[2].isProcessing || SimulationData.Length == 0)">
|
|
@if (StepData[2].isProcessing)
|
|
{
|
|
<text>Berekenen...</text>
|
|
}
|
|
else
|
|
{
|
|
<text>Bereken Kosten</text>
|
|
}
|
|
</button>
|
|
</div>
|
|
|
|
@if (StepData[2].Completed)
|
|
{
|
|
<div class="results">
|
|
<p><strong>Originele kosten:</strong> <span class="result-value">€@normalCost.ToString("0.00")</span></p>
|
|
<p><strong>Gesimuleerde kosten:</strong> <span class="result-value">€@simulatedCost.ToString("0.00")</span></p>
|
|
<p><strong>Besparing:</strong> <span class="result-value">€@((normalCost - simulatedCost).ToString("0.00"))</span></p>
|
|
@if (normalCost > 0)
|
|
{
|
|
<p><strong>Besparingspercent:</strong> <span class="result-value">@(((normalCost - simulatedCost) / normalCost * 100).ToString("0.00"))%</span></p>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
<!-- Chart - Directly on page, full width -->
|
|
@if (FilteredFluviusData.Length > 0)
|
|
{
|
|
<div class="chart-container">
|
|
<h2>Overzicht: Energie Data & Simulatie</h2>
|
|
|
|
<!-- Time Period Toggle -->
|
|
<div class="time-toggle-container">
|
|
<button class="@GetTimeButtonClass("all")" @onclick="SetTimePeriodAll" @onclick:stopPropagation>
|
|
All periodes
|
|
</button>
|
|
<button class="@GetTimeButtonClass("year")" @onclick="SetTimePeriodYear" @onclick:stopPropagation>
|
|
Per jaar
|
|
</button>
|
|
<button class="@GetTimeButtonClass("month")" @onclick="SetTimePeriodMonth" @onclick:stopPropagation>
|
|
Per maand
|
|
</button>
|
|
<button class="@GetTimeButtonClass("week")" @onclick="SetTimePeriodWeek" @onclick:stopPropagation>
|
|
Per week
|
|
</button>
|
|
<button class="@GetTimeButtonClass("day")" @onclick="SetTimePeriodDay" @onclick:stopPropagation>
|
|
Per dag
|
|
</button>
|
|
</div>
|
|
|
|
<RadzenChart Class="unified-chart">
|
|
<!-- Original Data -->
|
|
<RadzenAreaSeries Smooth=true Data="@FilteredFluviusData" CategoryProperty="Time" Title="Consumption" ValueProperty="Consumption">
|
|
</RadzenAreaSeries>
|
|
<RadzenAreaSeries Smooth=true Data="@FilteredFluviusData" CategoryProperty="Time" Title="Production" ValueProperty="Production">
|
|
</RadzenAreaSeries>
|
|
|
|
<!-- Simulated Data -->
|
|
@if (FilteredSimulationData.Length > 0)
|
|
{
|
|
<RadzenAreaSeries Smooth=true Data="@FilteredSimulationData" CategoryProperty="Time" Title="Simulated Consumption" ValueProperty="Consumption">
|
|
</RadzenAreaSeries>
|
|
<RadzenAreaSeries Smooth=true Data="@FilteredSimulationData" CategoryProperty="Time" Title="Simulated Production" ValueProperty="Production">
|
|
</RadzenAreaSeries>
|
|
<RadzenAreaSeries Smooth=true Data="@FilteredSimulationData" CategoryProperty="Time" Title="Battery Charge" ValueProperty="BatteryCharge">
|
|
</RadzenAreaSeries>
|
|
}
|
|
|
|
<RadzenCategoryAxis Formatter="@FormatObject" Padding="20" LabelAutoRotation="-45">
|
|
<RadzenGridLines Visible="true"/>
|
|
<RadzenAxisTitle Text="Tijd"/>
|
|
</RadzenCategoryAxis>
|
|
<RadzenValueAxis Formatter="@FormatObject">
|
|
<RadzenGridLines Visible="true"/>
|
|
<RadzenAxisTitle Text="Energie (kWh)"/>
|
|
</RadzenValueAxis>
|
|
</RadzenChart>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
EnergyData[] FluviusDataRaw = [];
|
|
EnergyData[] SimulationData = [];
|
|
|
|
DataFilter.FilterOption SelectedFilterOption = DataFilter.FilterOption.ALL;
|
|
|
|
EnergyData[] FilteredFluviusData = [];
|
|
EnergyData[] FilteredSimulationData = [];
|
|
|
|
EnergyCostCalculator calculator = new();
|
|
double normalCost = 0.0;
|
|
double simulatedCost = 0.0;
|
|
|
|
struct PipelineStep{
|
|
public bool Expanded;
|
|
public bool Completed;
|
|
public bool isProcessing;
|
|
}
|
|
|
|
PipelineStep[] StepData = [
|
|
new PipelineStep(), // Step 1
|
|
new PipelineStep(), // Step 2
|
|
new PipelineStep(), // Step 3
|
|
];
|
|
|
|
string _uploadedFileName = string.Empty;
|
|
string? _uploadError = null;
|
|
|
|
string _timePeriod = "all";
|
|
|
|
double BatteryCapacity = 7.5;
|
|
double Efficiency = 90;
|
|
|
|
private string GetPipelineStepClasses(int step){
|
|
var classes = new List<string>();
|
|
if (StepData[step].Expanded) classes.Add("expanded");
|
|
if (StepData[step].Completed) classes.Add("completed");
|
|
if (StepData[step].isProcessing) classes.Add("running");
|
|
return string.Join(" ", classes);
|
|
}
|
|
|
|
private string GetTimeButtonClass(string period)
|
|
{
|
|
var baseClass = "time-toggle-button";
|
|
if (_timePeriod == period)
|
|
{
|
|
return $"{baseClass} active";
|
|
}
|
|
return baseClass;
|
|
}
|
|
|
|
private void SetTimePeriodAll() => SetTimePeriod("all");
|
|
private void SetTimePeriodYear() => SetTimePeriod("year");
|
|
private void SetTimePeriodMonth() => SetTimePeriod("month");
|
|
private void SetTimePeriodWeek() => SetTimePeriod("week");
|
|
private void SetTimePeriodDay() => SetTimePeriod("day");
|
|
|
|
private void ToggleBox(int step)
|
|
{
|
|
if(step == 1 && FluviusDataRaw.Length == 0) return;
|
|
if(step == 2 && SimulationData.Length == 0) return;
|
|
StepData[step].Expanded = !StepData[step].Expanded;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task OnFileUploaded(InputFileChangeEventArgs e)
|
|
{
|
|
_uploadError = null;
|
|
var file = e.File;
|
|
_uploadedFileName = file.Name;
|
|
|
|
if (file.ContentType != "text/csv")
|
|
{
|
|
_uploadError = "Alleen CSV bestanden zijn toegestaan!";
|
|
StateHasChanged();
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
StepData[0].isProcessing = true;
|
|
StateHasChanged();
|
|
|
|
FluviusDataRaw = await FluviusDataHandler.LoadAndProcessFile(file);
|
|
SetTimePeriod("all");
|
|
FilteredSimulationData = [];
|
|
|
|
StepData[0].isProcessing = false;
|
|
StepData[0].Completed = true;
|
|
StepData[1].Completed = false;
|
|
StepData[2].Completed = false;
|
|
SimulationData = [];
|
|
|
|
StateHasChanged();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StepData[0].isProcessing = false;
|
|
_uploadError = ex.Message;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private void SimulateBattery()
|
|
{
|
|
if (FluviusDataRaw.Length == 0) return;
|
|
|
|
StepData[1].isProcessing = true;
|
|
StateHasChanged();
|
|
|
|
try
|
|
{
|
|
SimulationData = BatterySimulator.SimulateBattery(FluviusDataRaw, BatteryCapacity, Efficiency/100);
|
|
SetTimePeriod("all");
|
|
|
|
StepData[1].isProcessing = false;
|
|
StepData[1].Completed = true;
|
|
StepData[2].Completed = false;
|
|
normalCost = 0;
|
|
simulatedCost = 0;
|
|
|
|
StateHasChanged();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StepData[1].isProcessing = false;
|
|
Console.WriteLine($"Error: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void Calculate()
|
|
{
|
|
if (SimulationData.Length == 0) return;
|
|
|
|
StepData[2].isProcessing = true;
|
|
StateHasChanged();
|
|
|
|
try
|
|
{
|
|
normalCost = calculator.CalculateCostOfEnergyUsage(FluviusDataRaw);
|
|
StepData[2].isProcessing = false;
|
|
StepData[2].Completed = true;
|
|
StateHasChanged();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StepData[2].isProcessing = false;
|
|
Console.WriteLine($"Error: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void SetTimePeriod(string period)
|
|
{
|
|
_timePeriod = period;
|
|
var filterOption = period switch {
|
|
"all" => DataFilter.FilterOption.ALL,
|
|
"year" => DataFilter.FilterOption.YEAR,
|
|
"month" => DataFilter.FilterOption.MONTH,
|
|
"week" => DataFilter.FilterOption.WEEK,
|
|
"day" => DataFilter.FilterOption.DAY,
|
|
};
|
|
// Filter data based on selected period
|
|
if(FluviusDataRaw.Length > 0) FilteredFluviusData = DataFilter.FilterData(FluviusDataRaw, filterOption);
|
|
if(FilteredSimulationData.Length > 0) FilteredSimulationData = DataFilter.FilterData(SimulationData, filterOption);
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
private string FormatObject(object value) {
|
|
if(value is double d) return $"{d:0.##} kWh";
|
|
if(value is DateTime time) return time.ToString("dd/MM/yyyy");
|
|
if(value is DateOnly date) return date.ToString("dd/MM/yyyy");
|
|
return string.Empty;
|
|
}
|
|
}
|