Compare commits

..

12 Commits

26 changed files with 166282 additions and 33745 deletions

BIN
BattSim Logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -6,9 +6,6 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders> <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest> <ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
<WasmAllowUndefinedSymbols>true</WasmAllowUndefinedSymbols>
<RunAOTCompilation>true</RunAOTCompilation>
<WasmShellGenerateAOTProfile>true</WasmShellGenerateAOTProfile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

27
Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
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
# Install required workloads
RUN dotnet workload install wasm-tools
# Copy project and solution files
COPY *.csproj ./
RUN dotnet restore
# Copy the rest of the application
COPY . .
# Publish the app
RUN dotnet publish -c Release -o /app/publish
# Runtime stage: Use Nginx to serve static files
FROM nginx:alpine
COPY --from=build /app/publish/wwwroot /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

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

@@ -8,5 +8,16 @@ namespace BattSim.Models
public bool DayTariff { 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;
}
} }
} }

View File

@@ -1,121 +1,503 @@
@page "/" @page "/"
@using Radzen @using Radzen
@using Radzen.Blazor @using Radzen.Blazor
@using BattSim.Models @using BattSim.Models
@using BattSim.Services @using BattSim.Services
<PageTitle>BattSim</PageTitle> <PageTitle>Thuisbatterij Simulator</PageTitle>
<h1>BattSim</h1> <h1 style="color: white">Thuisbatterij Simulator</h1>
<h2>Input Data</h2> <div class="pipeline-container">
<div> <!-- Pipeline with 3 connected boxes -->
<p>Upload your Fluvius quarterly csv file here. The longer the timeframe the longer it takes to process.</p> <div class="pipeline-row">
<InputFile OnChange="OnFileUploaded" accept=".csv"/> <div class="pipeline-connector"></div>
@if (_isLoadingFile)
{ <!-- Box 1: Data Upload -->
<p>Loading...</p> <div class="pipeline-box @GetPipelineStepClasses(0)">
} <div class="box-header" @onclick="() => ToggleBox(0)">
else <div class="step-number">1</div>
{ <div class="step-title">Fluvius Data</div>
@if (FluviusDataRaw.Count != 0) <div class="status-indicator"></div>
{ </div>
<p>@(FluviusDataRaw.Count) entries read.</p> <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>
<!-- 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;">
<!-- Vlaamse energieheffing (moved here) -->
<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[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>
@if (FluviusDataDaily.Count != 0)
{
<RadzenChart>
<RadzenAreaSeries Smooth=true Data="@FluviusDataDaily.Values" CategoryProperty="Time" Title="Consumption" ValueProperty="Consumption">
<RadzenChartTooltipOptions Visible="true"/>
</RadzenAreaSeries>
<RadzenAreaSeries Smooth=true Data="@FluviusDataDaily.Values" CategoryProperty="Time" Title="Production" ValueProperty="Production">
<RadzenChartTooltipOptions Visible="true"/>
</RadzenAreaSeries>
<RadzenCategoryAxis Formatter="@FormatObject" Padding="20" LabelAutoRotation="-45">
<RadzenGridLines Visible="true"/>
<RadzenAxisTitle Text="Time"/>
</RadzenCategoryAxis>
<RadzenValueAxis Formatter="@FormatObject">
<RadzenGridLines Visible="true"/>
<RadzenAxisTitle Text="Energy"/>
</RadzenValueAxis>
</RadzenChart>
}
}
</div> </div>
@code{
Dictionary<DateTime, EnergyData> FluviusDataRaw = []; <!-- Chart - Directly on page, full width -->
Dictionary<DateOnly, EnergyData> FluviusDataDaily = []; @if (FilteredFluviusData.Length > 0)
bool _isLoadingFile = false; {
<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 {
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) 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();
FluviusDataRaw = await FluviusDataHandler.LoadAndProcessFile(file); FluviusDataRaw = await FluviusDataHandler.LoadAndProcessFile(file);
FluviusDataDaily = FluviusDataHandler.GenerateDailyData(FluviusDataRaw); SetTimePeriod("all");
_isLoadingFile = false; FilteredSimulationData = [];
StepData[0].isProcessing = false;
StepData[0].Completed = true;
StepData[1].Completed = false;
StepData[2].Completed = false;
SimulationData = [];
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 $"{d:0.##} kWh"; if(value is double d) return $"{d:0.##} kWh";
if(value is DateTime date) return date.ToString("dd/MM/yyyy"); 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; return string.Empty;
} }
} }
<h2>Simulate Battery</h2>
<div>
<p>Generate adjusted energy data simulating the effect of a battery with properties as configured here:</p>
<ul>
<li>
<p>Capacity: <InputNumber @bind-value="BatteryCapacity"/> kWh</p>
</li>
<li>
<p>Discharge Rate: <InputNumber @bind-value="DischargeRate"/> kW</p>
</li>
<li>
<p>Charge Rate: <InputNumber @bind-value="ChargeRate"/> kW</p>
</li>
<li>
<p>Round-trip Efficiency: <InputNumber @bind-value="Efficiency"/> %</p>
</li>
</ul>
<Button @onclick="SimulateBattery">Simulate</Button>
</div>
@code {
double BatteryCapacity = 7.5;
double DischargeRate = 3;
double ChargeRate = 3;
double Efficiency = 90;
BatteryDayResult[] SimulationData = [];
private async Task SimulateBattery(){
Console.WriteLine("Simulating...");
// SimulationData = BatterySimulator.SimulateBattery(EnergyData, BatteryCapacity).ToArray();
Console.WriteLine("Done simulating!");
}
}
<h2>Calculate Cost</h2>

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");

