Compare commits

..

15 Commits

28 changed files with 166475 additions and 33937 deletions

BIN
BattSim Logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

34
Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS launch
WORKDIR /src
# Install Python (required for AOT compilation)
RUN apt-get update && \
apt-get install -y python3 && \
ln -s /usr/bin/python3 /usr/bin/python
# Update workloads and SDK
RUN dotnet --list-sdks
RUN dotnet --list-runtimes
RUN dotnet workload update
# Copy project and solution files
COPY *.csproj *.sln ./
# Install required workloads
RUN dotnet workload install wasm-tools
# Copy the rest of the application
COPY . .
# Restore dependencies
RUN dotnet restore
# Publish the app (optional, if you want to test publish output)
# RUN dotnet publish -c Release -o /app/publish
# Runtime stage: Use the SDK image to run the app
ENTRYPOINT ["dotnet", "run", "--no-launch-profile", "--urls", "http://0.0.0.0:5000"]
EXPOSE 5000

View File

@@ -1,3 +1,5 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@Body <main>
@Body
</main>

View File

@@ -1,14 +0,0 @@
using System;
namespace BattSim.Models
{
public class BatteryDayResult
{
public DateOnly Date { get; set; }
public double ChargedEnergy { get; set; }
public double UsedEnergy { get; set; }
public double RemainingEnergy { get; set; }
public double ReducedConsumption { get; set; }
public double ReducedProduction { get; set; }
}
}

View File

@@ -5,8 +5,19 @@ namespace BattSim.Models
public class EnergyData public class EnergyData
{ {
public DateTime Time { get; set; } public DateTime Time { get; set; }
public bool DayTarif { get; set; } public bool DayTariff { get; set; }
public double Consumption { get; set; } public double Consumption { get; set; }
public double Production { get; set; } public double Production { get; set; }
public double BatteryCharge { get; set; }
public EnergyData(){}
public EnergyData(EnergyData other)
{
Time = other.Time;
DayTariff = other.DayTariff;
Consumption = other.Consumption;
Production = other.Production;
BatteryCharge = other.BatteryCharge;
}
} }
} }

8
Models/PipelineStep.cs Normal file
View File

@@ -0,0 +1,8 @@
namespace BattSim.Models;
public struct PipelineStep
{
public bool Expanded;
public bool Completed;
public bool isProcessing;
}

View File