View File

@@ -1,8 +1,40 @@
# 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. You can find it at : [wattif.douwco.be](https://wattif.douwco.be/)
## Used Tools ---
This project makes use of the Radzen.blazer package for UI components.
## How It Works
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);
// // Calculate reduced values batteryCharge = batteryCharge - (energyDrawn / efficiency); // Adjust for loss
// var reducedConsumption = System.Math.Min(usedEnergy, day.TotalConsumption); simulatedBatteryEnergyData.Consumption = Math.Max(deficit, 0);
// var reducedProduction = day.TotalProduction - chargedEnergy;
// Register the current charge
// results.Add(new BatteryDayResult simulatedBatteryEnergyData.BatteryCharge = batteryCharge;
// { results.Add(simulatedBatteryEnergyData);
// 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

@@ -13,7 +13,7 @@ namespace BattSim.Services
{ {
public static class FluviusDataHandler public static class FluviusDataHandler
{ {
public static async Task<Dictionary<DateTime, EnergyData>> LoadAndProcessFile(IBrowserFile file) public static async Task<EnergyData[]> LoadAndProcessFile(IBrowserFile file)
{ {
var lines = await ReadCsvFile(file); var lines = await ReadCsvFile(file);
var energyData = new ConcurrentDictionary<DateTime,EnergyData>(); // Thread-safe collection var energyData = new ConcurrentDictionary<DateTime,EnergyData>(); // Thread-safe collection
@@ -48,21 +48,22 @@ namespace BattSim.Services
Console.WriteLine($"Error parsing line: {line}. Skipping to next line. Exception: {e}"); Console.WriteLine($"Error parsing line: {line}. Skipping to next line. Exception: {e}");
} }
}); });
return energyData.ToDictionary(); var results = energyData.Values.ToArray();
results.Sort((a,b)=>a.Time.CompareTo(b.Time));
return results;
} }
public static Dictionary<DateOnly, EnergyData> GenerateDailyData(Dictionary<DateTime, EnergyData> energyData) public static EnergyData[] GenerateDailyData(EnergyData[] energyData)
{ {
var dailyEnergyData = new ConcurrentDictionary<DateOnly, EnergyData>(); var dailyEnergyData = new ConcurrentDictionary<DateOnly, EnergyData>();
Parallel.ForEach(energyData, entry => Parallel.ForEach(energyData, energy =>
{ {
var date = DateOnly.FromDateTime(entry.Key); var date = DateOnly.FromDateTime(energy.Time);
var energy = entry.Value;
// Use AddOrUpdate to avoid double lookup // Use AddOrUpdate to avoid double lookup
dailyEnergyData.AddOrUpdate( dailyEnergyData.AddOrUpdate(
date, date,
energy, // If key doesn't exist, add this value new EnergyData(energy), // If key doesn't exist, add this value
(_, existing) => (_, existing) =>
{ {
// If key exists, aggregate the values // If key exists, aggregate the values
@@ -72,7 +73,7 @@ namespace BattSim.Services
} }
); );
}); });
return dailyEnergyData.ToDictionary(); return dailyEnergyData.Values.ToArray();
} }
private static async Task<List<string>> ReadCsvFile(IBrowserFile file) private static async Task<List<string>> ReadCsvFile(IBrowserFile file)
@@ -84,7 +85,7 @@ namespace BattSim.Services
await reader.ReadLineAsync(); await reader.ReadLineAsync();
// Read all lines into memory // Read all lines into memory
var lines = new List<string>(); var lines = new List<string>();
string line; string? line;
while ((line = await reader.ReadLineAsync()) is not null) while ((line = await reader.ReadLineAsync()) is not null)
{ {
lines.Add(line); lines.Add(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

12
docker-compose.yml Normal file
View File

@@ -0,0 +1,12 @@
version: '3.8'
services:
wattif:
build:
context: .
dockerfile: Dockerfile
ports:
- "5000:80"
restart: unless-stopped
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf

18
nginx.conf Normal file
View File

@@ -0,0 +1,18 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html =404;
}
include /etc/nginx/mime.types;
types {
application/wasm wasm;
application/manifest+json webmanifest;
application/octet-stream dll pdb webcil;
}
}

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>