@@ -1,91 +1,265 @@
@page "/" @page "/"
@using Radzen @using Radzen
@using Radzen.Blazor @using Radzen.Blazor
@using BattSim.Models @using BattSim.Models
@using BattSim.Services @using BattSim.Services
@using BattSim.Pages.Nodes
<PageTitle>BattSim</PageTitle> <PageTitle>Wattif</PageTitle>
<h1>BattSim</h1> <h1 style="color: white">Wattif - Simuleer verbeteringen aan je energiesysteem</h1>
<h2>Input Data</h2> <div class="pipeline-container">
<p>Upload your fluvius daily csv file here.</p> <!-- Pipeline with 3 connected boxes -->
<InputFile OnChange="LoadCsvFile" accept=".csv"/> <div class="pipeline-row">
@if (_isLoadingFile){ <p>Loading...</p> } <div class="pipeline-connector"></div>
@if (EnergyData.Length != 0){
<RadzenChart> <!-- Node Components -->
<RadzenAreaSeries Smooth=true Data="@EnergyData" CategoryProperty="Time" Title="Consumption" ValueProperty="Consumption"> <FluviusUploadNode
<RadzenChartTooltipOptions Visible="true" /> FluviusDataRaw="FluviusDataRaw"
</RadzenAreaSeries> StepData="StepData[0]"
<RadzenAreaSeries Smooth=true Data="@EnergyData" CategoryProperty="Time" Title="Production" ValueProperty="Production"> UploadedFileName="_uploadedFileName"
<RadzenChartTooltipOptions Visible="true" /> UploadError="_uploadError"
</RadzenAreaSeries> OnFileUploadedCallback="OnFileUploaded"
<RadzenCategoryAxis Formatter="@FormatObject" Padding="20" LabelAutoRotation="-45"> OnToggleBoxCallback="() => ToggleBox(0)" />
<RadzenAxisTitle Text="Time" />
</RadzenCategoryAxis> <BatterySimulationNode
<RadzenValueAxis Formatter="@FormatObject"> FluviusDataRaw="FluviusDataRaw"
<RadzenGridLines Visible="true" /> SimulationData="SimulationData"
<RadzenAxisTitle Text="Energy" /> StepData="StepData[1]"
</RadzenValueAxis> BatteryCapacity="BatteryCapacity"
</RadzenChart> Efficiency="Efficiency"
OnSimulateBatteryCallback="SimulateBattery"
OnToggleBoxCallback="() => ToggleBox(1)" />
<CostCalculationNode
SimulationData="SimulationData"
FluviusDataRaw="FluviusDataRaw"
StepData="StepData[2]"
Calculator="calculator"
NormalCost="normalCost"
SimulatedCost="simulatedCost"
OnCalculateCallback="Calculate"
OnToggleBoxCallback="() => ToggleBox(2)" />
</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>
Volledige periode
</button>
<button class="@GetTimeButtonClass("year")" @onclick="SetTimePeriodYear" @onclick:stopPropagation>
Laatste jaar
</button>
<button class="@GetTimeButtonClass("month")" @onclick="SetTimePeriodMonth" @onclick:stopPropagation>
Laatste maand
</button>
<button class="@GetTimeButtonClass("week")" @onclick="SetTimePeriodWeek" @onclick:stopPropagation>
Laatste week
</button>
<button class="@GetTimeButtonClass("day")" @onclick="SetTimePeriodDay" @onclick:stopPropagation>
Laatste 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{ @code {
EnergyData[] EnergyData = []; EnergyData[] FluviusDataRaw = [];
bool _isLoadingFile = false; EnergyData[] SimulationData = [];
private async Task LoadCsvFile(InputFileChangeEventArgs e) DataFilter.FilterOption SelectedFilterOption = DataFilter.FilterOption.ALL;
EnergyData[] FilteredFluviusData = [];
EnergyData[] FilteredSimulationData = [];
EnergyCostCalculator calculator = new();
double normalCost = 0.0;
double simulatedCost = 0.0;
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 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; var file = e.File;
_uploadedFileName = file.Name;
if (file.ContentType != "text/csv") if (file.ContentType != "text/csv")
{ {
Console.WriteLine("Only CSV files are allowed!"); _uploadError = "Alleen CSV bestanden zijn toegestaan!";
StateHasChanged();
return; return;
} }
try try
{ {
Console.WriteLine("Reading csv file..."); StepData[0].isProcessing = true;
_isLoadingFile = true;
StateHasChanged(); StateHasChanged();
var loadingTask = DataLoader.LoadAndProcessData(file); FluviusDataRaw = await FluviusDataHandler.LoadAndProcessFile(file);
var energyData = await loadingTask; SetTimePeriod("all");
EnergyData = energyData.ToArray(); FilteredSimulationData = [];
StepData[0].isProcessing = false;
StepData[0].Completed = true;
StepData[1].Completed = false;
StepData[2].Completed = false;
SimulationData = [];
_isLoadingFile = false;
StateHasChanged(); StateHasChanged();
Console.WriteLine("Done reading csv file!");
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"Error loading file: {ex.Message}"); StepData[0].isProcessing = false;
_uploadError = ex.Message;
StateHasChanged();
} }
} }
private void OnSeriesClick(){} 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);
simulatedCost = calculator.CalculateCostOfEnergyUsage(SimulationData);
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(SimulationData.Length > 0) FilteredSimulationData = DataFilter.FilterData(SimulationData, filterOption);
StateHasChanged();
}
private string FormatObject(object value) { private string FormatObject(object value) {
if(value is double d) return $"{value:0.##} kWh"; if(value is double d) return $"{d:0.##} kWh";
if(value is DateOnly date) return (date.Day == 1) ? date.ToString("MM/yyyy") : string.Empty; if(value is DateTime time) return time.ToString("dd/MM/yyyy");
else return string.Empty; if(value is DateOnly date) return date.ToString("dd/MM/yyyy");
} return string.Empty;
}
<h2>Simulate Battery</h2>
<p>Set the battery capacity</p>
<InputNumber @bind-value="BatteryCapacity"/>
<Button @onclick="SimulateBattery">Simulate</Button>
<h2>Calculate Cost</h2>
@code {
double BatteryCapacity = 0.0;
BatteryDayResult[] SimulationData = [];
private async Task SimulateBattery(){
Console.WriteLine("Simulating...");
SimulationData = BatterySimulator.SimulateBattery(EnergyData, BatteryCapacity).ToArray();
Console.WriteLine("Done simulating!");
} }
} }

View File

@@ -0,0 +1,104 @@
@using Radzen
@using Radzen.Blazor
@using BattSim.Models
@using BattSim.Services
<div class="pipeline-box @GetPipelineStepClasses(StepData)">
<div class="box-header" @onclick="ToggleBox">
<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.isProcessing || FluviusDataRaw.Length == 0)">
@if (StepData.isProcessing)
{
<text>Simuleren...</text>
}
else
{
<text>Simuleer Batterij</text>
}
</button>
</div>
@if (StepData.Completed)
{
<div class="success-message">
Simulatie voltooid! @SimulationData.Length kwartieren gesimuleerd.
</div>
}
}
</div>
</div>
</div>
@code {
[Parameter]
public EnergyData[] FluviusDataRaw { get; set; } = [];
[Parameter]
public EnergyData[] SimulationData { get; set; } = [];
[Parameter]
public PipelineStep StepData { get; set; }
[Parameter]
public double BatteryCapacity { get; set; } = 7.5;
[Parameter]
public double Efficiency { get; set; } = 90;
[Parameter]
public EventCallback OnSimulateBatteryCallback { get; set; }
[Parameter]
public EventCallback OnToggleBoxCallback { get; set; }
private string GetPipelineStepClasses(PipelineStep step)
{
var classes = new List<string>();
if (step.Expanded) classes.Add("expanded");
if (step.Completed) classes.Add("completed");
if (step.isProcessing) classes.Add("running");
return string.Join(" ", classes);
}
private void SimulateBattery()
{
OnSimulateBatteryCallback.InvokeAsync();
}
private void ToggleBox()
{
OnToggleBoxCallback.InvokeAsync();
}
}

View File

@@ -0,0 +1,180 @@
@using Radzen
@using Radzen.Blazor
@using BattSim.Models
@using BattSim.Services
<div class="pipeline-box @GetPipelineStepClasses(StepData)">
<div class="box-header" @onclick="ToggleBox">
<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>
<!-- Contractuele 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;">
Contractuele 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>
<!-- Vaste Kosten Subbox (Collapsed by Default) -->
<details class="collapsible-section" style="margin-top: 1rem;">
<summary class="collapsible-header">Vaste Kosten</summary>
<div class="collapsible-content">
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<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>
<!-- Heffingen Groene Stroom -->
<div style="margin-top: 1rem;">
<h5 style="color: var(--green-clr); margin-bottom: 0.5rem;">Heffingen Groene Stroom</h5>
<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-top: 1rem;">
<h5 style="color: var(--green-clr); margin-bottom: 0.5rem;">Net- en distributiekosten</h5>
<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-top: 1rem;">
<h5 style="color: var(--green-clr); margin-bottom: 0.5rem;">Heffingen en Toeslagen</h5>
<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>
</details>
</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>
<div style="display: flex; justify-content: center; margin-top: 1.5rem;">
<button @onclick="Calculate" @onclick:stopPropagation disabled="@(StepData.isProcessing || SimulationData.Length == 0)">
@if (StepData.isProcessing)
{
<text>Berekenen...</text>
}
else
{
<text>Bereken Kosten</text>
}
</button>
</div>
@if (StepData.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>
@code {
[Parameter]
public EnergyData[] SimulationData { get; set; } = [];
[Parameter]
public EnergyData[] FluviusDataRaw { get; set; } = [];
[Parameter]
public PipelineStep StepData { get; set; }
[Parameter]
public EnergyCostCalculator Calculator { get; set; } = new();
[Parameter]
public double NormalCost { get; set; } = 0.0;
[Parameter]
public double SimulatedCost { get; set; } = 0.0;
[Parameter]
public EventCallback OnCalculateCallback { get; set; }
[Parameter]
public EventCallback OnToggleBoxCallback { get; set; }
private string GetPipelineStepClasses(PipelineStep step)
{
var classes = new List<string>();
if (step.Expanded) classes.Add("expanded");
if (step.Completed) classes.Add("completed");
if (step.isProcessing) classes.Add("running");
return string.Join(" ", classes);
}
private void Calculate()
{
OnCalculateCallback.InvokeAsync();
}
private void ToggleBox()
{
OnToggleBoxCallback.InvokeAsync();
}
}

View File

@@ -0,0 +1,103 @@
@using Radzen
@using Radzen.Blazor
@using BattSim.Models
@using BattSim.Services
<div class="pipeline-box @GetPipelineStepClasses(StepData)">
<div class="box-header" @onclick="ToggleBox">
<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.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.isProcessing)
{
<p class="file-name">@UploadedFileName</p>
}
</div>
</div>
@if (StepData.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>
@code {
[Parameter]
public EnergyData[] FluviusDataRaw { get; set; } = [];
[Parameter]
public PipelineStep StepData { get; set; }
[Parameter]
public string UploadedFileName { get; set; } = string.Empty;
[Parameter]
public string? UploadError { get; set; }
[Parameter]
public EventCallback<InputFileChangeEventArgs> OnFileUploadedCallback { get; set; }
[Parameter]
public EventCallback OnToggleBoxCallback { get; set; }
private string GetPipelineStepClasses(PipelineStep step)
{
var classes = new List<string>();
if (step.Expanded) classes.Add("expanded");
if (step.Completed) classes.Add("completed");
if (step.isProcessing) classes.Add("running");
return string.Join(" ", classes);
}
private async Task OnFileUploaded(InputFileChangeEventArgs e)
{
await OnFileUploadedCallback.InvokeAsync(e);
}
private void ToggleBox()
{
OnToggleBoxCallback.InvokeAsync();
}
}

View File

@@ -4,7 +4,6 @@ using Radzen;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
// Setup Frontend
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app"); builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after"); builder.RootComponents.Add<HeadOutlet>("head::after");
@@ -12,5 +11,4 @@ builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddRadzenComponents(); builder.Services.AddRadzenComponents();
await builder.Build().RunAsync(); await builder.Build().RunAsync();

View File

@@ -1,8 +1,38 @@
# BatSim # Wattif: Home Energy Improvement Simulator
A tool that uses your current home energy data an simulates the impact of installing a home battery. It also does a cost analysis. **Watt if?** is a simulation tool designed to help Flemish homeowners evaluate the impact of energy improvements such as batteries, heat pumps, solar panels, and air conditioning on their **energy bills** and **carbon emissions**. Currently, Wattif works exclusively with data from **Fluvius**, the Flemish energy distribution company.
The tool works specifically for the flemish system with input data from fluvius and cost analysis based on the flemish energy pricing. ---
## Used Tools ## How It Works
This project makes use of the Radzen.blazer package for UI components.
1. **Upload Your Data**
Download your electricity usage and gas usage from the mijn fluvius website and upload it to the tool.
2. **Add Home Improvements**
Add the improvements you want to make and configure them.
3. **Simulate & Analyze**
Press the simulate button and look at the new energy usage and carbon intensity of your house.
4. **Enter Your Energy Contract**
Fill out your current energy contract pricing details and the tool cacluates savings/losses.
---
## Roadmap
### ✅ Completed
- Read and visualize Fluvius electricity data
- Simulate battery impact
- Calculate original and simulated energy costs
### 🚀 Planned
- Calculate carbon intensity of energy usage
- Simulate solar panel performance
- Simulate air conditioning impact
- Calculate ROI for improvements
- Read and visualize Fluvius gas data
- Simulate heat pump efficiency

View File

@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using BattSim.Models; using BattSim.Models;
@@ -5,39 +6,32 @@ namespace BattSim.Services
{ {
public static class BatterySimulator public static class BatterySimulator
{ {
public static List<BatteryDayResult> SimulateBattery(EnergyData[] data, double batteryCapacity) public static EnergyData[] SimulateBattery(EnergyData[] energyData, double batteryCapacity, double efficiency)
{ {
var results = new List<BatteryDayResult>(); var results = new List<EnergyData>();
double remainingEnergy = 0; double batteryCharge = 0;
foreach (var day in data) foreach (var e in energyData)
{ {
// // Charge battery from production var simulatedBatteryEnergyData = new EnergyData(e);
// var chargedEnergy = System.Math.Min(day.TotalProduction, batteryCapacity); // Simulate charging the battery based on production
// var excessProduction = day.TotalProduction - chargedEnergy; double excessProduction = batteryCharge + e.Production - batteryCapacity;
batteryCharge = double.Min(batteryCharge + e.Production, batteryCapacity);
simulatedBatteryEnergyData.Production = double.Max(excessProduction, 0);
// // Use battery for consumption // Simulate discharging the battery based on consumption
// var usedEnergy = System.Math.Min(chargedEnergy + remainingEnergy, day.TotalConsumption); double availableEnergy = batteryCharge * efficiency;
// var remainingAfterUse = chargedEnergy + remainingEnergy - usedEnergy; double deficit = e.Consumption - availableEnergy;
double energyDrawn = Math.Min(e.Consumption, availableEnergy);
batteryCharge = batteryCharge - (energyDrawn / efficiency); // Adjust for loss
simulatedBatteryEnergyData.Consumption = Math.Max(deficit, 0);
// // Calculate reduced values // Register the current charge
// var reducedConsumption = System.Math.Min(usedEnergy, day.TotalConsumption); simulatedBatteryEnergyData.BatteryCharge = batteryCharge;
// var reducedProduction = day.TotalProduction - chargedEnergy; results.Add(simulatedBatteryEnergyData);
// results.Add(new BatteryDayResult
// {
// Date = day.Date,
// ChargedEnergy = chargedEnergy,
// UsedEnergy = usedEnergy,
// RemainingEnergy = remainingAfterUse,
// ReducedConsumption = reducedConsumption,
// ReducedProduction = reducedProduction
// });
// remainingEnergy = remainingAfterUse;
} }
return results; return results.ToArray();
} }
} }
} }

30
Services/DataFilter.cs Normal file
View File

@@ -0,0 +1,30 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using BattSim.Models;
namespace BattSim.Services
{
public static class DataFilter
{
public enum FilterOption { ALL, YEAR, MONTH, WEEK, DAY }
public static EnergyData[] FilterData(EnergyData[] data, FilterOption filterOption)
{
var dailyData = data
.GroupBy(d=>d.Time.Date)
.Select(group=> new EnergyData{
Consumption = group.Sum(d => d.Consumption),
Production = group.Sum(d => d.Production),
BatteryCharge = group.Max(d => d.BatteryCharge),
Time = group.First().Time
}).ToArray();
return filterOption switch {
FilterOption.DAY => data[(data.Length > 96?data.Length - 96:0)..], // 24 hours * 4 quarters per hour
FilterOption.WEEK => data[(data.Length > 672?data.Length - 672:0)..], // 7 days * 24 hours * 4 quarters per hour
FilterOption.MONTH => dailyData[(dailyData.Length > 30?dailyData.Length - 30:0)..],
FilterOption.YEAR => dailyData[(dailyData.Length > 365?dailyData.Length - 365:0)..],
_ => dailyData
};
}
}
}

View File

@@ -0,0 +1,92 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Threading.Tasks;
using BattSim.Models;
using Microsoft.AspNetCore.Components.Forms;
namespace BattSim.Services
{
public class EnergyCostCalculator
{
// Base cost
public double BasePayment { get; set; } = 35;
public double DataManagementCost { get; set; } = 18.77;
public double FlemishEnergyTariff { get; set; } = 118.56;
// Energy cost
public double EnergyCost { get; set; } = 0.1364;
public double ReturnCost { get; set; } = 0.0266;
// Green energy cost
public double GreenCertificateCost { get; set; } = 0.01172;
public double HeatingCertificateCost { get; set; } = 0.00415;
// Net and distributino ocsts
public double CapacityCost { get; set; } = 58.20;
public double UsageTariff { get; set; } = 0.0637;
// Tax
public double EnergyContribution { get; set; } = 0.00205;
public double SpecialTariffs { get; set; } = 0.0502;
public double CalculateCostOfEnergyUsage(EnergyData[] energyData, bool print = false) {
int amountOfDays = energyData.Last().Time.Subtract(energyData.First().Time).Days;
double yearfactor = amountOfDays / 365.0;
double energyConsumption = energyData.Sum(e => e.Consumption);
double energyProduction = energyData.Sum(e => e.Production);
double baseCost = yearfactor * BasePayment;
if(print) Console.WriteLine($"Base Cost: €{baseCost}");
double energyCost = energyConsumption * EnergyCost;
if (print) Console.WriteLine($"Energy Cost: €{energyCost}");
double returnCost = energyProduction * ReturnCost;
if (print) Console.WriteLine($"Return Cost: € -{returnCost}");
double greenCertificateCost = energyConsumption * GreenCertificateCost;
if (print) Console.WriteLine($"Green Certificate Cost: €{greenCertificateCost}");
double heatingCertificateCost = energyConsumption * HeatingCertificateCost;
if (print) Console.WriteLine($"Heating Certificate Cost: €{heatingCertificateCost}");
double dataManagmentCost = yearfactor * DataManagementCost;
if (print) Console.WriteLine($"Data Management Cost: €{dataManagmentCost}");
double capacityCost = CalculateYearCapacity(energyData) * yearfactor;
if (print) Console.WriteLine($"Capacity Cost: €{capacityCost}");
double usageTariffCost = energyConsumption * UsageTariff;
if (print) Console.WriteLine($"Usage Tariff Cost: €{usageTariffCost}");
double energyContributionCost = energyConsumption * EnergyContribution;
if (print) Console.WriteLine($"Energy Contribution Cost: €{energyContributionCost}");
double specialTariffsCost = energyConsumption * SpecialTariffs;
if (print) Console.WriteLine($"Special Tariffs Cost: €{specialTariffsCost}");
double flemishEnergyTarif = yearfactor * FlemishEnergyTariff;
if (print) Console.WriteLine($"Flemish Tariff: €{specialTariffsCost}");
return baseCost + energyCost - returnCost + greenCertificateCost + heatingCertificateCost + dataManagmentCost +
capacityCost + usageTariffCost + energyContributionCost + specialTariffsCost + flemishEnergyTarif;
}
private double CalculateYearCapacity(EnergyData[] energyData)
{
double peakSum = 0;
int months = 0;
for(int i = 0; i<12; i++)
{
var monthData = energyData.Where(e => e.Time.Month == i);
if (monthData.Count() == 0) continue;
peakSum += monthData.Max(e => e.Consumption) * 4;
months++;
}
return (peakSum / months) * CapacityCost;
}
}
}

View File

@@ -4,44 +4,31 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection.PortableExecutable;
using System.Threading.Tasks; using System.Threading.Tasks;
using BattSim.Models; using BattSim.Models;
using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Forms;
namespace BattSim.Services namespace BattSim.Services
{ {
public static class DataLoader public static class FluviusDataHandler
{ {
public static async Task<List<EnergyData>> LoadAndProcessData(IBrowserFile file) public static async Task<EnergyData[]> LoadAndProcessFile(IBrowserFile file)
{ {
var energyData = new ConcurrentBag<EnergyData>(); // Thread-safe collection var lines = await ReadCsvFile(file);
await using var stream = file.OpenReadStream(maxAllowedSize: (int)1.0e9); var energyData = new ConcurrentDictionary<DateTime,EnergyData>(); // Thread-safe collection
using var reader = new StreamReader(stream, bufferSize: (int)1.0e6);
// Skip header
await reader.ReadLineAsync();
// Read all lines into memory
var lines = new List<string>();
string line;
while ((line = await reader.ReadLineAsync()) is not null)
{
lines.Add(line);
}
// Process lines in parallel // Process lines in parallel
Parallel.ForEach(lines, line => Parallel.ForEach(lines, line =>
{ {
try try
{ {
var parts = SplitLine(line); var parts = SplitLine(line);
DateTime time = ParseDateTime(parts[0..2]); DateTime time = ParseDateTime(parts[..2]);
double volume = ParseVolume(parts[8]); double volume = ParseVolume(parts[8]);
string register = parts[7].Trim(); string register = parts[7].Trim();
bool dayTarif = register.Contains("Dag"); bool dayTarif = register.Contains("Dag");
// Use ConcurrentBag for thread-safe additions var entry = new EnergyData { Time = time, DayTariff = dayTarif };
var entry = new EnergyData { Time = time, DayTarif = dayTarif };
if (register.Contains("Afname")) if (register.Contains("Afname"))
entry.Consumption = volume; entry.Consumption = volume;
else if (register.Contains("Injectie")) else if (register.Contains("Injectie"))
@@ -49,28 +36,61 @@ namespace BattSim.Services
else else
throw new Exception("Unknown volume register"); throw new Exception("Unknown volume register");
energyData.Add(entry); energyData.AddOrUpdate(entry.Time, entry, (_, existing) =>
{
existing.Consumption += entry.Consumption;
existing.Production += entry.Production;
return existing;
});
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine($"Error parsing line: {line}. Skipping to next line. Exception: {e}"); Console.WriteLine($"Error parsing line: {line}. Skipping to next line. Exception: {e}");
} }
}); });
var results = energyData.Values.ToArray();
results.Sort((a,b)=>a.Time.CompareTo(b.Time));
return results;
}
// Group by Time and merge entries public static EnergyData[] GenerateDailyData(EnergyData[] energyData)
var groupedData = energyData {
.GroupBy(e => e.Time) var dailyEnergyData = new ConcurrentDictionary<DateOnly, EnergyData>();
.Select(g => Parallel.ForEach(energyData, energy =>
{ {
var first = g.First(); var date = DateOnly.FromDateTime(energy.Time);
first.Consumption = g.Sum(e => e.Consumption);
first.Production = g.Sum(e => e.Production);
return first;
})
.OrderBy(e => e.Time)
.ToList();
return groupedData; // Use AddOrUpdate to avoid double lookup
dailyEnergyData.AddOrUpdate(
date,
new EnergyData(energy), // If key doesn't exist, add this value
(_, existing) =>
{
// If key exists, aggregate the values
existing.Consumption += energy.Consumption;
existing.Production += energy.Production;
return existing;
}
);
});
return dailyEnergyData.Values.ToArray();
}
private static async Task<List<string>> ReadCsvFile(IBrowserFile file)
{
await using var stream = file.OpenReadStream(maxAllowedSize: (int)1.0e9);
using var reader = new StreamReader(stream, bufferSize: (int)1.0e6);
// Skip header
await reader.ReadLineAsync();
// Read all lines into memory
var lines = new List<string>();
string? line;
while ((line = await reader.ReadLineAsync()) is not null)
{
lines.Add(line);
}
return lines;
} }
private static string[] SplitLine(string line) private static string[] SplitLine(string line)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

10
docker-compose.yml Normal file
View File

@@ -0,0 +1,10 @@
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "5000:5000"
restart: unless-stopped

735
wwwroot/css/site.css Normal file
View File

@@ -0,0 +1,735 @@
/* ===== Douwco.be Theme ===== */
@font-face {
font-family: 'Righteous';
src: url('/fonts/Righteous.ttf') format('truetype');
}
@font-face {
font-family: 'Montserrat';
src: url('/fonts/Montserrat.ttf') format('truetype');
font-weight: normal;
}
@font-face {
font-family: 'Montserrat-Bold';
src: url('/fonts/Montserrat.ttf') format('truetype');
font-weight: bold;
}
:root {
--background-clr: #283e3e;
--background-accent-clr: #324f4f;
--blue-clr: #47bcdf;
--green-clr: #6ede9a;
--purple-clr: #a48da;
--orange-clr: #e2a661;
--white-clr: #ffffff;
--text-clr: #e0e0e0;
--border-clr: #47bcdf;
--success-clr: #2ecc71;
--warning-clr: #f39c12;
--error-clr: #e74c3c;
}
/* ===== Base Styles ===== */
html, body {
height: 100%;
margin: 0;
padding: 0;
font-family: 'Montserrat', sans-serif;
}
body {
background-color: var(--background-clr);
color: var(--text-clr);
display: flex;
flex-direction: column;
min-height: 100vh;
}
main {
flex: 1;
margin: 20px;
overflow-y: auto;
overflow-x: hidden;
scroll-behavior: smooth;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Righteous', sans-serif;
color: var(--white-clr);
}
h1 {
font-size: 2.5rem;
margin-bottom: 2rem;
text-align: center;
}
h2 {
font-size: 1.8rem;
margin: 1.5rem 0 1rem 0;
color: var(--blue-clr);
}
h3 {
font-size: 1.3rem;
margin: 1rem 0 0.5rem 0;
color: var(--green-clr);
}
p {
line-height: 1.6;
margin: 0.5rem 0;
}
a {
color: var(--blue-clr);
text-decoration: none;
}
a:hover {
color: var(--green-clr);
}
/* ===== Pipeline Layout ===== */
.pipeline-container {
display: flex;
flex-direction: column;
gap: 2rem;
margin: 2rem 0;
}
.pipeline-row {
display: flex;
justify-content: center;
align-items: stretch;
gap: 1rem;
position: relative;
margin-bottom: 2rem;
}
.pipeline-connector {
position: absolute;
top: 25px;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(to right, var(--blue-clr), var(--green-clr));
z-index: 0;
}
.pipeline-connector::before,
.pipeline-connector::after {
content: '';
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--blue-clr);
top: -4px;
}
.pipeline-connector::before {
left: -6px;
}
.pipeline-connector::after {
right: -6px;
background: var(--green-clr);
}
/* ===== Pipeline Boxes ===== */
.pipeline-box {
background-color: var(--background-accent-clr);
border-radius: 15px;
padding: 1.5rem;
width: 300px;
position: relative;
z-index: 1;
transition: border-color 0.3s, box-shadow 0.3s;
border: 2px solid var(--background-accent-clr);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
height: 100px;
overflow: hidden;
}
.pipeline-box:hover {
border-color: var(--blue-clr);
box-shadow: 0 6px 20px rgba(71, 188, 223, 0.3);
}
.pipeline-box.expanded {
width: 360px;
border-color: var(--green-clr);
box-shadow: 0 8px 25px rgba(110, 222, 154, 0.4);
height: auto;
overflow: visible;
}
.pipeline-box:not(.expanded) .box-content {
display: none;
}
.pipeline-box.completed {
border-color: var(--success-clr);
}
.pipeline-box.completed .box-header {
border-bottom-color: var(--success-clr);
}
.pipeline-box.completed .status-indicator {
background-color: var(--success-clr);
box-shadow: 0 0 15px var(--success-clr);
}
.pipeline-box.completed .step-number {
background-color: var(--success-clr);
}
/* Box Header */
.pipeline-box .box-header {
display: flex;
cursor: pointer;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--border-clr);
transition: all 0.3s ease;
}
.pipeline-box.expanded .box-header {
border-bottom-color: var(--green-clr);
}
.pipeline-box .step-number {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, var(--blue-clr), var(--green-clr));
display: flex;
align-items: center;
justify-content: center;
font-family: 'Montserrat-Bold', sans-serif;
font-size: 1.2rem;
color: white;
font-weight: bold;
}
.pipeline-box .step-title {
font-size: 1.3rem;
font-family: 'Montserrat-Bold', sans-serif;
color: var(--white-clr);
flex: 1;
text-align: center;
}
.pipeline-box .status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: var(--background-clr);
border: 2px solid var(--blue-clr);
transition: all 0.3s ease;
}
.pipeline-box.running .status-indicator {
background-color: var(--orange-clr);
animation: pulse 1.5s infinite;
}
/* Box Content */
.pipeline-box .box-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.pipeline-box.expanded .box-content {
max-height: 3000px;
}
.pipeline-box:not(.expanded) .box-content {
display: none;
}
.pipeline-box .box-content-inner {
padding: 0.5rem 0;
}
.pipeline-box .box-description {
font-size: 0.95rem;
color: var(--text-clr);
margin-bottom: 1.5rem;
text-align: center;
}
/* Form Elements */
.pipeline-box .form-section {
margin: 1rem 0;
}
.pipeline-box label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: var(--text-clr);
}
.pipeline-box .input-group {
margin-bottom: 1rem;
}
.pipeline-box .input-group input[type="number"] {
margin-bottom: 1rem;
}
.pipeline-box input[type="number"],
.pipeline-box input[type="file"] {
padding: 0.75rem;
border: 2px solid var(--background-clr);
border-radius: 8px;
background-color: var(--background-clr);
color: var(--white-clr);
font-family: 'Montserrat', sans-serif;
font-size: 1rem;
transition: all 0.3s ease;
}
.pipeline-box input[type="number"]:focus,
.pipeline-box input[type="file"]:focus {
outline: none;
border-color: var(--blue-clr);
box-shadow: 0 0 10px rgba(71, 188, 223, 0.5);
}
.pipeline-box input[type="number"] {
-moz-appearance: textfield;
width: 75px;
}
.pipeline-box input[type="number"]::-webkit-outer-spin-button,
.pipeline-box input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.pipeline-box input[type="file"] {
width: 100%;
}
.pipeline-box .file-input-wrapper {
border: 2px dashed var(--border-clr);
border-radius: 8px;
padding: 1.5rem;
text-align: center;
transition: all 0.3s ease;
}
.pipeline-box .file-input-wrapper:hover {
border-color: var(--blue-clr);
}
.pipeline-box .file-input-wrapper input[type="file"] {
display: none;
}
.pipeline-box .file-input-label {
color: var(--blue-clr);
cursor: pointer;
font-weight: 600;
}
.pipeline-box .file-name {
margin-top: 0.5rem;
font-size: 0.9rem;
color: var(--green-clr);
word-break: break-all;
}
/* Buttons */
.pipeline-box button,
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, var(--blue-clr), var(--green-clr));
color: white;
font-family: 'Montserrat-Bold', sans-serif;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 1rem;
}
.pipeline-box button:hover,
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(71, 188, 223, 0.4);
}
.pipeline-box button:active,
.btn:active {
transform: translateY(0);
}
.pipeline-box button:disabled,
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.pipeline-box.button-container {
display: flex;
justify-content: center;
margin-top: 1rem;
}
/* Foldable container */
.collapsible-section {
border: 1px solid var(--border-clr);
border-radius: 0.5rem;
padding: 0.5rem;
background: var(--bg-secondary);
}
.collapsible-header {
cursor: pointer;
font-weight: bold;
padding: 0.5rem;
margin: -0.5rem;
margin-bottom: 0.5rem;
color: var(--green-clr);
}
.collapsible-header:hover {
background: var(--bg-hover);
}
.collapsible-content {
padding: 0.5rem;
display: grid;
gap: 1rem;
}
/* Results Display */
.pipeline-box .results {
margin-top: 1.5rem;
padding: 1rem;
background-color: rgba(71, 188, 223, 0.1);
border-radius: 8px;
border-left: 4px solid var(--blue-clr);
}
.pipeline-box .results p {
margin: 0.5rem 0;
font-size: 1rem;
}
.pipeline-box .results .result-value {
font-family: 'Montserrat-Bold', sans-serif;
color: var(--green-clr);
font-size: 1.2rem;
}
/* ===== Chart Display ===== */
.chart-container {
width: 100%;
margin: 2rem 0 0 0;
padding: 0;
}
.chart-container h2 {
text-align: center;
margin-bottom: 1.5rem;
color: var(--white-clr);
}
.unified-chart {
min-height: 500px;
background-color: var(--background-accent-clr);
border-radius: 15px;
padding: 1.5rem;
margin: 0 auto;
max-width: calc(100vw - 4rem);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
border: 1px solid var(--border-clr);
}
/* Time Period Toggle */
.time-toggle-container {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.time-toggle-button {
padding: 0.5rem 1rem;
border: 2px solid var(--border-clr);
background-color: var(--background-accent-clr);
color: var(--text-clr);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-family: 'Montserrat', sans-serif;
font-size: 0.9rem;
}
.time-toggle-button:hover {
border-color: var(--blue-clr);
background-color: rgba(71, 188, 223, 0.1);
}
.time-toggle-button.active {
background: linear-gradient(135deg, var(--blue-clr), var(--green-clr));
color: white;
border-color: transparent;
box-shadow: 0 4px 12px rgba(71, 188, 223, 0.4);
}
.time-toggle-button.active:hover {
transform: translateY(-2px);
}
/* For active button state via data attribute */
.time-toggle-button[data-active="true"] {
background: linear-gradient(135deg, var(--blue-clr), var(--green-clr));
color: white;
border-color: transparent;
box-shadow: 0 4px 12px rgba(71, 188, 223, 0.4);
}
.time-toggle-button[data-active="true"]:hover {
transform: translateY(-2px);
}
/* ===== Loading States ===== */
.loading-indicator {
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid var(--background-clr);
border-top-color: var(--blue-clr);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.2); }
}
/* ===== Chart Styling ===== */
.chart-container {
background-color: var(--background-clr);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
}
/* Radzen Chart Customization */
:root {
--rz-color-primary: var(--blue-clr);
--rz-color-secondary: var(--green-clr);
--rz-color-text: var(--white-clr);
--rz-color-surface: var(--background-accent-clr);
--rz-color-border: var(--border-clr);
}
.rz-chart {
background-color: var(--background-clr) !important;
}
.rz-chart text {
fill: var(--white-clr) !important;
font-family: 'Montserrat', sans-serif !important;
}
.rz-series-0 path {
stroke: var(--blue-clr) !important;
fill: rgba(71, 188, 223, 0.3) !important;
}
.rz-series-1 path {
stroke: var(--green-clr) !important;
fill: rgba(110, 222, 154, 0.3) !important;
}
.rz-series-2 path {
stroke: var(--purple-clr) !important;
fill: rgba(164, 141, 170, 0.3) !important;
}
.rz-series-3 path {
stroke: var(--orange-clr) !important;
fill: rgba(226, 166, 97, 0.3) !important;
}
.rz-series-4 path {
stroke: #e74c3c !important;
fill: rgba(231, 76, 60, 0.3) !important;
}
.rz-gridline line {
stroke: var(--background-clr) !important;
}
/* ===== Cost Calculation Grid ===== */
.cost-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin: 1rem 0;
}
.cost-grid .input-group {
margin-bottom: 0.5rem;
}
.cost-grid label {
font-size: 0.85rem;
}
.cost-grid input {
font-size: 0.85rem;
padding: 0.5rem;
}
/* ===== Responsive Design ===== */
@media (max-width: 1200px) {
.pipeline-row {
flex-wrap: wrap;
justify-content: center;
}
.pipeline-connector {
display: none;
}
.pipeline-box {
width: 280px;
}
.pipeline-box.expanded {
width: 340px;
}
.unified-chart-container {
width: calc(100% - 2rem);
}
}
@media (max-width: 900px) {
.pipeline-box {
width: 100%;
min-height: 150px;
}
.pipeline-box.expanded {
width: 100%;
}
.pipeline-row {
flex-direction: column;
align-items: center;
}
.pipeline-connector {
display: none;
}
.unified-chart-container {
width: calc(100% - 2rem);
margin: 1rem;
}
}
@media (max-width: 768px) {
main {
margin: 10px;
}
h1 {
font-size: 1.8rem;
}
h2 {
font-size: 1.4rem;
}
.pipeline-box {
min-width: 100%;
}
.pipeline-box.expanded {
min-width: 100%;
transform: none;
}
.unified-chart-container {
padding: 1rem;
margin: 1rem;
}
}
/* ===== Info Text ===== */
.info-text {
background-color: rgba(71, 188, 223, 0.1);
border-left: 4px solid var(--blue-clr);
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
font-size: 0.95rem;
}
.info-text p {
margin: 0;
}
/* ===== Success Message ===== */
.success-message {
background-color: rgba(46, 204, 113, 0.2);
border-left: 4px solid var(--success-clr);
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
font-size: 1rem;
color: var(--success-clr);
}
/* ===== Error Message ===== */
.error-message {
background-color: rgba(231, 76, 60, 0.2);
border-left: 4px solid var(--error-clr);
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
font-size: 1rem;
color: var(--error-clr);
}

Binary file not shown.

BIN
wwwroot/fonts/Righteous.ttf Normal file

Binary file not shown.

View File

@@ -4,16 +4,23 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BattSim</title> <title>BattSim - Energie Simulator</title>
<base href="/" /> <base href="/" />
<link rel="preload" id="webassembly" /> <link rel="preload" id="webassembly" />
<!-- Main CSS with theme -->
<link rel="stylesheet" href="css/site.css" />
<link rel="stylesheet" href="css/app.css" /> <link rel="stylesheet" href="css/app.css" />
<!-- If you add any scoped CSS files, uncomment the following to load them
<link href="BattSim.styles.css" rel="stylesheet" /> --> <!-- Radzen Blazor CSS -->
<link href="_content/Radzen.Blazor/css/default.css" rel="stylesheet">
<!-- Google Fonts fallback if custom fonts not available -->
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&family=Righteous&display=swap" rel="stylesheet">
<link href="manifest.webmanifest" rel="manifest" /> <link href="manifest.webmanifest" rel="manifest" />
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" /> <link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" /> <link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
<link rel="stylesheet" href="_content/Radzen.Blazor/css/default.css">
<script type="importmap"></script> <script type="importmap"></script>
</head> </head>
@@ -31,6 +38,7 @@
<a href="." class="reload">Reload</a> <a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span> <span class="dismiss">🗙</span>
</div> </div>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script> <script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
<script>navigator.serviceWorker.register('service-worker.js', { updateViaCache: 'none' });</script> <script>navigator.serviceWorker.register('service-worker.js', { updateViaCache: 'none' });</script>
<script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script> <script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script>