From 662eba29ae46b1ff4e5b1bb8a87e34c02cd3aacf Mon Sep 17 00:00:00 2001 From: Thomas Bergmann Date: Sun, 2 Nov 2025 13:31:21 +0100 Subject: [PATCH 01/16] feat: add influx dao for weather --- build.gradle.kts | 3 +- devices/build.gradle.kts | 4 + .../capi/business/config/CarUsageProfile.java | 4 +- .../capi/business/config/DeviceConfig.java | 8 +- .../capi/business/config/FlatPriceConfig.java | 2 +- .../config/ImportExportChargeConfig.java | 4 +- .../config/SimulationTimeSettings.java | 4 +- .../capi/business/device/DeviceState.java | 4 +- .../forecast/EnergyPriceForecast.java | 2 +- .../business/forecast/WeatherForecast.java | 2 +- .../business/simulation/EnergySystem.java | 4 +- .../business/statistics/HourlyEfficiency.java | 4 +- .../statistics/SolarPanelStatistics.java | 2 +- .../eos/devices/capi/units/PowerUnits.java | 6 - .../internal/business/devices/Battery.java | 6 +- .../business/devices/ElectricCar.java | 2 +- .../internal/business/devices/Grid.java | 4 +- .../internal/business/devices/NoisyUsage.java | 2 +- .../internal/business/devices/SolarPanel.java | 4 +- .../forecast/CsvStockEnergyPriceProvider.java | 2 +- .../business/forecast/FlatPriceService.java | 2 +- .../business/forecast/FlatWeatherService.java | 2 +- .../forecast/InfluxWeatherForecast.java | 25 ++ .../statistics/CurvePanelStatistic.java | 2 +- .../statistics/SolarPanelStatisticsImpl.java | 2 +- .../config/ConfigurationLoaderTest.java | 2 +- .../business/config/CsvPriceConfigTest.java | 2 +- .../SolarPanelStatisticsLoaderTest.java | 2 +- .../business/devices/BatteryTest.java | 6 +- .../business/devices/ElectricCarTest.java | 6 +- .../internal/business/devices/GridTest.java | 4 +- .../business/devices/NoisyUsageTest.java | 4 +- .../business/devices/SolarPanelTest.java | 6 +- .../forecast/FlatWeatherServiceTest.java | 2 +- .../business/simulation/SimulationTest.java | 8 +- .../statistics/CurvePanelStatisticTest.java | 2 +- forecast/build.gradle.kts | 14 + .../capi/WeatherForecastImporter.java | 8 + .../src/main/resources/application.properties | 10 + .../src/test/resources/weather_20251024.png | Bin .../src/test/resources/weather_20251027.png | Bin optimization/build.gradle.kts | 1 + .../capi/business/OptimizationGoal.java | 2 +- .../capi/business/OptimizationResult.java | 2 +- .../capi/goals/CarChargeGoal.java | 4 +- .../capi/goals/GridUsingGoal.java | 2 +- .../capi/goals/OptimizationGoals.java | 2 +- .../capi/goals/PercentagePenalty.java | 4 +- .../capi/goals/CarChargeGoalTest.java | 6 +- .../capi/goals/GridUsingGoalTest.java | 4 +- .../capi/goals/OptimizationGoalsTest.java | 6 +- .../internal/business/OptimizerTest.java | 2 +- .../OptimizationConfigurationLoaderTest.java | 2 +- persistence-api/build.gradle.kts | 12 + .../de/hatoka/eos/persistence/capi}/Dao.java | 2 +- .../persistence/capi/WeatherForcastDAO.java | 7 + .../persistence/capi/WeatherForecastPO.java | 19 ++ persistence-influx/build.gradle.kts | 18 ++ .../persistence/influx/InfluxDBConfig.java | 33 +++ .../influx/InfluxWeatherForecastDao.java | 114 ++++++++ .../src/main/resources/META-INF/beans.xml | 1 + .../influx/InfluxWeatherForecastDaoTest.java | 245 ++++++++++++++++++ .../src/test/resources/application.yml | 8 + settings.gradle | 4 + units/build.gradle.kts | 10 + .../de/hatoka/eos/units/capi}/Energy.java | 2 +- .../eos/units/capi}/EnergyJsonConverter.java | 8 +- .../hatoka/eos/units/capi}/EnergyUnits.java | 2 +- .../capi}/LocalDateTimeJsonConverter.java | 7 +- .../units/capi}/LocalTimeJsonConverter.java | 4 +- .../java/de/hatoka/eos/units/capi}/Money.java | 2 +- .../eos/units/capi}/MoneyJsonConverter.java | 10 +- .../de/hatoka/eos/units/capi}/Percentage.java | 2 +- .../units/capi}/PercentageJsonConverter.java | 4 +- .../java/de/hatoka/eos/units/capi}/Power.java | 3 +- .../eos/units/capi}/PowerJsonConverter.java | 8 +- .../de/hatoka/eos/units/capi/PowerUnits.java | 6 + .../units/capi}/TimeZoneJsonConverter.java | 8 +- 78 files changed, 645 insertions(+), 113 deletions(-) delete mode 100644 devices/src/main/java/de/hatoka/eos/devices/capi/units/PowerUnits.java create mode 100644 devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/InfluxWeatherForecast.java create mode 100644 forecast/build.gradle.kts create mode 100644 forecast/src/main/java/de/hatoka/eos/forecast/capi/WeatherForecastImporter.java create mode 100644 forecast/src/main/resources/application.properties rename {devices => forecast}/src/test/resources/weather_20251024.png (100%) rename {devices => forecast}/src/test/resources/weather_20251027.png (100%) create mode 100644 persistence-api/build.gradle.kts rename {devices/src/main/java/de/hatoka/eos/devices/capi/persistence => persistence-api/src/main/java/de/hatoka/eos/persistence/capi}/Dao.java (91%) create mode 100644 persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForcastDAO.java create mode 100644 persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastPO.java create mode 100644 persistence-influx/build.gradle.kts create mode 100644 persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxDBConfig.java create mode 100644 persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java create mode 100644 persistence-influx/src/main/resources/META-INF/beans.xml create mode 100644 persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java create mode 100644 persistence-influx/src/test/resources/application.yml create mode 100644 units/build.gradle.kts rename {devices/src/main/java/de/hatoka/eos/devices/capi/units => units/src/main/java/de/hatoka/eos/units/capi}/Energy.java (97%) rename {devices/src/main/java/de/hatoka/eos/devices/capi/units => units/src/main/java/de/hatoka/eos/units/capi}/EnergyJsonConverter.java (95%) rename {devices/src/main/java/de/hatoka/eos/devices/capi/units => units/src/main/java/de/hatoka/eos/units/capi}/EnergyUnits.java (83%) rename {devices/src/main/java/de/hatoka/eos/devices/capi/units => units/src/main/java/de/hatoka/eos/units/capi}/LocalDateTimeJsonConverter.java (88%) rename {devices/src/main/java/de/hatoka/eos/devices/capi/units => units/src/main/java/de/hatoka/eos/units/capi}/LocalTimeJsonConverter.java (96%) rename {devices/src/main/java/de/hatoka/eos/devices/capi/units => units/src/main/java/de/hatoka/eos/units/capi}/Money.java (98%) rename {devices/src/main/java/de/hatoka/eos/devices/capi/units => units/src/main/java/de/hatoka/eos/units/capi}/MoneyJsonConverter.java (84%) rename {devices/src/main/java/de/hatoka/eos/devices/capi/units => units/src/main/java/de/hatoka/eos/units/capi}/Percentage.java (97%) rename {devices/src/main/java/de/hatoka/eos/devices/capi/units => units/src/main/java/de/hatoka/eos/units/capi}/PercentageJsonConverter.java (96%) rename {devices/src/main/java/de/hatoka/eos/devices/capi/units => units/src/main/java/de/hatoka/eos/units/capi}/Power.java (95%) rename {devices/src/main/java/de/hatoka/eos/devices/capi/units => units/src/main/java/de/hatoka/eos/units/capi}/PowerJsonConverter.java (95%) create mode 100644 units/src/main/java/de/hatoka/eos/units/capi/PowerUnits.java rename {devices/src/main/java/de/hatoka/eos/devices/capi/units => units/src/main/java/de/hatoka/eos/units/capi}/TimeZoneJsonConverter.java (83%) diff --git a/build.gradle.kts b/build.gradle.kts index 48e395d..2d9241e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -46,7 +46,8 @@ subprojects { } withType { useJUnitPlatform() - // environment "API_KEY", System.properties['API_KEY'] + environment("INFLUX_URI", System.getenv("INFLUX_URI") ?: "http://localhost:8086") + environment("INFLUX_TOKEN", System.getenv("INFLUX_TOKEN") ?: "eos-token-invalid") } withType { diff --git a/devices/build.gradle.kts b/devices/build.gradle.kts index ae5dc3f..4955fa1 100644 --- a/devices/build.gradle.kts +++ b/devices/build.gradle.kts @@ -5,6 +5,10 @@ plugins { } dependencies { + implementation(project(":units")) + implementation(project(":persistence-api")) + implementation(project(":persistence-influx")) + implementation("org.slf4j:slf4j-api") implementation("org.apache.commons:commons-csv") diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/CarUsageProfile.java b/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/CarUsageProfile.java index 44e0b6b..6cb4711 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/CarUsageProfile.java +++ b/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/CarUsageProfile.java @@ -4,8 +4,8 @@ import com.fasterxml.jackson.annotation.JsonPropertyDescription; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import de.hatoka.eos.devices.capi.units.Energy; -import de.hatoka.eos.devices.capi.units.LocalTimeJsonConverter; +import de.hatoka.eos.units.capi.Energy; +import de.hatoka.eos.units.capi.LocalTimeJsonConverter; import java.time.DayOfWeek; import java.time.LocalTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/DeviceConfig.java b/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/DeviceConfig.java index 10e1b87..f3a3d01 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/DeviceConfig.java +++ b/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/DeviceConfig.java @@ -7,10 +7,10 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import de.hatoka.eos.devices.capi.business.device.DeviceType; import de.hatoka.eos.devices.capi.business.statistics.SolarPanelStatistics; -import de.hatoka.eos.devices.capi.units.Energy; -import de.hatoka.eos.devices.capi.units.Percentage; -import de.hatoka.eos.devices.capi.units.PercentageJsonConverter; -import de.hatoka.eos.devices.capi.units.Power; +import de.hatoka.eos.units.capi.Energy; +import de.hatoka.eos.units.capi.Percentage; +import de.hatoka.eos.units.capi.PercentageJsonConverter; +import de.hatoka.eos.units.capi.Power; import de.hatoka.eos.devices.internal.business.statistics.CurvePanelStatistic; import de.hatoka.eos.devices.internal.business.statistics.SolarPanelStatisticsImpl; import de.hatoka.eos.devices.internal.business.statistics.SolarPanelStatisticsLoader; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/FlatPriceConfig.java b/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/FlatPriceConfig.java index 7f3298e..fb6becb 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/FlatPriceConfig.java +++ b/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/FlatPriceConfig.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Money; /** * The FlatPriceConfig defines the import and export prices, which are static or flat. diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/ImportExportChargeConfig.java b/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/ImportExportChargeConfig.java index aa39d29..d8e93cb 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/ImportExportChargeConfig.java +++ b/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/ImportExportChargeConfig.java @@ -2,8 +2,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; -import de.hatoka.eos.devices.capi.units.Energy; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Energy; +import de.hatoka.eos.units.capi.Money; /** * The ImportExportChargeConfig defines additional charge or fee for import and export of energy, depending on the energy provider. diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/SimulationTimeSettings.java b/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/SimulationTimeSettings.java index 1c081dc..0bf8378 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/SimulationTimeSettings.java +++ b/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/SimulationTimeSettings.java @@ -5,8 +5,8 @@ import com.fasterxml.jackson.annotation.JsonPropertyDescription; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import de.hatoka.eos.devices.capi.units.LocalDateTimeJsonConverter; -import de.hatoka.eos.devices.capi.units.TimeZoneJsonConverter; +import de.hatoka.eos.units.capi.LocalDateTimeJsonConverter; +import de.hatoka.eos.units.capi.TimeZoneJsonConverter; import java.time.Duration; import java.time.LocalDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/device/DeviceState.java b/devices/src/main/java/de/hatoka/eos/devices/capi/business/device/DeviceState.java index 6e75b7e..81bbccf 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/device/DeviceState.java +++ b/devices/src/main/java/de/hatoka/eos/devices/capi/business/device/DeviceState.java @@ -1,7 +1,7 @@ package de.hatoka.eos.devices.capi.business.device; -import de.hatoka.eos.devices.capi.units.Energy; -import de.hatoka.eos.devices.capi.units.Percentage; +import de.hatoka.eos.units.capi.Energy; +import de.hatoka.eos.units.capi.Percentage; /** * DeviceState represents the state of a device in a simulation context. diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/EnergyPriceForecast.java b/devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/EnergyPriceForecast.java index ef16b50..d427538 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/EnergyPriceForecast.java +++ b/devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/EnergyPriceForecast.java @@ -1,6 +1,6 @@ package de.hatoka.eos.devices.capi.business.forecast; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Money; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/WeatherForecast.java b/devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/WeatherForecast.java index c2853be..5d37136 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/WeatherForecast.java +++ b/devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/WeatherForecast.java @@ -1,6 +1,6 @@ package de.hatoka.eos.devices.capi.business.forecast; -import de.hatoka.eos.devices.capi.units.Percentage; +import de.hatoka.eos.units.capi.Percentage; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/EnergySystem.java b/devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/EnergySystem.java index 7432cf9..8576fdd 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/EnergySystem.java +++ b/devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/EnergySystem.java @@ -1,7 +1,7 @@ package de.hatoka.eos.devices.capi.business.simulation; -import de.hatoka.eos.devices.capi.units.Energy; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Energy; +import de.hatoka.eos.units.capi.Money; /** * EnergySystem represents the energy flow in a system, including production, charging, discharging, consumption, import, export, and costs. All diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/HourlyEfficiency.java b/devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/HourlyEfficiency.java index 1fb271f..df2ae80 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/HourlyEfficiency.java +++ b/devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/HourlyEfficiency.java @@ -4,8 +4,8 @@ import com.fasterxml.jackson.annotation.JsonPropertyDescription; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import de.hatoka.eos.devices.capi.units.Percentage; -import de.hatoka.eos.devices.capi.units.PercentageJsonConverter; +import de.hatoka.eos.units.capi.Percentage; +import de.hatoka.eos.units.capi.PercentageJsonConverter; public class HourlyEfficiency { diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/SolarPanelStatistics.java b/devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/SolarPanelStatistics.java index da2a11a..3bcac72 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/SolarPanelStatistics.java +++ b/devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/SolarPanelStatistics.java @@ -1,6 +1,6 @@ package de.hatoka.eos.devices.capi.business.statistics; -import de.hatoka.eos.devices.capi.units.Percentage; +import de.hatoka.eos.units.capi.Percentage; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/units/PowerUnits.java b/devices/src/main/java/de/hatoka/eos/devices/capi/units/PowerUnits.java deleted file mode 100644 index 307da2d..0000000 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/units/PowerUnits.java +++ /dev/null @@ -1,6 +0,0 @@ -package de.hatoka.eos.devices.capi.units; - -public enum PowerUnits -{ - K_W; -} diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/Battery.java b/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/Battery.java index 24f6978..caaab5e 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/Battery.java +++ b/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/Battery.java @@ -6,9 +6,9 @@ import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; -import de.hatoka.eos.devices.capi.units.Energy; -import de.hatoka.eos.devices.capi.units.Percentage; -import de.hatoka.eos.devices.capi.units.Power; +import de.hatoka.eos.units.capi.Energy; +import de.hatoka.eos.units.capi.Percentage; +import de.hatoka.eos.units.capi.Power; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/ElectricCar.java b/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/ElectricCar.java index 35aa1eb..1295a32 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/ElectricCar.java +++ b/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/ElectricCar.java @@ -7,7 +7,7 @@ import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; -import de.hatoka.eos.devices.capi.units.Energy; +import de.hatoka.eos.units.capi.Energy; import java.time.LocalTime; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/Grid.java b/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/Grid.java index 572a528..308f0d9 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/Grid.java +++ b/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/Grid.java @@ -5,8 +5,8 @@ import de.hatoka.eos.devices.capi.business.device.DeviceState; import de.hatoka.eos.devices.capi.business.forecast.EnergyPriceForecast; import de.hatoka.eos.devices.capi.business.simulation.*; -import de.hatoka.eos.devices.capi.units.Energy; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Energy; +import de.hatoka.eos.units.capi.Money; public class Grid implements Device { diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/NoisyUsage.java b/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/NoisyUsage.java index 678c700..9ee8c64 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/NoisyUsage.java +++ b/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/NoisyUsage.java @@ -6,7 +6,7 @@ import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; -import de.hatoka.eos.devices.capi.units.Power; +import de.hatoka.eos.units.capi.Power; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/SolarPanel.java b/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/SolarPanel.java index aba2b0a..0934a8d 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/SolarPanel.java +++ b/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/SolarPanel.java @@ -7,8 +7,8 @@ import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; import de.hatoka.eos.devices.capi.business.statistics.SolarPanelStatistics; -import de.hatoka.eos.devices.capi.units.Percentage; -import de.hatoka.eos.devices.capi.units.Power; +import de.hatoka.eos.units.capi.Percentage; +import de.hatoka.eos.units.capi.Power; public class SolarPanel implements Device { diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/CsvStockEnergyPriceProvider.java b/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/CsvStockEnergyPriceProvider.java index 8abb7c8..021b17f 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/CsvStockEnergyPriceProvider.java +++ b/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/CsvStockEnergyPriceProvider.java @@ -2,7 +2,7 @@ import de.hatoka.eos.devices.capi.business.config.CsvPriceConfig; import de.hatoka.eos.devices.capi.business.forecast.EnergyPriceForecast; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Money; import java.io.BufferedReader; import java.io.IOException; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/FlatPriceService.java b/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/FlatPriceService.java index b8a46c9..2a31a1c 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/FlatPriceService.java +++ b/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/FlatPriceService.java @@ -2,7 +2,7 @@ import de.hatoka.eos.devices.capi.business.config.FlatPriceConfig; import de.hatoka.eos.devices.capi.business.forecast.EnergyPriceForecast; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Money; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/FlatWeatherService.java b/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/FlatWeatherService.java index 9c50240..1c831d8 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/FlatWeatherService.java +++ b/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/FlatWeatherService.java @@ -1,7 +1,7 @@ package de.hatoka.eos.devices.internal.business.forecast; import de.hatoka.eos.devices.capi.business.forecast.WeatherForecast; -import de.hatoka.eos.devices.capi.units.Percentage; +import de.hatoka.eos.units.capi.Percentage; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/InfluxWeatherForecast.java b/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/InfluxWeatherForecast.java new file mode 100644 index 0000000..4fdb7db --- /dev/null +++ b/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/InfluxWeatherForecast.java @@ -0,0 +1,25 @@ +package de.hatoka.eos.devices.internal.business.forecast; + +import de.hatoka.eos.devices.capi.business.forecast.WeatherForecast; +import de.hatoka.eos.persistence.influx.InfluxWeatherForecastDao; +import de.hatoka.eos.units.capi.Percentage; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import java.time.ZonedDateTime; + +/** + * Weather forecast implementation that reads from InfluxDB via ForecastDAO. + */ +@Singleton +public class InfluxWeatherForecast implements WeatherForecast +{ + @Inject + InfluxWeatherForecastDao forecastDAO; + + @Override + public Percentage getSunProbability(ZonedDateTime time) + { + return forecastDAO.get(time).getSunProbability(); + } +} diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/CurvePanelStatistic.java b/devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/CurvePanelStatistic.java index ba39075..aa8d08c 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/CurvePanelStatistic.java +++ b/devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/CurvePanelStatistic.java @@ -1,7 +1,7 @@ package de.hatoka.eos.devices.internal.business.statistics; import de.hatoka.eos.devices.capi.business.statistics.SolarPanelStatistics; -import de.hatoka.eos.devices.capi.units.Percentage; +import de.hatoka.eos.units.capi.Percentage; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/SolarPanelStatisticsImpl.java b/devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/SolarPanelStatisticsImpl.java index 2d8c08e..66b2f09 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/SolarPanelStatisticsImpl.java +++ b/devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/SolarPanelStatisticsImpl.java @@ -3,7 +3,7 @@ import de.hatoka.eos.devices.capi.business.statistics.HourlyEfficiency; import de.hatoka.eos.devices.capi.business.statistics.SolarPanelStatisticsConfig; import de.hatoka.eos.devices.capi.business.statistics.SolarPanelStatistics; -import de.hatoka.eos.devices.capi.units.Percentage; +import de.hatoka.eos.units.capi.Percentage; import java.time.ZonedDateTime; import java.util.HashMap; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/ConfigurationLoaderTest.java b/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/ConfigurationLoaderTest.java index ff15c1e..430b035 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/ConfigurationLoaderTest.java +++ b/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/ConfigurationLoaderTest.java @@ -4,7 +4,7 @@ import de.hatoka.eos.devices.capi.business.config.InstallationConfig; import de.hatoka.eos.devices.capi.business.config.SimulationConfig; import de.hatoka.eos.devices.capi.business.device.DeviceType; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Money; import de.hatoka.eos.devices.internal.business.DateTooling; import org.junit.jupiter.api.Test; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/CsvPriceConfigTest.java b/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/CsvPriceConfigTest.java index 00aefc8..ed6987a 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/CsvPriceConfigTest.java +++ b/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/CsvPriceConfigTest.java @@ -3,7 +3,7 @@ import de.hatoka.eos.devices.capi.business.config.CsvPriceConfig; import de.hatoka.eos.devices.capi.business.config.GridConfig; import de.hatoka.eos.devices.capi.business.config.InstallationConfig; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Money; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/SolarPanelStatisticsLoaderTest.java b/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/SolarPanelStatisticsLoaderTest.java index d9e5198..3ff4bc8 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/SolarPanelStatisticsLoaderTest.java +++ b/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/SolarPanelStatisticsLoaderTest.java @@ -2,7 +2,7 @@ import de.hatoka.eos.devices.capi.business.statistics.HourlyEfficiency; import de.hatoka.eos.devices.capi.business.statistics.SolarPanelStatisticsConfig; -import de.hatoka.eos.devices.capi.units.Percentage; +import de.hatoka.eos.units.capi.Percentage; import de.hatoka.eos.devices.internal.business.statistics.SolarPanelStatisticsLoader; import org.junit.jupiter.api.Test; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/BatteryTest.java b/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/BatteryTest.java index 9f33749..78c7f6c 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/BatteryTest.java +++ b/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/BatteryTest.java @@ -5,9 +5,9 @@ import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; -import de.hatoka.eos.devices.capi.units.Energy; -import de.hatoka.eos.devices.capi.units.Percentage; -import de.hatoka.eos.devices.capi.units.Power; +import de.hatoka.eos.units.capi.Energy; +import de.hatoka.eos.units.capi.Percentage; +import de.hatoka.eos.units.capi.Power; import de.hatoka.eos.devices.internal.business.DateTooling; import org.junit.jupiter.api.Test; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/ElectricCarTest.java b/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/ElectricCarTest.java index 1f81919..5b4c0c6 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/ElectricCarTest.java +++ b/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/ElectricCarTest.java @@ -6,9 +6,9 @@ import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; -import de.hatoka.eos.devices.capi.units.Energy; -import de.hatoka.eos.devices.capi.units.Percentage; -import de.hatoka.eos.devices.capi.units.Power; +import de.hatoka.eos.units.capi.Energy; +import de.hatoka.eos.units.capi.Percentage; +import de.hatoka.eos.units.capi.Power; import de.hatoka.eos.devices.internal.business.DateTooling; import org.junit.jupiter.api.Test; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/GridTest.java b/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/GridTest.java index 35aa3ae..a6ab691 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/GridTest.java +++ b/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/GridTest.java @@ -4,8 +4,8 @@ import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; -import de.hatoka.eos.devices.capi.units.Energy; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Energy; +import de.hatoka.eos.units.capi.Money; import de.hatoka.eos.devices.internal.business.DateTooling; import org.junit.jupiter.api.Test; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/NoisyUsageTest.java b/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/NoisyUsageTest.java index 502abae..4570173 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/NoisyUsageTest.java +++ b/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/NoisyUsageTest.java @@ -5,8 +5,8 @@ import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; -import de.hatoka.eos.devices.capi.units.Energy; -import de.hatoka.eos.devices.capi.units.Power; +import de.hatoka.eos.units.capi.Energy; +import de.hatoka.eos.units.capi.Power; import de.hatoka.eos.devices.internal.business.DateTooling; import org.junit.jupiter.api.Test; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/SolarPanelTest.java b/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/SolarPanelTest.java index d12320a..4010df0 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/SolarPanelTest.java +++ b/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/SolarPanelTest.java @@ -5,9 +5,9 @@ import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; -import de.hatoka.eos.devices.capi.units.Energy; -import de.hatoka.eos.devices.capi.units.Percentage; -import de.hatoka.eos.devices.capi.units.Power; +import de.hatoka.eos.units.capi.Energy; +import de.hatoka.eos.units.capi.Percentage; +import de.hatoka.eos.units.capi.Power; import de.hatoka.eos.devices.internal.business.DateTooling; import org.junit.jupiter.api.Test; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/forecast/FlatWeatherServiceTest.java b/devices/src/test/java/de/hatoka/eos/devices/internal/business/forecast/FlatWeatherServiceTest.java index 25fb902..98882b9 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/forecast/FlatWeatherServiceTest.java +++ b/devices/src/test/java/de/hatoka/eos/devices/internal/business/forecast/FlatWeatherServiceTest.java @@ -1,7 +1,7 @@ package de.hatoka.eos.devices.internal.business.forecast; import de.hatoka.eos.devices.capi.business.forecast.WeatherForecast; -import de.hatoka.eos.devices.capi.units.Percentage; +import de.hatoka.eos.units.capi.Percentage; import de.hatoka.eos.devices.internal.business.DateTooling; import org.junit.jupiter.api.Test; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/simulation/SimulationTest.java b/devices/src/test/java/de/hatoka/eos/devices/internal/business/simulation/SimulationTest.java index 6915d07..cecc8ed 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/simulation/SimulationTest.java +++ b/devices/src/test/java/de/hatoka/eos/devices/internal/business/simulation/SimulationTest.java @@ -8,10 +8,10 @@ import de.hatoka.eos.devices.capi.business.simulation.SimulationRequest; import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; import de.hatoka.eos.devices.capi.business.simulation.Simulator; -import de.hatoka.eos.devices.capi.units.Energy; -import de.hatoka.eos.devices.capi.units.Money; -import de.hatoka.eos.devices.capi.units.Percentage; -import de.hatoka.eos.devices.capi.units.Power; +import de.hatoka.eos.units.capi.Energy; +import de.hatoka.eos.units.capi.Money; +import de.hatoka.eos.units.capi.Percentage; +import de.hatoka.eos.units.capi.Power; import de.hatoka.eos.devices.internal.business.DateTooling; import de.hatoka.eos.devices.internal.business.config.ConfigurationLoader; import de.hatoka.eos.devices.internal.business.devices.Battery; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/statistics/CurvePanelStatisticTest.java b/devices/src/test/java/de/hatoka/eos/devices/internal/business/statistics/CurvePanelStatisticTest.java index c24dfa5..e856110 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/statistics/CurvePanelStatisticTest.java +++ b/devices/src/test/java/de/hatoka/eos/devices/internal/business/statistics/CurvePanelStatisticTest.java @@ -1,6 +1,6 @@ package de.hatoka.eos.devices.internal.business.statistics; -import de.hatoka.eos.devices.capi.units.Percentage; +import de.hatoka.eos.units.capi.Percentage; import de.hatoka.eos.devices.internal.business.DateTooling; import org.junit.jupiter.api.Test; diff --git a/forecast/build.gradle.kts b/forecast/build.gradle.kts new file mode 100644 index 0000000..d5f436e --- /dev/null +++ b/forecast/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + java + id("io.quarkus") +} + +dependencies { + implementation(project(":units")) + + implementation("com.fasterxml.jackson.core:jackson-annotations") + implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("org.slf4j:slf4j-api") + + testImplementation("io.quarkus:quarkus-junit5") +} diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/capi/WeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/capi/WeatherForecastImporter.java new file mode 100644 index 0000000..a9340dd --- /dev/null +++ b/forecast/src/main/java/de/hatoka/eos/forecast/capi/WeatherForecastImporter.java @@ -0,0 +1,8 @@ +package de.hatoka.eos.forecast.capi; + +/** + * WeatherForecastImporter retrieves weather data and stores the data at influx db. + */ +public class WeatherForecastImporter +{ +} diff --git a/forecast/src/main/resources/application.properties b/forecast/src/main/resources/application.properties new file mode 100644 index 0000000..77661f7 --- /dev/null +++ b/forecast/src/main/resources/application.properties @@ -0,0 +1,10 @@ +# InfluxDB Configuration +influxdb.url=http://localhost:8086 +influxdb.token=your-influxdb-token +influxdb.org=eos +influxdb.bucket=forecast + +# Quarkus Configuration +quarkus.arc.remove-unused-beans=false +quarkus.log.level=INFO +quarkus.log.category."de.hatoka.eos.forecast".level=DEBUG diff --git a/devices/src/test/resources/weather_20251024.png b/forecast/src/test/resources/weather_20251024.png similarity index 100% rename from devices/src/test/resources/weather_20251024.png rename to forecast/src/test/resources/weather_20251024.png diff --git a/devices/src/test/resources/weather_20251027.png b/forecast/src/test/resources/weather_20251027.png similarity index 100% rename from devices/src/test/resources/weather_20251027.png rename to forecast/src/test/resources/weather_20251027.png diff --git a/optimization/build.gradle.kts b/optimization/build.gradle.kts index 1d70f3c..3c2f92e 100644 --- a/optimization/build.gradle.kts +++ b/optimization/build.gradle.kts @@ -6,6 +6,7 @@ plugins { dependencies { implementation(project(":devices")) + implementation(project(":units")) implementation("org.slf4j:slf4j-api") diff --git a/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/OptimizationGoal.java b/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/OptimizationGoal.java index ac592ff..4fcb9e5 100644 --- a/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/OptimizationGoal.java +++ b/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/OptimizationGoal.java @@ -1,7 +1,7 @@ package de.hatoka.eos.optimization.capi.business; import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Money; /** * OptimizationGoal defines a goal of optimization, each goal is represented by a penalty if the goal was not reached. diff --git a/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/OptimizationResult.java b/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/OptimizationResult.java index 217e330..674f35f 100644 --- a/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/OptimizationResult.java +++ b/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/OptimizationResult.java @@ -1,7 +1,7 @@ package de.hatoka.eos.optimization.capi.business; import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Money; /** * OptimizationResult contains the simulation result and the calculated penalty for optimization goals. diff --git a/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/CarChargeGoal.java b/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/CarChargeGoal.java index 5374aa9..af508c2 100644 --- a/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/CarChargeGoal.java +++ b/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/CarChargeGoal.java @@ -4,8 +4,8 @@ import de.hatoka.eos.devices.capi.business.device.DeviceState; import de.hatoka.eos.devices.capi.business.device.DeviceType; import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; -import de.hatoka.eos.devices.capi.units.Money; -import de.hatoka.eos.devices.capi.units.Percentage; +import de.hatoka.eos.units.capi.Money; +import de.hatoka.eos.units.capi.Percentage; import de.hatoka.eos.optimization.capi.business.OptimizationGoal; import java.util.List; diff --git a/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/GridUsingGoal.java b/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/GridUsingGoal.java index 1b270eb..eefac37 100644 --- a/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/GridUsingGoal.java +++ b/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/GridUsingGoal.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Money; import de.hatoka.eos.optimization.capi.business.OptimizationGoal; /** diff --git a/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/OptimizationGoals.java b/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/OptimizationGoals.java index b8b8b82..11dba6c 100644 --- a/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/OptimizationGoals.java +++ b/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/OptimizationGoals.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Money; import de.hatoka.eos.optimization.capi.business.OptimizationGoal; /** diff --git a/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/PercentagePenalty.java b/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/PercentagePenalty.java index ee81b11..2d668fb 100644 --- a/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/PercentagePenalty.java +++ b/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/PercentagePenalty.java @@ -1,8 +1,8 @@ package de.hatoka.eos.optimization.capi.goals; import com.fasterxml.jackson.annotation.JsonProperty; -import de.hatoka.eos.devices.capi.units.Money; -import de.hatoka.eos.devices.capi.units.Percentage; +import de.hatoka.eos.units.capi.Money; +import de.hatoka.eos.units.capi.Percentage; /** * PercentagePenalty declares the penalty for non reached percentage goals. diff --git a/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/CarChargeGoalTest.java b/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/CarChargeGoalTest.java index 3be3721..8b7c4ae 100644 --- a/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/CarChargeGoalTest.java +++ b/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/CarChargeGoalTest.java @@ -5,9 +5,9 @@ import de.hatoka.eos.devices.capi.business.device.DeviceType; import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; -import de.hatoka.eos.devices.capi.units.Energy; -import de.hatoka.eos.devices.capi.units.Money; -import de.hatoka.eos.devices.capi.units.Percentage; +import de.hatoka.eos.units.capi.Energy; +import de.hatoka.eos.units.capi.Money; +import de.hatoka.eos.units.capi.Percentage; import de.hatoka.eos.optimization.internal.business.config.OptimizationConfigurationLoader; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; diff --git a/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/GridUsingGoalTest.java b/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/GridUsingGoalTest.java index 4ec3c14..f015550 100644 --- a/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/GridUsingGoalTest.java +++ b/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/GridUsingGoalTest.java @@ -2,8 +2,8 @@ import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; -import de.hatoka.eos.devices.capi.units.Energy; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Energy; +import de.hatoka.eos.units.capi.Money; import org.junit.jupiter.api.Test; import java.util.Map; diff --git a/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/OptimizationGoalsTest.java b/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/OptimizationGoalsTest.java index 7eab46b..f492a57 100644 --- a/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/OptimizationGoalsTest.java +++ b/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/OptimizationGoalsTest.java @@ -5,9 +5,9 @@ import de.hatoka.eos.devices.capi.business.device.DeviceType; import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; -import de.hatoka.eos.devices.capi.units.Energy; -import de.hatoka.eos.devices.capi.units.Money; -import de.hatoka.eos.devices.capi.units.Percentage; +import de.hatoka.eos.units.capi.Energy; +import de.hatoka.eos.units.capi.Money; +import de.hatoka.eos.units.capi.Percentage; import de.hatoka.eos.optimization.internal.business.config.OptimizationConfigurationLoader; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; diff --git a/optimization/src/test/java/de/hatoka/eos/optimization/internal/business/OptimizerTest.java b/optimization/src/test/java/de/hatoka/eos/optimization/internal/business/OptimizerTest.java index c90e508..385ee0b 100644 --- a/optimization/src/test/java/de/hatoka/eos/optimization/internal/business/OptimizerTest.java +++ b/optimization/src/test/java/de/hatoka/eos/optimization/internal/business/OptimizerTest.java @@ -1,7 +1,7 @@ package de.hatoka.eos.optimization.internal.business; import de.hatoka.eos.devices.capi.business.config.InstallationConfig; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Money; import de.hatoka.eos.devices.internal.business.config.ConfigurationLoader; import de.hatoka.eos.optimization.capi.business.OptimizationResult; import de.hatoka.eos.optimization.capi.business.Optimizer; diff --git a/optimization/src/test/java/de/hatoka/eos/optimization/internal/business/config/OptimizationConfigurationLoaderTest.java b/optimization/src/test/java/de/hatoka/eos/optimization/internal/business/config/OptimizationConfigurationLoaderTest.java index 7d4558e..0909f01 100644 --- a/optimization/src/test/java/de/hatoka/eos/optimization/internal/business/config/OptimizationConfigurationLoaderTest.java +++ b/optimization/src/test/java/de/hatoka/eos/optimization/internal/business/config/OptimizationConfigurationLoaderTest.java @@ -1,6 +1,6 @@ package de.hatoka.eos.optimization.internal.business.config; -import de.hatoka.eos.devices.capi.units.Money; +import de.hatoka.eos.units.capi.Money; import de.hatoka.eos.optimization.capi.goals.OptimizationGoals; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; diff --git a/persistence-api/build.gradle.kts b/persistence-api/build.gradle.kts new file mode 100644 index 0000000..11f5d37 --- /dev/null +++ b/persistence-api/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + java +} + +dependencies { + implementation(project(":units")) + + implementation("com.fasterxml.jackson.core:jackson-annotations") + implementation("com.fasterxml.jackson.core:jackson-databind") + + testImplementation("org.junit.jupiter:junit-jupiter") +} diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/persistence/Dao.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/Dao.java similarity index 91% rename from devices/src/main/java/de/hatoka/eos/devices/capi/persistence/Dao.java rename to persistence-api/src/main/java/de/hatoka/eos/persistence/capi/Dao.java index 70630e7..ddc76c4 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/persistence/Dao.java +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/Dao.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.persistence; +package de.hatoka.eos.persistence.capi; /** * Generic access methods for storage diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForcastDAO.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForcastDAO.java new file mode 100644 index 0000000..753f9ec --- /dev/null +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForcastDAO.java @@ -0,0 +1,7 @@ +package de.hatoka.eos.persistence.capi; + +import java.time.ZonedDateTime; + +public interface WeatherForcastDAO extends Dao +{ +} diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastPO.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastPO.java new file mode 100644 index 0000000..1027644 --- /dev/null +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastPO.java @@ -0,0 +1,19 @@ +package de.hatoka.eos.persistence.capi; + +import de.hatoka.eos.units.capi.Percentage; + +public class WeatherForecastPO +{ + public static final String COLUMN_SUN_PROBABILITY = "sun_probability"; + private Percentage sunProbability; + + public Percentage getSunProbability() + { + return sunProbability; + } + + public void setSunProbability(Percentage sunProbability) + { + this.sunProbability = sunProbability; + } +} diff --git a/persistence-influx/build.gradle.kts b/persistence-influx/build.gradle.kts new file mode 100644 index 0000000..5b6a9cb --- /dev/null +++ b/persistence-influx/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + `java-library` + id("io.quarkus") +} + +dependencies { + implementation(project(":units")) + implementation(project(":persistence-api")) + + implementation("jakarta.inject:jakarta.inject-api") + implementation("io.quarkus:quarkus-arc") + implementation("io.quarkus:quarkus-config-yaml") + + implementation("com.influxdb:influxdb-client-java") + + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("io.quarkus:quarkus-junit5") +} diff --git a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxDBConfig.java b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxDBConfig.java new file mode 100644 index 0000000..67e4e77 --- /dev/null +++ b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxDBConfig.java @@ -0,0 +1,33 @@ +package de.hatoka.eos.persistence.influx; + +import com.influxdb.client.InfluxDBClient; +import com.influxdb.client.InfluxDBClientFactory; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Singleton; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * Configuration for InfluxDB client. + */ +@Singleton +public class InfluxDBConfig +{ + @ConfigProperty(name = "eos.influxdb.url", defaultValue = "http://localhost:8086") + String influxdbUrl; + + @ConfigProperty(name = "eos.influxdb.token", defaultValue = "") + String influxdbToken; + + @ConfigProperty(name = "eos.influxdb.org", defaultValue = "eos") + String influxdbOrg; + + @ConfigProperty(name = "eos.influxdb.bucket", defaultValue = "forecast") + String influxdbBucket; + + @Produces + @Singleton + public InfluxDBClient influxDBClient() + { + return InfluxDBClientFactory.create(influxdbUrl, influxdbToken.toCharArray(), influxdbOrg, influxdbBucket); + } +} diff --git a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java new file mode 100644 index 0000000..833bb5a --- /dev/null +++ b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java @@ -0,0 +1,114 @@ +package de.hatoka.eos.persistence.influx; + +import com.influxdb.client.DeleteApi; +import com.influxdb.client.InfluxDBClient; +import com.influxdb.client.QueryApi; +import com.influxdb.client.WriteApiBlocking; +import com.influxdb.client.domain.WritePrecision; +import com.influxdb.client.write.Point; +import com.influxdb.query.FluxTable; +import de.hatoka.eos.persistence.capi.WeatherForcastDAO; +import de.hatoka.eos.persistence.capi.WeatherForecastPO; +import de.hatoka.eos.units.capi.Percentage; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +/** + * InfluxDB implementation for writing forecast data. + */ +@Singleton +public class InfluxWeatherForecastDao implements WeatherForcastDAO +{ + private static final String WEATHER_MEASUREMENT = "weather_forecast"; + public static final String FILTER_INSTANT = """ + from(bucket: "%s") + |> range(start: %s, stop: %s) + |> filter(fn: (r) => r["_measurement"] == "%s") + |> filter(fn: (r) => r["_field"] == "%s") + |> last() + """; + + private final WriteApiBlocking writeApi; + private final DeleteApi deleteApi; + private final QueryApi queryApi; + private final String bucketName; + private final String influxdbOrg; + + @Inject + InfluxWeatherForecastDao(InfluxDBClient influxDBClient, InfluxDBConfig config) + { + writeApi = influxDBClient.getWriteApiBlocking(); + deleteApi = influxDBClient.getDeleteApi(); + queryApi = influxDBClient.getQueryApi(); + bucketName = config.influxdbBucket; + influxdbOrg = config.influxdbOrg; + } + + @Override + public void update(ZonedDateTime time, WeatherForecastPO data) + { + try + { + Point point = Point.measurement(WEATHER_MEASUREMENT) + .time(time.toInstant(), WritePrecision.S) + .addField(WeatherForecastPO.COLUMN_SUN_PROBABILITY, data.getSunProbability().value()); + + writeApi.writePoint(point); + } + catch(Exception e) + { + throw new RuntimeException("Can't write weather forecast to influx", e); + } + } + + @Override + public void delete(ZonedDateTime zonedDateTime) + { + try + { + // Delete data within a 1-minute window around the specified time + deleteApi.delete( + zonedDateTime.minusMinutes(1).toOffsetDateTime(), + zonedDateTime.plusMinutes(1).toOffsetDateTime(), + "_measurement=\"" + WEATHER_MEASUREMENT + "\"", + bucketName, + influxdbOrg + ); + } + catch (Exception e) + { + throw new RuntimeException("Can't delete weather forecast from influx", e); + } + } + + private String formatTimeForFlux(ZonedDateTime time) + { + return time.format(DateTimeFormatter.ISO_INSTANT); + } + + @Override + public WeatherForecastPO get(ZonedDateTime zonedDateTime) + { + String flux = String.format(FILTER_INSTANT, + bucketName, + formatTimeForFlux(zonedDateTime.minusMinutes(1)), + formatTimeForFlux(zonedDateTime.plusMinutes(1)), + WEATHER_MEASUREMENT, WeatherForecastPO.COLUMN_SUN_PROBABILITY); + List tables = queryApi.query(flux); + if (!tables.isEmpty() && !tables.getFirst().getRecords().isEmpty()) + { + WeatherForecastPO data = new WeatherForecastPO(); + Object value = tables.getFirst().getRecords().getFirst().getValueByKey("_value"); + if (value instanceof Number aNumber) + { + data.setSunProbability(new Percentage(aNumber.doubleValue())); + } + return data; + } + return null; + } +} diff --git a/persistence-influx/src/main/resources/META-INF/beans.xml b/persistence-influx/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..2d69bfb --- /dev/null +++ b/persistence-influx/src/main/resources/META-INF/beans.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java b/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java new file mode 100644 index 0000000..e672feb --- /dev/null +++ b/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java @@ -0,0 +1,245 @@ +package de.hatoka.eos.persistence.influx; + +import de.hatoka.eos.persistence.capi.WeatherForecastPO; +import de.hatoka.eos.units.capi.Percentage; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.time.ZonedDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration test for InfluxWeatherForecastDao. + * + * This test requires InfluxDB to be running (e.g., via docker-compose). + * The test uses a dedicated test bucket to avoid interfering with production data. + */ +@QuarkusTest +class InfluxWeatherForecastDaoTest +{ + @Inject + InfluxWeatherForecastDao dao; + + // Test data constants + private static final ZonedDateTime TEST_TIME = ZonedDateTime.of(2024, 1, 15, 12, 0, 0, 0, ZoneId.of("UTC")); + private static final Percentage TEST_SUN_PROBABILITY = new Percentage(0.75); // 75% + private static final Percentage UPDATED_SUN_PROBABILITY = new Percentage(0.85); // 85% + + @AfterEach + void tearDown() + { + dao.delete(TEST_TIME); + dao.delete(TEST_TIME.plusHours(1)); + dao.delete(TEST_TIME.minusHours(1)); + } + + @Test + void shouldStoreAndRetrieveWeatherForecastData() + { + // Given + WeatherForecastPO forecast = new WeatherForecastPO(); + forecast.setSunProbability(TEST_SUN_PROBABILITY); + + // When + dao.update(TEST_TIME, forecast); + + // Then + WeatherForecastPO retrievedForecast = dao.get(TEST_TIME); + + assertNotNull(retrievedForecast, "Retrieved forecast should not be null"); + assertNotNull(retrievedForecast.getSunProbability(), "Sun probability should not be null"); + assertEquals(TEST_SUN_PROBABILITY.value(), retrievedForecast.getSunProbability().value(), 0.001, + "Sun probability should match the stored value"); + } + + @Test + void shouldUpdateExistingWeatherForecastData() + { + // Given - store initial data + WeatherForecastPO initialForecast = new WeatherForecastPO(); + initialForecast.setSunProbability(TEST_SUN_PROBABILITY); + dao.update(TEST_TIME, initialForecast); + + // When - update with new data + WeatherForecastPO updatedForecast = new WeatherForecastPO(); + updatedForecast.setSunProbability(UPDATED_SUN_PROBABILITY); + dao.update(TEST_TIME, updatedForecast); + + // Then + WeatherForecastPO retrievedForecast = dao.get(TEST_TIME); + + assertNotNull(retrievedForecast, "Retrieved forecast should not be null"); + assertNotNull(retrievedForecast.getSunProbability(), "Sun probability should not be null"); + assertEquals(UPDATED_SUN_PROBABILITY.value(), retrievedForecast.getSunProbability().value(), 0.001, + "Sun probability should reflect the updated value"); + } + + @Test + void shouldReturnEmptyForecastWhenNoDataExists() + { + // Given - a time point with no data + ZonedDateTime nonExistentTime = TEST_TIME.plusDays(365); + + // When + + // Then + assertNull(dao.get(nonExistentTime), "Retrieved forecast should not be null"); + } + + @Test + void shouldHandleMultipleTimePoints() + { + // Given - multiple forecasts at different times + ZonedDateTime time1 = TEST_TIME; + ZonedDateTime time2 = TEST_TIME.plusHours(1); + ZonedDateTime time3 = TEST_TIME.plusHours(2); + + WeatherForecastPO forecast1 = new WeatherForecastPO(); + forecast1.setSunProbability(new Percentage(0.5)); + + WeatherForecastPO forecast2 = new WeatherForecastPO(); + forecast2.setSunProbability(new Percentage(0.7)); + + WeatherForecastPO forecast3 = new WeatherForecastPO(); + forecast3.setSunProbability(new Percentage(0.9)); + + // When + dao.update(time1, forecast1); + dao.update(time2, forecast2); + dao.update(time3, forecast3); + + // Then + WeatherForecastPO retrieved1 = dao.get(time1); + WeatherForecastPO retrieved2 = dao.get(time2); + WeatherForecastPO retrieved3 = dao.get(time3); + + assertEquals(0.5, retrieved1.getSunProbability().value(), 0.001); + assertEquals(0.7, retrieved2.getSunProbability().value(), 0.001); + assertEquals(0.9, retrieved3.getSunProbability().value(), 0.001); + + // Clean up additional test data + dao.delete(time2); + dao.delete(time3); + } + + @Test + void shouldHandlePreciseTimeRetrieval() + { + // Given - forecast with precise timestamp + ZonedDateTime preciseTime = TEST_TIME.truncatedTo(ChronoUnit.SECONDS); + WeatherForecastPO forecast = new WeatherForecastPO(); + forecast.setSunProbability(TEST_SUN_PROBABILITY); + + // When + dao.update(preciseTime, forecast); + + // Then - should retrieve data even with slight time variations + WeatherForecastPO retrieved = dao.get(preciseTime); + + assertNotNull(retrieved.getSunProbability()); + assertEquals(TEST_SUN_PROBABILITY.value(), retrieved.getSunProbability().value(), 0.001); + } + + @Test + void shouldHandleBoundaryPercentageValues() + { + // Test with 0% sun probability + WeatherForecastPO forecast0 = new WeatherForecastPO(); + forecast0.setSunProbability(Percentage.ZERO); + dao.update(TEST_TIME, forecast0); + + // Test with 100% sun probability + ZonedDateTime time100 = TEST_TIME.plusMinutes(5); + WeatherForecastPO forecast100 = new WeatherForecastPO(); + forecast100.setSunProbability(Percentage.ONE_HUNDRED); + dao.update(time100, forecast100); + + // Verify boundary values + WeatherForecastPO retrieved0 = dao.get(TEST_TIME); + WeatherForecastPO retrieved100 = dao.get(time100); + + assertEquals(0.0, retrieved0.getSunProbability().value(), 0.001); + assertEquals(1.0, retrieved100.getSunProbability().value(), 0.001); + + // Clean up + dao.delete(time100); + } + + @Test + void shouldDeleteWeatherForecastData() + { + // Given - store some data first + WeatherForecastPO forecast = new WeatherForecastPO(); + forecast.setSunProbability(TEST_SUN_PROBABILITY); + dao.update(TEST_TIME, forecast); + + // Verify data exists + WeatherForecastPO retrievedBeforeDelete = dao.get(TEST_TIME); + assertNotNull(retrievedBeforeDelete.getSunProbability(), + "Data should exist before deletion"); + + // When - delete the data + dao.delete(TEST_TIME); + + // Then - data should no longer exist + assertNull(dao.get(TEST_TIME),"Data should not exist after deletion"); + } + + @Test + void shouldDeleteOnlySpecificTimeData() + { + // Given - store data at multiple time points + ZonedDateTime time1 = TEST_TIME; + ZonedDateTime time2 = TEST_TIME.plusHours(1); + ZonedDateTime time3 = TEST_TIME.plusHours(2); + + WeatherForecastPO forecast1 = new WeatherForecastPO(); + forecast1.setSunProbability(new Percentage(0.3)); + + WeatherForecastPO forecast2 = new WeatherForecastPO(); + forecast2.setSunProbability(new Percentage(0.6)); + + WeatherForecastPO forecast3 = new WeatherForecastPO(); + forecast3.setSunProbability(new Percentage(0.9)); + + dao.update(time1, forecast1); + dao.update(time2, forecast2); + dao.update(time3, forecast3); + + // When - delete only the middle data point + dao.delete(time2); + + // Then - only the deleted data should be gone + WeatherForecastPO retrieved1 = dao.get(time1); + WeatherForecastPO retrieved2 = dao.get(time2); + WeatherForecastPO retrieved3 = dao.get(time3); + + assertNotNull(retrieved1.getSunProbability(), "First data point should still exist"); + assertNull(retrieved2, "Second data point should be deleted"); + assertNotNull(retrieved3.getSunProbability(), "Third data point should still exist"); + + assertEquals(0.3, retrieved1.getSunProbability().value(), 0.001); + assertEquals(0.9, retrieved3.getSunProbability().value(), 0.001); + + // Clean up remaining data + dao.delete(time1); + dao.delete(time3); + } + + @Test + void shouldHandleDeleteOfNonExistentData() + { + // Given - a time point with no data + ZonedDateTime nonExistentTime = TEST_TIME.plusDays(100); + + // When/Then - deleting non-existent data should not throw an exception + assertDoesNotThrow(() -> dao.delete(nonExistentTime), + "Deleting non-existent data should not throw an exception"); + } +} + diff --git a/persistence-influx/src/test/resources/application.yml b/persistence-influx/src/test/resources/application.yml new file mode 100644 index 0000000..4c5fdb1 --- /dev/null +++ b/persistence-influx/src/test/resources/application.yml @@ -0,0 +1,8 @@ +# Test configuration for InfluxDB using environment variables +eos: + influxdb: + url: ${INFLUX_URI} + token: ${INFLUX_TOKEN} + org: eos + bucket: energy_simulation + diff --git a/settings.gradle b/settings.gradle index ddeaea8..f07b756 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,6 +13,10 @@ rootProject.name = 'eos-java' include 'versions' include 'versions-test' +include 'units' +include 'persistence-api' +include 'persistence-influx' include 'devices' include 'optimization' +include 'forecast' // include 'service-eos' diff --git a/units/build.gradle.kts b/units/build.gradle.kts new file mode 100644 index 0000000..dc91638 --- /dev/null +++ b/units/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + java +} + +dependencies { + implementation("com.fasterxml.jackson.core:jackson-annotations") + implementation("com.fasterxml.jackson.core:jackson-databind") + + testImplementation("org.junit.jupiter:junit-jupiter") +} diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/units/Energy.java b/units/src/main/java/de/hatoka/eos/units/capi/Energy.java similarity index 97% rename from devices/src/main/java/de/hatoka/eos/devices/capi/units/Energy.java rename to units/src/main/java/de/hatoka/eos/units/capi/Energy.java index 6759d9a..3321191 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/units/Energy.java +++ b/units/src/main/java/de/hatoka/eos/units/capi/Energy.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.units; +package de.hatoka.eos.units.capi; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/units/EnergyJsonConverter.java b/units/src/main/java/de/hatoka/eos/units/capi/EnergyJsonConverter.java similarity index 95% rename from devices/src/main/java/de/hatoka/eos/devices/capi/units/EnergyJsonConverter.java rename to units/src/main/java/de/hatoka/eos/units/capi/EnergyJsonConverter.java index e5fc75e..e114037 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/units/EnergyJsonConverter.java +++ b/units/src/main/java/de/hatoka/eos/units/capi/EnergyJsonConverter.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.units; +package de.hatoka.eos.units.capi; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -34,12 +34,12 @@ public static class Deserializer extends JsonDeserializer public Energy deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { JsonNode node = p.getCodec().readTree(p); - + double amount = node.get("amount").asDouble(); String unitStr = node.get("unit").asText(); EnergyUnits unit = EnergyUnits.valueOf(unitStr); - + return new Energy(amount, unit); } } -} \ No newline at end of file +} diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/units/EnergyUnits.java b/units/src/main/java/de/hatoka/eos/units/capi/EnergyUnits.java similarity index 83% rename from devices/src/main/java/de/hatoka/eos/devices/capi/units/EnergyUnits.java rename to units/src/main/java/de/hatoka/eos/units/capi/EnergyUnits.java index 0b38676..7f554af 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/units/EnergyUnits.java +++ b/units/src/main/java/de/hatoka/eos/units/capi/EnergyUnits.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.units; +package de.hatoka.eos.units.capi; public enum EnergyUnits { diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/units/LocalDateTimeJsonConverter.java b/units/src/main/java/de/hatoka/eos/units/capi/LocalDateTimeJsonConverter.java similarity index 88% rename from devices/src/main/java/de/hatoka/eos/devices/capi/units/LocalDateTimeJsonConverter.java rename to units/src/main/java/de/hatoka/eos/units/capi/LocalDateTimeJsonConverter.java index 745ddb1..6f0ff34 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/units/LocalDateTimeJsonConverter.java +++ b/units/src/main/java/de/hatoka/eos/units/capi/LocalDateTimeJsonConverter.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.units; +package de.hatoka.eos.units.capi; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -9,12 +9,11 @@ import java.io.IOException; import java.time.LocalDateTime; -import java.time.LocalTime; import java.time.format.DateTimeFormatter; /** * Combined JSON serializer and deserializer for LocalDateTime objects. - * Handles serialization/deserialization of LocalTime as a string in yyyy/MM/dd-HH:mm format. + * Handles serialization/deserialization of LocalDateTime as a string in yyyy/MM/dd-HH:mm format. */ public class LocalDateTimeJsonConverter { @@ -38,4 +37,4 @@ public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) thro return LocalDateTime.parse(timeStr, FORMATTER); } } -} \ No newline at end of file +} diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/units/LocalTimeJsonConverter.java b/units/src/main/java/de/hatoka/eos/units/capi/LocalTimeJsonConverter.java similarity index 96% rename from devices/src/main/java/de/hatoka/eos/devices/capi/units/LocalTimeJsonConverter.java rename to units/src/main/java/de/hatoka/eos/units/capi/LocalTimeJsonConverter.java index 75367f1..3add86f 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/units/LocalTimeJsonConverter.java +++ b/units/src/main/java/de/hatoka/eos/units/capi/LocalTimeJsonConverter.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.units; +package de.hatoka.eos.units.capi; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -37,4 +37,4 @@ public LocalTime deserialize(JsonParser p, DeserializationContext ctxt) throws I return LocalTime.parse(timeStr, FORMATTER); } } -} \ No newline at end of file +} diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/units/Money.java b/units/src/main/java/de/hatoka/eos/units/capi/Money.java similarity index 98% rename from devices/src/main/java/de/hatoka/eos/devices/capi/units/Money.java rename to units/src/main/java/de/hatoka/eos/units/capi/Money.java index c614b6e..b0e5c72 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/units/Money.java +++ b/units/src/main/java/de/hatoka/eos/units/capi/Money.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.units; +package de.hatoka.eos.units.capi; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/units/MoneyJsonConverter.java b/units/src/main/java/de/hatoka/eos/units/capi/MoneyJsonConverter.java similarity index 84% rename from devices/src/main/java/de/hatoka/eos/devices/capi/units/MoneyJsonConverter.java rename to units/src/main/java/de/hatoka/eos/units/capi/MoneyJsonConverter.java index b343c0f..bab86e3 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/units/MoneyJsonConverter.java +++ b/units/src/main/java/de/hatoka/eos/units/capi/MoneyJsonConverter.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.units; +package de.hatoka.eos.units.capi; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -9,8 +9,8 @@ import java.math.MathContext; /** - * Combined JSON serializer and deserializer for Power objects. - * Handles serialization/deserialization of Power as an object with "amount" and "unit" fields. + * Combined JSON serializer and deserializer for Money objects. + * Handles serialization/deserialization of Money as an object with "amount" and "currency" fields. */ public class MoneyJsonConverter { @@ -32,10 +32,10 @@ public static class Deserializer extends JsonDeserializer public Money deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { JsonNode node = p.getCodec().readTree(p); - + double amount = node.get("amount").asDouble(); String currencyMnemonic = node.get("currency").asText(); return new Money(new BigDecimal(amount, MathContext.DECIMAL32), currencyMnemonic); } } -} \ No newline at end of file +} diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/units/Percentage.java b/units/src/main/java/de/hatoka/eos/units/capi/Percentage.java similarity index 97% rename from devices/src/main/java/de/hatoka/eos/devices/capi/units/Percentage.java rename to units/src/main/java/de/hatoka/eos/units/capi/Percentage.java index 0c57a5b..8200071 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/units/Percentage.java +++ b/units/src/main/java/de/hatoka/eos/units/capi/Percentage.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.units; +package de.hatoka.eos.units.capi; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/units/PercentageJsonConverter.java b/units/src/main/java/de/hatoka/eos/units/capi/PercentageJsonConverter.java similarity index 96% rename from devices/src/main/java/de/hatoka/eos/devices/capi/units/PercentageJsonConverter.java rename to units/src/main/java/de/hatoka/eos/units/capi/PercentageJsonConverter.java index f01fa01..2f303a2 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/units/PercentageJsonConverter.java +++ b/units/src/main/java/de/hatoka/eos/units/capi/PercentageJsonConverter.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.units; +package de.hatoka.eos.units.capi; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -33,4 +33,4 @@ public Percentage deserialize(JsonParser p, DeserializationContext ctxt) throws return new Percentage(value); } } -} \ No newline at end of file +} diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/units/Power.java b/units/src/main/java/de/hatoka/eos/units/capi/Power.java similarity index 95% rename from devices/src/main/java/de/hatoka/eos/devices/capi/units/Power.java rename to units/src/main/java/de/hatoka/eos/units/capi/Power.java index f3e7142..1358216 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/units/Power.java +++ b/units/src/main/java/de/hatoka/eos/units/capi/Power.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.units; +package de.hatoka.eos.units.capi; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @@ -28,5 +28,4 @@ public Power multiply(Percentage percentage) { return new Power(amount() * percentage.toFraction(), unit); } - } diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/units/PowerJsonConverter.java b/units/src/main/java/de/hatoka/eos/units/capi/PowerJsonConverter.java similarity index 95% rename from devices/src/main/java/de/hatoka/eos/devices/capi/units/PowerJsonConverter.java rename to units/src/main/java/de/hatoka/eos/units/capi/PowerJsonConverter.java index 99c2503..5e9b50f 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/units/PowerJsonConverter.java +++ b/units/src/main/java/de/hatoka/eos/units/capi/PowerJsonConverter.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.units; +package de.hatoka.eos.units.capi; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -34,12 +34,12 @@ public static class Deserializer extends JsonDeserializer public Power deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { JsonNode node = p.getCodec().readTree(p); - + double amount = node.get("amount").asDouble(); String unitStr = node.get("unit").asText(); PowerUnits unit = PowerUnits.valueOf(unitStr); - + return new Power(amount, unit); } } -} \ No newline at end of file +} diff --git a/units/src/main/java/de/hatoka/eos/units/capi/PowerUnits.java b/units/src/main/java/de/hatoka/eos/units/capi/PowerUnits.java new file mode 100644 index 0000000..7257fbe --- /dev/null +++ b/units/src/main/java/de/hatoka/eos/units/capi/PowerUnits.java @@ -0,0 +1,6 @@ +package de.hatoka.eos.units.capi; + +public enum PowerUnits +{ + K_W; +} diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/units/TimeZoneJsonConverter.java b/units/src/main/java/de/hatoka/eos/units/capi/TimeZoneJsonConverter.java similarity index 83% rename from devices/src/main/java/de/hatoka/eos/devices/capi/units/TimeZoneJsonConverter.java rename to units/src/main/java/de/hatoka/eos/units/capi/TimeZoneJsonConverter.java index 89c0cc7..79ca9a5 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/units/TimeZoneJsonConverter.java +++ b/units/src/main/java/de/hatoka/eos/units/capi/TimeZoneJsonConverter.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.units; +package de.hatoka.eos.units.capi; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -11,8 +11,8 @@ import java.time.ZoneId; /** - * Combined JSON serializer and deserializer for LocalTime objects. - * Handles serialization/deserialization of LocalTime as a string in HH:mm format. + * Combined JSON serializer and deserializer for ZoneId objects. + * Handles serialization/deserialization of ZoneId as a string. */ public class TimeZoneJsonConverter { @@ -34,4 +34,4 @@ public ZoneId deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx return ZoneId.of(timeZoneStr); } } -} \ No newline at end of file +} From 748b9ff87ff445f288612bca5b02c6bcd377c4e7 Mon Sep 17 00:00:00 2001 From: Thomas Bergmann Date: Sun, 2 Nov 2025 14:36:11 +0100 Subject: [PATCH 02/16] feat: MeteoMediaWeatherForecastImporter --- .../src/test/resources/application.properties | 9 +- forecast/build.gradle.kts | 10 +- .../eos/forecast/MeteoMediaStation.java | 18 +++ .../MeteoMediaWeatherForecastImporter.java | 143 ++++++++++++++++++ ...MediaWeatherSunshineDurationConverter.java | 19 ++- .../capi/WeatherForecastImporter.java | 8 - .../src/main/resources/META-INF/beans.xml | 1 + .../src/main/resources/application.properties | 16 +- ...MeteoMediaWeatherForecastImporterTest.java | 39 +++++ ...aWeatherSunshineDurationConverterTest.java | 48 +++--- forecast/src/test/resources/application.yml | 8 + 11 files changed, 276 insertions(+), 43 deletions(-) create mode 100644 forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaStation.java create mode 100644 forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java rename devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/WeatherSunshineDurationConverter.java => forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherSunshineDurationConverter.java (95%) delete mode 100644 forecast/src/main/java/de/hatoka/eos/forecast/capi/WeatherForecastImporter.java create mode 100644 forecast/src/main/resources/META-INF/beans.xml create mode 100644 forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java rename devices/src/test/java/de/hatoka/eos/devices/internal/business/forecast/WeatherSunshineDurationConverterTest.java => forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherSunshineDurationConverterTest.java (75%) create mode 100644 forecast/src/test/resources/application.yml diff --git a/devices/src/test/resources/application.properties b/devices/src/test/resources/application.properties index 98ce92a..c260bcc 100644 --- a/devices/src/test/resources/application.properties +++ b/devices/src/test/resources/application.properties @@ -1,9 +1,12 @@ -quarkus.log.level=INFO -quarkus.log.category."com.intershop".level=DEBUG +quarkus.profile=production -# InfluxDB Metrics Configuration for tests +# InfluxDB Metrics Configuration eos.metrics.influxdb.enabled=true eos.metrics.influxdb.url=http://localhost:8086 eos.metrics.influxdb.token=eos-token-12345678901234567890 eos.metrics.influxdb.org=eos eos.metrics.influxdb.bucket=energy_simulation + +# Quarkus Configuration +quarkus.log.level=INFO +quarkus.log.category."de.hatoka.eos".level=DEBUG diff --git a/forecast/build.gradle.kts b/forecast/build.gradle.kts index d5f436e..50e4b8f 100644 --- a/forecast/build.gradle.kts +++ b/forecast/build.gradle.kts @@ -1,14 +1,22 @@ plugins { - java + `java-library` + jacoco id("io.quarkus") } dependencies { implementation(project(":units")) + implementation(project(":persistence-api")) + implementation(project(":persistence-influx")) implementation("com.fasterxml.jackson.core:jackson-annotations") implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("io.quarkus:quarkus-arc") + implementation("org.apache.commons:commons-csv") implementation("org.slf4j:slf4j-api") + implementation("jakarta.inject:jakarta.inject-api") testImplementation("io.quarkus:quarkus-junit5") + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaStation.java b/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaStation.java new file mode 100644 index 0000000..fef834a --- /dev/null +++ b/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaStation.java @@ -0,0 +1,18 @@ +package de.hatoka.eos.forecast; + +public enum MeteoMediaStation +{ + APOLDA("095550"); + + private final String stationNumber; + + MeteoMediaStation(String stationNumber) + { + this.stationNumber = stationNumber; + } + + public String getStationNumber() + { + return stationNumber; + } +} diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java new file mode 100644 index 0000000..8d76e04 --- /dev/null +++ b/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java @@ -0,0 +1,143 @@ +package de.hatoka.eos.forecast; + +import de.hatoka.eos.persistence.capi.WeatherForcastDAO; +import de.hatoka.eos.persistence.capi.WeatherForecastPO; +import de.hatoka.eos.units.capi.Percentage; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.Duration; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Map; + +/** + * Downloads weather forecast PNG images from external services and processes them to extract sunshine duration data, which is then stored in the + * database. + */ +@Singleton +public class MeteoMediaWeatherForecastImporter +{ + private static final Logger logger = LoggerFactory.getLogger(MeteoMediaWeatherForecastImporter.class); + private static final String URI_FORMAT = "https://wetterstationen.meteomedia.de/messnetz/vorhersagegrafik/%s.png"; + + @Inject + MeteoMediaWeatherSunshineDurationConverter converter; + @Inject + WeatherForcastDAO weatherDao; + + public ZonedDateTime importWeatherForecast(MeteoMediaStation station) throws IOException, InterruptedException + { + ZonedDateTime todayStartOfDayUTC = ZonedDateTime.now().toLocalDate().atStartOfDay(ZoneId.of("UTC")); + importWeatherForecast(URI.create(URI_FORMAT.formatted(station.getStationNumber())), todayStartOfDayUTC); + return todayStartOfDayUTC; + } + + /** + * Downloads and processes a weather forecast PNG from the specified URL. + */ + private void importWeatherForecast(URI imageUrl, ZonedDateTime startDate) throws IOException, InterruptedException + { + logger.info("Starting weather forecast import from URL: {}", imageUrl); + + // Download the PNG image + byte[] imageData = downloadImage(imageUrl); + logger.debug("Downloaded {} bytes of image data", imageData.length); + + // Save to temporary file for processing + Path tempFile = saveTempImage(imageData, startDate); + + try + { + // Process the image and extract sunshine data + Map sunshineDurationPerHour = converter.extractSunshineDuration(converter.loadImageFromFile(tempFile.toFile()), + startDate); + + logger.info("Extracted sunshine data for {} hours", sunshineDurationPerHour.size()); + + // Store the data in the database + storeSunshineData(sunshineDurationPerHour); + + logger.info("Successfully imported weather forecast data for {}", startDate); + } + finally + { + // Clean up temporary file + Files.deleteIfExists(tempFile); + } + } + + private byte[] downloadImage(URI imageUrl) throws IOException, InterruptedException + { + try (HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .followRedirects(HttpClient.Redirect.NORMAL) + .build()) + { + HttpRequest request = HttpRequest.newBuilder() + .uri(imageUrl) + .timeout(Duration.ofSeconds(30)) + .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36") + .header("Accept", "image/png,image/*;q=0.9,*/*;q=0.8") + .GET() + .build(); + + logger.debug("Sending HTTP request to: {}", imageUrl); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); + + if (response.statusCode() != 200) + { + throw new IOException("HTTP request failed with status code: " + response.statusCode()); + } + return response.body(); + } + } + + private Path saveTempImage(byte[] imageData, ZonedDateTime startDate) throws IOException + { + String filename = "weather_forecast_" + startDate.format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmm")) + ".png"; + + Path tempFile = Files.createTempFile("weather_", filename); + Files.write(tempFile, imageData, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + + logger.debug("Saved temporary image file: {}", tempFile); + return tempFile; + } + + private void storeSunshineData(Map sunshineDurationPerHour) + { + for (Map.Entry entry : sunshineDurationPerHour.entrySet()) + { + ZonedDateTime dateTime = entry.getKey(); + Integer sunshineMinutes = entry.getValue(); + + // Convert sunshine minutes (0-60) to probability (0.0-1.0) + double sunProbability = Math.min(1.0, sunshineMinutes / 60.0); + + WeatherForecastPO forecast = new WeatherForecastPO(); + forecast.setSunProbability(new Percentage(sunProbability)); + + try + { + weatherDao.update(dateTime, forecast); + logger.debug("Stored weather data for {}: {}% sun probability", dateTime, Math.round(sunProbability * 100)); + } + catch(Exception e) + { + logger.error("Failed to store weather data for " + dateTime, e); + } + } + } +} diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/WeatherSunshineDurationConverter.java b/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherSunshineDurationConverter.java similarity index 95% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/WeatherSunshineDurationConverter.java rename to forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherSunshineDurationConverter.java index 6e04f2b..73197f8 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/WeatherSunshineDurationConverter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherSunshineDurationConverter.java @@ -1,5 +1,6 @@ -package de.hatoka.eos.devices.internal.business.forecast; +package de.hatoka.eos.forecast; +import jakarta.inject.Singleton; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; import org.slf4j.LoggerFactory; @@ -7,6 +8,7 @@ import javax.imageio.ImageIO; import java.awt.Color; import java.awt.image.BufferedImage; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; @@ -32,7 +34,8 @@ * 6. Sunshine duration per hour (yellow bars) - THIS IS WHAT WE EXTRACT * 7. Precipitation 6h */ -public class WeatherSunshineDurationConverter +@Singleton +public class MeteoMediaWeatherSunshineDurationConverter { private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd"); @@ -102,6 +105,18 @@ BufferedImage loadImage(String resourceName) throws IOException } } + /** + * Loads an image from a file path. + * + * @param file the image file + * @return the loaded BufferedImage + * @throws IOException if the file cannot be read + */ + BufferedImage loadImageFromFile(File file) throws IOException + { + return ImageIO.read(file); + } + /** * Extracts sunshine duration data from the weather forecast image. * The image contains hourly sunshine duration bars in yellow color. diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/capi/WeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/capi/WeatherForecastImporter.java deleted file mode 100644 index a9340dd..0000000 --- a/forecast/src/main/java/de/hatoka/eos/forecast/capi/WeatherForecastImporter.java +++ /dev/null @@ -1,8 +0,0 @@ -package de.hatoka.eos.forecast.capi; - -/** - * WeatherForecastImporter retrieves weather data and stores the data at influx db. - */ -public class WeatherForecastImporter -{ -} diff --git a/forecast/src/main/resources/META-INF/beans.xml b/forecast/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..2d69bfb --- /dev/null +++ b/forecast/src/main/resources/META-INF/beans.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/forecast/src/main/resources/application.properties b/forecast/src/main/resources/application.properties index 77661f7..c260bcc 100644 --- a/forecast/src/main/resources/application.properties +++ b/forecast/src/main/resources/application.properties @@ -1,10 +1,12 @@ -# InfluxDB Configuration -influxdb.url=http://localhost:8086 -influxdb.token=your-influxdb-token -influxdb.org=eos -influxdb.bucket=forecast +quarkus.profile=production + +# InfluxDB Metrics Configuration +eos.metrics.influxdb.enabled=true +eos.metrics.influxdb.url=http://localhost:8086 +eos.metrics.influxdb.token=eos-token-12345678901234567890 +eos.metrics.influxdb.org=eos +eos.metrics.influxdb.bucket=energy_simulation # Quarkus Configuration -quarkus.arc.remove-unused-beans=false quarkus.log.level=INFO -quarkus.log.category."de.hatoka.eos.forecast".level=DEBUG +quarkus.log.category."de.hatoka.eos".level=DEBUG diff --git a/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java new file mode 100644 index 0000000..df05a36 --- /dev/null +++ b/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java @@ -0,0 +1,39 @@ +package de.hatoka.eos.forecast; + +import de.hatoka.eos.persistence.capi.WeatherForcastDAO; +import de.hatoka.eos.persistence.capi.WeatherForecastPO; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; +import java.time.ZonedDateTime; +import java.time.ZoneId; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test for WeatherForecastImporter functionality. + * Note: This test requires network access to download actual weather images. + * For CI/CD environments, you might want to mock the HTTP client or use test resources. + */ +@QuarkusTest +class MeteoMediaWeatherForecastImporterTest +{ + @Inject + MeteoMediaWeatherForecastImporter importer; + + @Inject + WeatherForcastDAO weatherDao; + + @Test + void testImportFromValidUrl() throws IOException, InterruptedException + { + // this importer can only import data of today and tomorrow + ZonedDateTime testDate = importer.importWeatherForecast(MeteoMediaStation.APOLDA); + WeatherForecastPO retrieved = weatherDao.get(testDate.plusHours(1)); + assertNotNull(retrieved); + assertNotNull(retrieved.getSunProbability()); + } +} diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/forecast/WeatherSunshineDurationConverterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherSunshineDurationConverterTest.java similarity index 75% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/forecast/WeatherSunshineDurationConverterTest.java rename to forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherSunshineDurationConverterTest.java index 1620e40..949579a 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/forecast/WeatherSunshineDurationConverterTest.java +++ b/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherSunshineDurationConverterTest.java @@ -1,6 +1,7 @@ -package de.hatoka.eos.devices.internal.business.forecast; +package de.hatoka.eos.forecast; import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.awt.*; @@ -12,7 +13,7 @@ import static org.junit.jupiter.api.Assertions.*; @QuarkusTest -class WeatherSunshineDurationConverterTest +class MeteoMediaWeatherSunshineDurationConverterTest { private static final String TEST_IMAGE_CEST = "weather_20251024.png"; private static final String TEST_IMAGE_CET = "weather_20251027.png"; @@ -20,8 +21,8 @@ class WeatherSunshineDurationConverterTest private static final LocalDate START_DATE_CET = LocalDate.of(2025, 10, 27); private static final ZoneId BERLIN = ZoneId.of("Europe/Berlin"); private static final ZoneId UTC = ZoneId.of("UTC"); - private final WeatherSunshineDurationConverter converter = new WeatherSunshineDurationConverter(); - private final WeatherSunshineDurationConverter.SunHourRegion region = WeatherSunshineDurationConverter.WINDOW_DAY1; + private final MeteoMediaWeatherSunshineDurationConverter converter = new MeteoMediaWeatherSunshineDurationConverter(); + private final MeteoMediaWeatherSunshineDurationConverter.SunHourRegion region = MeteoMediaWeatherSunshineDurationConverter.WINDOW_DAY1; @Test void testCalculateHourXPosition() @@ -33,10 +34,10 @@ void testCalculateHourXPosition() int x = converter.calculateHourXPosition(region, position); System.out.printf("Hour(UTC) %2d: X=%d%n", position, x); } - assertEquals(183, converter.calculateHourXPosition(region,0), "marker between 180 und 187"); - assertEquals(190, converter.calculateHourXPosition(region,1)); - assertEquals(231, converter.calculateHourXPosition(region,7)); - assertEquals(237, converter.calculateHourXPosition(region,8)); + Assertions.assertEquals(183, converter.calculateHourXPosition(region,0), "marker between 180 und 187"); + Assertions.assertEquals(190, converter.calculateHourXPosition(region,1)); + Assertions.assertEquals(231, converter.calculateHourXPosition(region,7)); + Assertions.assertEquals(237, converter.calculateHourXPosition(region,8)); } @@ -45,32 +46,35 @@ void testColorAtHourPositions() throws IOException { BufferedImage image = converter.loadImage(TEST_IMAGE_CEST); // position 407 is one pixel above the zero line - assertEquals(WeatherSunshineDurationConverter.GRAY, new Color(image.getRGB(converter.calculateHourXPosition(region,0), 407))); - assertEquals(WeatherSunshineDurationConverter.BLACK, new Color(image.getRGB(converter.calculateHourXPosition(region,7), 407))); - assertEquals(WeatherSunshineDurationConverter.YELLOW, new Color(image.getRGB(converter.calculateHourXPosition(region,8), 407))); + Assertions.assertEquals( + MeteoMediaWeatherSunshineDurationConverter.GRAY, new Color(image.getRGB(converter.calculateHourXPosition(region,0), 407))); + Assertions.assertEquals( + MeteoMediaWeatherSunshineDurationConverter.BLACK, new Color(image.getRGB(converter.calculateHourXPosition(region,7), 407))); + Assertions.assertEquals( + MeteoMediaWeatherSunshineDurationConverter.YELLOW, new Color(image.getRGB(converter.calculateHourXPosition(region,8), 407))); } @Test void testMeasureBarHeights() throws IOException { BufferedImage image = converter.loadImage(TEST_IMAGE_CEST); - assertEquals(0, converter.measureBarHeightToBlackLine(image, converter.calculateHourXPosition(region,0), region)); - assertEquals(1, converter.measureBarHeightToBlackLine(image, converter.calculateHourXPosition(region,7), region)); - assertEquals(15, converter.measureBarHeightToBlackLine(image, converter.calculateHourXPosition(region,8), region)); - assertEquals(5, converter.measureBarHeightToBlackLine(image, converter.calculateHourXPosition(region,11), region)); + Assertions.assertEquals(0, converter.measureBarHeightToBlackLine(image, converter.calculateHourXPosition(region,0), region)); + Assertions.assertEquals(1, converter.measureBarHeightToBlackLine(image, converter.calculateHourXPosition(region,7), region)); + Assertions.assertEquals(15, converter.measureBarHeightToBlackLine(image, converter.calculateHourXPosition(region,8), region)); + Assertions.assertEquals(5, converter.measureBarHeightToBlackLine(image, converter.calculateHourXPosition(region,11), region)); } @Test void testSunMinutes() throws IOException { BufferedImage image = converter.loadImage(TEST_IMAGE_CEST); - assertEquals(0, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,0), region)); - assertEquals(2, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,7), region)); - assertEquals(26, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,8), region)); - assertEquals(26, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,9), region)); - assertEquals(4, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,10), region)); - assertEquals(9, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,11), region)); - assertEquals(11, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,12), region)); + Assertions.assertEquals(0, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,0), region)); + Assertions.assertEquals(2, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,7), region)); + Assertions.assertEquals(26, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,8), region)); + Assertions.assertEquals(26, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,9), region)); + Assertions.assertEquals(4, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,10), region)); + Assertions.assertEquals(9, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,11), region)); + Assertions.assertEquals(11, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,12), region)); } @Test diff --git a/forecast/src/test/resources/application.yml b/forecast/src/test/resources/application.yml new file mode 100644 index 0000000..4c5fdb1 --- /dev/null +++ b/forecast/src/test/resources/application.yml @@ -0,0 +1,8 @@ +# Test configuration for InfluxDB using environment variables +eos: + influxdb: + url: ${INFLUX_URI} + token: ${INFLUX_TOKEN} + org: eos + bucket: energy_simulation + From c802e50021cd71b166a963d1cebe65f0ac175d18 Mon Sep 17 00:00:00 2001 From: Thomas Bergmann Date: Sun, 2 Nov 2025 14:46:15 +0100 Subject: [PATCH 03/16] feat: add second station --- .../MeteoMediaWeatherForecastImporter.java | 22 +++++++------------ ...MeteoMediaWeatherForecastImporterTest.java | 4 ++-- .../persistence/capi}/MeteoMediaStation.java | 5 +++-- .../persistence/capi/WeatherForecastPO.java | 13 +++++++++++ .../influx/InfluxWeatherForecastDao.java | 1 + 5 files changed, 27 insertions(+), 18 deletions(-) rename {forecast/src/main/java/de/hatoka/eos/forecast => persistence-api/src/main/java/de/hatoka/eos/persistence/capi}/MeteoMediaStation.java (72%) diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java index 8d76e04..d7e069d 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java @@ -1,5 +1,6 @@ package de.hatoka.eos.forecast; +import de.hatoka.eos.persistence.capi.MeteoMediaStation; import de.hatoka.eos.persistence.capi.WeatherForcastDAO; import de.hatoka.eos.persistence.capi.WeatherForecastPO; import de.hatoka.eos.units.capi.Percentage; @@ -39,20 +40,11 @@ public class MeteoMediaWeatherForecastImporter public ZonedDateTime importWeatherForecast(MeteoMediaStation station) throws IOException, InterruptedException { - ZonedDateTime todayStartOfDayUTC = ZonedDateTime.now().toLocalDate().atStartOfDay(ZoneId.of("UTC")); - importWeatherForecast(URI.create(URI_FORMAT.formatted(station.getStationNumber())), todayStartOfDayUTC); - return todayStartOfDayUTC; - } - - /** - * Downloads and processes a weather forecast PNG from the specified URL. - */ - private void importWeatherForecast(URI imageUrl, ZonedDateTime startDate) throws IOException, InterruptedException - { - logger.info("Starting weather forecast import from URL: {}", imageUrl); + ZonedDateTime startDate = ZonedDateTime.now().toLocalDate().atStartOfDay(ZoneId.of("UTC")); + logger.info("Starting weather forecast import for station: {}", station.name()); // Download the PNG image - byte[] imageData = downloadImage(imageUrl); + byte[] imageData = downloadImage(URI.create(URI_FORMAT.formatted(station.getStationNumber()))); logger.debug("Downloaded {} bytes of image data", imageData.length); // Save to temporary file for processing @@ -67,7 +59,7 @@ private void importWeatherForecast(URI imageUrl, ZonedDateTime startDate) throws logger.info("Extracted sunshine data for {} hours", sunshineDurationPerHour.size()); // Store the data in the database - storeSunshineData(sunshineDurationPerHour); + storeSunshineData(sunshineDurationPerHour, station); logger.info("Successfully imported weather forecast data for {}", startDate); } @@ -76,6 +68,7 @@ private void importWeatherForecast(URI imageUrl, ZonedDateTime startDate) throws // Clean up temporary file Files.deleteIfExists(tempFile); } + return startDate; } private byte[] downloadImage(URI imageUrl) throws IOException, InterruptedException @@ -116,7 +109,7 @@ private Path saveTempImage(byte[] imageData, ZonedDateTime startDate) throws IOE return tempFile; } - private void storeSunshineData(Map sunshineDurationPerHour) + private void storeSunshineData(Map sunshineDurationPerHour, MeteoMediaStation station) { for (Map.Entry entry : sunshineDurationPerHour.entrySet()) { @@ -127,6 +120,7 @@ private void storeSunshineData(Map sunshineDurationPerHo double sunProbability = Math.min(1.0, sunshineMinutes / 60.0); WeatherForecastPO forecast = new WeatherForecastPO(); + forecast.setStation(station); forecast.setSunProbability(new Percentage(sunProbability)); try diff --git a/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java index df05a36..37bb6ab 100644 --- a/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java +++ b/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java @@ -1,5 +1,6 @@ package de.hatoka.eos.forecast; +import de.hatoka.eos.persistence.capi.MeteoMediaStation; import de.hatoka.eos.persistence.capi.WeatherForcastDAO; import de.hatoka.eos.persistence.capi.WeatherForecastPO; import io.quarkus.test.junit.QuarkusTest; @@ -7,9 +8,7 @@ import org.junit.jupiter.api.Test; import java.io.IOException; -import java.net.URI; import java.time.ZonedDateTime; -import java.time.ZoneId; import static org.junit.jupiter.api.Assertions.*; @@ -35,5 +34,6 @@ void testImportFromValidUrl() throws IOException, InterruptedException WeatherForecastPO retrieved = weatherDao.get(testDate.plusHours(1)); assertNotNull(retrieved); assertNotNull(retrieved.getSunProbability()); + importer.importWeatherForecast(MeteoMediaStation.LEIPZIG_STADTWERKE); } } diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaStation.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/MeteoMediaStation.java similarity index 72% rename from forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaStation.java rename to persistence-api/src/main/java/de/hatoka/eos/persistence/capi/MeteoMediaStation.java index fef834a..d448b95 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaStation.java +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/MeteoMediaStation.java @@ -1,8 +1,9 @@ -package de.hatoka.eos.forecast; +package de.hatoka.eos.persistence.capi; public enum MeteoMediaStation { - APOLDA("095550"); + APOLDA("095550"), + LEIPZIG_STADTWERKE("104700"); private final String stationNumber; diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastPO.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastPO.java index 1027644..f4f671a 100644 --- a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastPO.java +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastPO.java @@ -7,6 +7,9 @@ public class WeatherForecastPO public static final String COLUMN_SUN_PROBABILITY = "sun_probability"; private Percentage sunProbability; + public static final String COLUMN_STATION = "station"; + private MeteoMediaStation station; + public Percentage getSunProbability() { return sunProbability; @@ -16,4 +19,14 @@ public void setSunProbability(Percentage sunProbability) { this.sunProbability = sunProbability; } + + public MeteoMediaStation getStation() + { + return station; + } + + public void setStation(MeteoMediaStation station) + { + this.station = station; + } } diff --git a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java index 833bb5a..75e0cf7 100644 --- a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java +++ b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java @@ -55,6 +55,7 @@ public void update(ZonedDateTime time, WeatherForecastPO data) { Point point = Point.measurement(WEATHER_MEASUREMENT) .time(time.toInstant(), WritePrecision.S) + .addTag(WeatherForecastPO.COLUMN_STATION, data.getStation().name()) .addField(WeatherForecastPO.COLUMN_SUN_PROBABILITY, data.getSunProbability().value()); writeApi.writePoint(point); From f922fbba55ea389d7c0209c6e076d9d148450ab5 Mon Sep 17 00:00:00 2001 From: Thomas Bergmann Date: Mon, 3 Nov 2025 09:58:19 +0100 Subject: [PATCH 04/16] rework: move station to key --- .../forecast/InfluxWeatherForecast.java | 13 +- .../MeteoMediaWeatherForecastImporter.java | 5 +- .../persistence/capi/WeatherForcastDAO.java | 4 +- .../persistence/capi/WeatherForecastKey.java | 11 ++ .../persistence/capi/WeatherForecastPO.java | 13 -- .../influx/InfluxWeatherForecastDao.java | 67 ++++--- .../influx/InfluxWeatherForecastDaoTest.java | 164 +++++++++--------- 7 files changed, 143 insertions(+), 134 deletions(-) create mode 100644 persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastKey.java diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/InfluxWeatherForecast.java b/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/InfluxWeatherForecast.java index 4fdb7db..7ede610 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/InfluxWeatherForecast.java +++ b/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/InfluxWeatherForecast.java @@ -1,6 +1,7 @@ package de.hatoka.eos.devices.internal.business.forecast; import de.hatoka.eos.devices.capi.business.forecast.WeatherForecast; +import de.hatoka.eos.persistence.capi.WeatherForecastKey; import de.hatoka.eos.persistence.influx.InfluxWeatherForecastDao; import de.hatoka.eos.units.capi.Percentage; import jakarta.inject.Inject; @@ -11,15 +12,19 @@ /** * Weather forecast implementation that reads from InfluxDB via ForecastDAO. */ -@Singleton public class InfluxWeatherForecast implements WeatherForecast { - @Inject - InfluxWeatherForecastDao forecastDAO; + private final String station; + private final InfluxWeatherForecastDao forecastDAO; + public InfluxWeatherForecast(String station, InfluxWeatherForecastDao forecastDAO) + { + this.station = station; + this.forecastDAO = forecastDAO; + } @Override public Percentage getSunProbability(ZonedDateTime time) { - return forecastDAO.get(time).getSunProbability(); + return forecastDAO.get(new WeatherForecastKey(station, time)).getSunProbability(); } } diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java index d7e069d..1d7e5ce 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java @@ -2,6 +2,7 @@ import de.hatoka.eos.persistence.capi.MeteoMediaStation; import de.hatoka.eos.persistence.capi.WeatherForcastDAO; +import de.hatoka.eos.persistence.capi.WeatherForecastKey; import de.hatoka.eos.persistence.capi.WeatherForecastPO; import de.hatoka.eos.units.capi.Percentage; import jakarta.inject.Inject; @@ -21,6 +22,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.List; import java.util.Map; /** @@ -120,12 +122,11 @@ private void storeSunshineData(Map sunshineDurationPerHo double sunProbability = Math.min(1.0, sunshineMinutes / 60.0); WeatherForecastPO forecast = new WeatherForecastPO(); - forecast.setStation(station); forecast.setSunProbability(new Percentage(sunProbability)); try { - weatherDao.update(dateTime, forecast); + weatherDao.update(new WeatherForecastKey(station.name(), dateTime), forecast); logger.debug("Stored weather data for {}: {}% sun probability", dateTime, Math.round(sunProbability * 100)); } catch(Exception e) diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForcastDAO.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForcastDAO.java index 753f9ec..202a993 100644 --- a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForcastDAO.java +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForcastDAO.java @@ -1,7 +1,5 @@ package de.hatoka.eos.persistence.capi; -import java.time.ZonedDateTime; - -public interface WeatherForcastDAO extends Dao +public interface WeatherForcastDAO extends Dao { } diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastKey.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastKey.java new file mode 100644 index 0000000..4bfca5f --- /dev/null +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastKey.java @@ -0,0 +1,11 @@ +package de.hatoka.eos.persistence.capi; + +import java.time.ZonedDateTime; + +public record WeatherForecastKey(String station, ZonedDateTime time) +{ + public static WeatherForecastKey valueOf(MeteoMediaStation station, ZonedDateTime time) + { + return new WeatherForecastKey(station.name(), time); + } +} diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastPO.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastPO.java index f4f671a..1027644 100644 --- a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastPO.java +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastPO.java @@ -7,9 +7,6 @@ public class WeatherForecastPO public static final String COLUMN_SUN_PROBABILITY = "sun_probability"; private Percentage sunProbability; - public static final String COLUMN_STATION = "station"; - private MeteoMediaStation station; - public Percentage getSunProbability() { return sunProbability; @@ -19,14 +16,4 @@ public void setSunProbability(Percentage sunProbability) { this.sunProbability = sunProbability; } - - public MeteoMediaStation getStation() - { - return station; - } - - public void setStation(MeteoMediaStation station) - { - this.station = station; - } } diff --git a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java index 75e0cf7..ce12e94 100644 --- a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java +++ b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java @@ -6,8 +6,10 @@ import com.influxdb.client.WriteApiBlocking; import com.influxdb.client.domain.WritePrecision; import com.influxdb.client.write.Point; +import com.influxdb.query.FluxRecord; import com.influxdb.query.FluxTable; import de.hatoka.eos.persistence.capi.WeatherForcastDAO; +import de.hatoka.eos.persistence.capi.WeatherForecastKey; import de.hatoka.eos.persistence.capi.WeatherForecastPO; import de.hatoka.eos.units.capi.Percentage; import jakarta.inject.Inject; @@ -15,7 +17,6 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.List; /** * InfluxDB implementation for writing forecast data. @@ -24,13 +25,18 @@ public class InfluxWeatherForecastDao implements WeatherForcastDAO { private static final String WEATHER_MEASUREMENT = "weather_forecast"; - public static final String FILTER_INSTANT = """ + private static final String COLUMN_STATION = "station"; + public static final String GET_QUERY = """ from(bucket: "%s") |> range(start: %s, stop: %s) |> filter(fn: (r) => r["_measurement"] == "%s") + |> filter(fn: (r) => r["station"] == "%s") |> filter(fn: (r) => r["_field"] == "%s") |> last() """; + public static final String DELETE_PREDICATE = """ + _measurement="%s" AND station="%s" + """; private final WriteApiBlocking writeApi; private final DeleteApi deleteApi; @@ -49,13 +55,13 @@ public class InfluxWeatherForecastDao implements WeatherForcastDAO } @Override - public void update(ZonedDateTime time, WeatherForecastPO data) + public void update(WeatherForecastKey key, WeatherForecastPO data) { try { Point point = Point.measurement(WEATHER_MEASUREMENT) - .time(time.toInstant(), WritePrecision.S) - .addTag(WeatherForecastPO.COLUMN_STATION, data.getStation().name()) + .time(key.time().toInstant(), WritePrecision.S) + .addTag(COLUMN_STATION, key.station()) .addField(WeatherForecastPO.COLUMN_SUN_PROBABILITY, data.getSunProbability().value()); writeApi.writePoint(point); @@ -67,20 +73,15 @@ public void update(ZonedDateTime time, WeatherForecastPO data) } @Override - public void delete(ZonedDateTime zonedDateTime) + public void delete(WeatherForecastKey key) { try { // Delete data within a 1-minute window around the specified time - deleteApi.delete( - zonedDateTime.minusMinutes(1).toOffsetDateTime(), - zonedDateTime.plusMinutes(1).toOffsetDateTime(), - "_measurement=\"" + WEATHER_MEASUREMENT + "\"", - bucketName, - influxdbOrg - ); + deleteApi.delete(key.time().minusMinutes(1).toOffsetDateTime(), key.time().plusMinutes(1).toOffsetDateTime(), + DELETE_PREDICATE.formatted(WEATHER_MEASUREMENT, key.station()), bucketName, influxdbOrg); } - catch (Exception e) + catch(Exception e) { throw new RuntimeException("Can't delete weather forecast from influx", e); } @@ -92,24 +93,36 @@ private String formatTimeForFlux(ZonedDateTime time) } @Override - public WeatherForecastPO get(ZonedDateTime zonedDateTime) + public WeatherForecastPO get(WeatherForecastKey key) { - String flux = String.format(FILTER_INSTANT, - bucketName, - formatTimeForFlux(zonedDateTime.minusMinutes(1)), - formatTimeForFlux(zonedDateTime.plusMinutes(1)), - WEATHER_MEASUREMENT, WeatherForecastPO.COLUMN_SUN_PROBABILITY); - List tables = queryApi.query(flux); - if (!tables.isEmpty() && !tables.getFirst().getRecords().isEmpty()) + String flux = String.format(GET_QUERY, bucketName, formatTimeForFlux(key.time().minusMinutes(1)), + formatTimeForFlux(key.time().plusMinutes(1)), WEATHER_MEASUREMENT, key.station(), WeatherForecastPO.COLUMN_SUN_PROBABILITY); + for (FluxTable table : queryApi.query(flux)) { - WeatherForecastPO data = new WeatherForecastPO(); - Object value = tables.getFirst().getRecords().getFirst().getValueByKey("_value"); - if (value instanceof Number aNumber) + for (FluxRecord record : table.getRecords()) { - data.setSunProbability(new Percentage(aNumber.doubleValue())); + WeatherForecastPO data = convert(record); + if (data != null) + { + return data; + } } - return data; } return null; } + + private WeatherForecastPO convert(FluxRecord record) + { + WeatherForecastPO data = new WeatherForecastPO(); + Object value = record.getValueByKey("_value"); + if (value instanceof Number aNumber) + { + data.setSunProbability(new Percentage(aNumber.doubleValue())); + } + else + { + return null; + } + return data; + } } diff --git a/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java b/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java index e672feb..b2c1a85 100644 --- a/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java +++ b/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java @@ -1,5 +1,7 @@ package de.hatoka.eos.persistence.influx; +import de.hatoka.eos.persistence.capi.MeteoMediaStation; +import de.hatoka.eos.persistence.capi.WeatherForecastKey; import de.hatoka.eos.persistence.capi.WeatherForecastPO; import de.hatoka.eos.units.capi.Percentage; import io.quarkus.test.junit.QuarkusTest; @@ -7,8 +9,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import java.time.ZonedDateTime; import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import static org.junit.jupiter.api.Assertions.*; @@ -33,23 +35,34 @@ class InfluxWeatherForecastDaoTest @AfterEach void tearDown() { - dao.delete(TEST_TIME); - dao.delete(TEST_TIME.plusHours(1)); - dao.delete(TEST_TIME.minusHours(1)); + dao.delete(getKey(TEST_TIME)); + dao.delete(getKey(TEST_TIME.plusHours(1))); + dao.delete(getKey(TEST_TIME.minusHours(1))); + } + + private static WeatherForecastPO createForecast(Percentage sunProbability) + { + WeatherForecastPO forecast = new WeatherForecastPO(); + forecast.setSunProbability(sunProbability); + return forecast; + } + + private static WeatherForecastKey getKey(ZonedDateTime time) + { + return WeatherForecastKey.valueOf(MeteoMediaStation.APOLDA, time); } @Test void shouldStoreAndRetrieveWeatherForecastData() { // Given - WeatherForecastPO forecast = new WeatherForecastPO(); - forecast.setSunProbability(TEST_SUN_PROBABILITY); + WeatherForecastPO forecast = createForecast(TEST_SUN_PROBABILITY); // When - dao.update(TEST_TIME, forecast); + dao.update(getKey(TEST_TIME), forecast); // Then - WeatherForecastPO retrievedForecast = dao.get(TEST_TIME); + WeatherForecastPO retrievedForecast = dao.get(getKey(TEST_TIME)); assertNotNull(retrievedForecast, "Retrieved forecast should not be null"); assertNotNull(retrievedForecast.getSunProbability(), "Sun probability should not be null"); @@ -61,17 +74,15 @@ void shouldStoreAndRetrieveWeatherForecastData() void shouldUpdateExistingWeatherForecastData() { // Given - store initial data - WeatherForecastPO initialForecast = new WeatherForecastPO(); - initialForecast.setSunProbability(TEST_SUN_PROBABILITY); - dao.update(TEST_TIME, initialForecast); + WeatherForecastPO initialForecast = createForecast(TEST_SUN_PROBABILITY); + dao.update(getKey(TEST_TIME), initialForecast); // When - update with new data - WeatherForecastPO updatedForecast = new WeatherForecastPO(); - updatedForecast.setSunProbability(UPDATED_SUN_PROBABILITY); - dao.update(TEST_TIME, updatedForecast); + WeatherForecastPO updatedForecast = createForecast(UPDATED_SUN_PROBABILITY); + dao.update(getKey(TEST_TIME), updatedForecast); // Then - WeatherForecastPO retrievedForecast = dao.get(TEST_TIME); + WeatherForecastPO retrievedForecast = dao.get(getKey(TEST_TIME)); assertNotNull(retrievedForecast, "Retrieved forecast should not be null"); assertNotNull(retrievedForecast.getSunProbability(), "Sun probability should not be null"); @@ -88,7 +99,7 @@ void shouldReturnEmptyForecastWhenNoDataExists() // When // Then - assertNull(dao.get(nonExistentTime), "Retrieved forecast should not be null"); + assertNull(dao.get(getKey(nonExistentTime)), "Retrieved forecast should not be null"); } @Test @@ -99,32 +110,19 @@ void shouldHandleMultipleTimePoints() ZonedDateTime time2 = TEST_TIME.plusHours(1); ZonedDateTime time3 = TEST_TIME.plusHours(2); - WeatherForecastPO forecast1 = new WeatherForecastPO(); - forecast1.setSunProbability(new Percentage(0.5)); - - WeatherForecastPO forecast2 = new WeatherForecastPO(); - forecast2.setSunProbability(new Percentage(0.7)); - - WeatherForecastPO forecast3 = new WeatherForecastPO(); - forecast3.setSunProbability(new Percentage(0.9)); - // When - dao.update(time1, forecast1); - dao.update(time2, forecast2); - dao.update(time3, forecast3); + dao.update(getKey(time1), createForecast(new Percentage(0.5))); + dao.update(getKey(time2), createForecast(new Percentage(0.7))); + dao.update(getKey(time3), createForecast(new Percentage(0.9))); // Then - WeatherForecastPO retrieved1 = dao.get(time1); - WeatherForecastPO retrieved2 = dao.get(time2); - WeatherForecastPO retrieved3 = dao.get(time3); - - assertEquals(0.5, retrieved1.getSunProbability().value(), 0.001); - assertEquals(0.7, retrieved2.getSunProbability().value(), 0.001); - assertEquals(0.9, retrieved3.getSunProbability().value(), 0.001); + assertEquals(0.5, dao.get(getKey(time1)).getSunProbability().value(), 0.001); + assertEquals(0.7, dao.get(getKey(time2)).getSunProbability().value(), 0.001); + assertEquals(0.9, dao.get(getKey(time3)).getSunProbability().value(), 0.001); // Clean up additional test data - dao.delete(time2); - dao.delete(time3); + dao.delete(getKey(time2)); + dao.delete(getKey(time3)); } @Test @@ -132,14 +130,13 @@ void shouldHandlePreciseTimeRetrieval() { // Given - forecast with precise timestamp ZonedDateTime preciseTime = TEST_TIME.truncatedTo(ChronoUnit.SECONDS); - WeatherForecastPO forecast = new WeatherForecastPO(); - forecast.setSunProbability(TEST_SUN_PROBABILITY); + WeatherForecastPO forecast = createForecast(TEST_SUN_PROBABILITY); // When - dao.update(preciseTime, forecast); + dao.update(getKey(preciseTime), forecast); // Then - should retrieve data even with slight time variations - WeatherForecastPO retrieved = dao.get(preciseTime); + WeatherForecastPO retrieved = dao.get(getKey(preciseTime)); assertNotNull(retrieved.getSunProbability()); assertEquals(TEST_SUN_PROBABILITY.value(), retrieved.getSunProbability().value(), 0.001); @@ -149,45 +146,39 @@ void shouldHandlePreciseTimeRetrieval() void shouldHandleBoundaryPercentageValues() { // Test with 0% sun probability - WeatherForecastPO forecast0 = new WeatherForecastPO(); - forecast0.setSunProbability(Percentage.ZERO); - dao.update(TEST_TIME, forecast0); + WeatherForecastPO forecast0 = createForecast(Percentage.ZERO); + dao.update(getKey(TEST_TIME), forecast0); // Test with 100% sun probability ZonedDateTime time100 = TEST_TIME.plusMinutes(5); - WeatherForecastPO forecast100 = new WeatherForecastPO(); - forecast100.setSunProbability(Percentage.ONE_HUNDRED); - dao.update(time100, forecast100); + WeatherForecastPO forecast100 = createForecast(Percentage.ONE_HUNDRED); + dao.update(getKey(time100), forecast100); // Verify boundary values - WeatherForecastPO retrieved0 = dao.get(TEST_TIME); - WeatherForecastPO retrieved100 = dao.get(time100); - - assertEquals(0.0, retrieved0.getSunProbability().value(), 0.001); - assertEquals(1.0, retrieved100.getSunProbability().value(), 0.001); + assertEquals(0.0, dao.get(getKey(TEST_TIME)).getSunProbability().value(), 0.001); + assertEquals(1.0, dao.get(getKey(time100)).getSunProbability().value(), 0.001); // Clean up - dao.delete(time100); + dao.delete(getKey(time100)); } @Test void shouldDeleteWeatherForecastData() { // Given - store some data first - WeatherForecastPO forecast = new WeatherForecastPO(); - forecast.setSunProbability(TEST_SUN_PROBABILITY); - dao.update(TEST_TIME, forecast); + WeatherForecastPO forecast = createForecast(TEST_SUN_PROBABILITY); + dao.update(getKey(TEST_TIME), forecast); // Verify data exists - WeatherForecastPO retrievedBeforeDelete = dao.get(TEST_TIME); + WeatherForecastPO retrievedBeforeDelete = dao.get(getKey(TEST_TIME)); assertNotNull(retrievedBeforeDelete.getSunProbability(), "Data should exist before deletion"); // When - delete the data - dao.delete(TEST_TIME); + dao.delete(getKey(TEST_TIME)); // Then - data should no longer exist - assertNull(dao.get(TEST_TIME),"Data should not exist after deletion"); + assertNull(dao.get(getKey(TEST_TIME)),"Data should not exist after deletion"); } @Test @@ -198,37 +189,21 @@ void shouldDeleteOnlySpecificTimeData() ZonedDateTime time2 = TEST_TIME.plusHours(1); ZonedDateTime time3 = TEST_TIME.plusHours(2); - WeatherForecastPO forecast1 = new WeatherForecastPO(); - forecast1.setSunProbability(new Percentage(0.3)); - - WeatherForecastPO forecast2 = new WeatherForecastPO(); - forecast2.setSunProbability(new Percentage(0.6)); - - WeatherForecastPO forecast3 = new WeatherForecastPO(); - forecast3.setSunProbability(new Percentage(0.9)); - - dao.update(time1, forecast1); - dao.update(time2, forecast2); - dao.update(time3, forecast3); + dao.update(getKey(time1), createForecast(new Percentage(0.3))); + dao.update(getKey(time2), createForecast(new Percentage(0.6))); + dao.update(getKey(time3), createForecast(new Percentage(0.9))); // When - delete only the middle data point - dao.delete(time2); + dao.delete(getKey(time2)); // Then - only the deleted data should be gone - WeatherForecastPO retrieved1 = dao.get(time1); - WeatherForecastPO retrieved2 = dao.get(time2); - WeatherForecastPO retrieved3 = dao.get(time3); - - assertNotNull(retrieved1.getSunProbability(), "First data point should still exist"); - assertNull(retrieved2, "Second data point should be deleted"); - assertNotNull(retrieved3.getSunProbability(), "Third data point should still exist"); - - assertEquals(0.3, retrieved1.getSunProbability().value(), 0.001); - assertEquals(0.9, retrieved3.getSunProbability().value(), 0.001); + assertEquals(0.3, dao.get(getKey(time1)).getSunProbability().value(), 0.001); + assertNull(dao.get(getKey(time2)), "Second data point should be deleted"); + assertEquals(0.9, dao.get(getKey(time3)).getSunProbability().value(), 0.001); // Clean up remaining data - dao.delete(time1); - dao.delete(time3); + dao.delete(getKey(time1)); + dao.delete(getKey(time3)); } @Test @@ -238,8 +213,27 @@ void shouldHandleDeleteOfNonExistentData() ZonedDateTime nonExistentTime = TEST_TIME.plusDays(100); // When/Then - deleting non-existent data should not throw an exception - assertDoesNotThrow(() -> dao.delete(nonExistentTime), + assertDoesNotThrow(() -> dao.delete(getKey(nonExistentTime)), "Deleting non-existent data should not throw an exception"); } + + @Test + void shouldDeleteOnlyStation() + { + // Given - store data at multiple time points + WeatherForecastKey leipzigKey = WeatherForecastKey.valueOf(MeteoMediaStation.LEIPZIG_STADTWERKE, TEST_TIME); + dao.update(getKey(TEST_TIME), createForecast(new Percentage(0.3))); + dao.update(leipzigKey, createForecast(new Percentage(0.6))); + + // When - delete only one station + dao.delete(getKey(TEST_TIME)); + + // Then - only the deleted data should be gone + assertNull(dao.get(getKey(TEST_TIME)), "one data point should be deleted"); + assertNotNull(dao.get(leipzigKey), "the other data point should not be deleted"); + assertEquals(0.6, dao.get(leipzigKey).getSunProbability().value(), 0.001); + + dao.delete(leipzigKey); + } } From 7a57db08c0db516ab81a4cb22b101407f62b914e Mon Sep 17 00:00:00 2001 From: Thomas Bergmann Date: Mon, 3 Nov 2025 18:17:40 +0100 Subject: [PATCH 05/16] feat: add OpenMeteoWeatherForecastImporter --- .../AbstractWeatherForecastImporter.java | 106 ++++++++++ .../MeteoMediaWeatherForecastImporter.java | 58 +----- .../OpenMeteoWeatherForecastImporter.java | 183 ++++++++++++++++++ .../eos/forecast/openmeteo/HourlyData.java | 43 ++++ .../forecast/openmeteo/OpenMeteoResponse.java | 69 +++++++ ...MeteoMediaWeatherForecastImporterTest.java | 3 +- .../OpenMeteoWeatherForecastImporterTest.java | 70 +++++++ .../persistence/capi/MeteoMediaStation.java | 23 ++- 8 files changed, 499 insertions(+), 56 deletions(-) create mode 100644 forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java create mode 100644 forecast/src/main/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporter.java create mode 100644 forecast/src/main/java/de/hatoka/eos/forecast/openmeteo/HourlyData.java create mode 100644 forecast/src/main/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoResponse.java create mode 100644 forecast/src/test/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporterTest.java diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java new file mode 100644 index 0000000..64f6c66 --- /dev/null +++ b/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java @@ -0,0 +1,106 @@ +package de.hatoka.eos.forecast; + +import de.hatoka.eos.persistence.capi.MeteoMediaStation; +import de.hatoka.eos.persistence.capi.WeatherForcastDAO; +import de.hatoka.eos.persistence.capi.WeatherForecastKey; +import de.hatoka.eos.persistence.capi.WeatherForecastPO; +import de.hatoka.eos.units.capi.Percentage; +import jakarta.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Map; + +/** + * Abstract base class for weather forecast importers that provides common functionality + * for downloading, processing, and storing weather forecast data. + */ +public abstract class AbstractWeatherForecastImporter +{ + protected final Logger logger = LoggerFactory.getLogger(getClass()); + + @Inject + protected WeatherForcastDAO weatherDao; + + /** + * Imports weather forecast data for the specified station. + * + * @param station the weather station to import data for + * @return the start date of the imported forecast data + * @throws IOException if there's an error downloading or processing the data + * @throws InterruptedException if the operation is interrupted + */ + public ZonedDateTime importWeatherForecast(MeteoMediaStation station) throws IOException, InterruptedException + { + ZonedDateTime startDate = ZonedDateTime.now().toLocalDate().atStartOfDay(ZoneId.of("UTC")); + logger.info("Starting weather forecast import for station: {}", station.name()); + + try + { + // Download and process the weather data (implementation specific) + Map sunshineDurationPerHour = downloadAndProcessWeatherData(station, startDate); + + logger.info("Extracted sunshine data for {} hours", sunshineDurationPerHour.size()); + + // Store the data in the database + storeSunshineData(sunshineDurationPerHour, station); + + logger.info("Successfully imported weather forecast data for {}", startDate); + } + catch (Exception e) + { + logger.error("Failed to import weather forecast for station " + station.name(), e); + throw e; + } + + return startDate; + } + + /** + * Downloads and processes weather data from the specific weather service. + * This method must be implemented by subclasses to handle their specific data sources. + * + * @param station the weather station to get data for + * @param startDate the start date for the forecast + * @return map of datetime to sunshine minutes per hour + * @throws IOException if there's an error downloading or processing the data + * @throws InterruptedException if the operation is interrupted + */ + protected abstract Map downloadAndProcessWeatherData(MeteoMediaStation station, ZonedDateTime startDate) + throws IOException, InterruptedException; + + /** + * Stores sunshine duration data in the database. + * Converts sunshine minutes (0-60) to sun probability (0.0-1.0) and stores in the database. + * + * @param sunshineDurationPerHour map of datetime to sunshine minutes + * @param station the weather station this data belongs to + */ + protected void storeSunshineData(Map sunshineDurationPerHour, MeteoMediaStation station) + { + for (Map.Entry entry : sunshineDurationPerHour.entrySet()) + { + ZonedDateTime dateTime = entry.getKey(); + Integer sunshineMinutes = entry.getValue(); + + // Convert sunshine minutes (0-60) to probability (0.0-1.0) + double sunProbability = Math.min(1.0, sunshineMinutes / 60.0); + + WeatherForecastPO forecast = new WeatherForecastPO(); + forecast.setSunProbability(new Percentage(sunProbability)); + + try + { + weatherDao.update(new WeatherForecastKey(station.name(), dateTime), forecast); + logger.debug("Stored weather data for {}: {}% sun probability", dateTime, Math.round(sunProbability * 100)); + } + catch(Exception e) + { + logger.error("Failed to store weather data for " + dateTime, e); + } + } + } +} \ No newline at end of file diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java index 1d7e5ce..8d02de4 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java @@ -1,14 +1,8 @@ package de.hatoka.eos.forecast; import de.hatoka.eos.persistence.capi.MeteoMediaStation; -import de.hatoka.eos.persistence.capi.WeatherForcastDAO; -import de.hatoka.eos.persistence.capi.WeatherForecastKey; -import de.hatoka.eos.persistence.capi.WeatherForecastPO; -import de.hatoka.eos.units.capi.Percentage; import jakarta.inject.Inject; import jakarta.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URI; @@ -19,32 +13,26 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.time.Duration; -import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.List; import java.util.Map; /** - * Downloads weather forecast PNG images from external services and processes them to extract sunshine duration data, which is then stored in the + * Downloads weather forecast PNG images from MeteoMedia external services and processes them to extract sunshine duration data, which is then stored in the * database. */ @Singleton -public class MeteoMediaWeatherForecastImporter +public class MeteoMediaWeatherForecastImporter extends AbstractWeatherForecastImporter { - private static final Logger logger = LoggerFactory.getLogger(MeteoMediaWeatherForecastImporter.class); private static final String URI_FORMAT = "https://wetterstationen.meteomedia.de/messnetz/vorhersagegrafik/%s.png"; @Inject MeteoMediaWeatherSunshineDurationConverter converter; - @Inject - WeatherForcastDAO weatherDao; - public ZonedDateTime importWeatherForecast(MeteoMediaStation station) throws IOException, InterruptedException + @Override + protected Map downloadAndProcessWeatherData(MeteoMediaStation station, ZonedDateTime startDate) + throws IOException, InterruptedException { - ZonedDateTime startDate = ZonedDateTime.now().toLocalDate().atStartOfDay(ZoneId.of("UTC")); - logger.info("Starting weather forecast import for station: {}", station.name()); - // Download the PNG image byte[] imageData = downloadImage(URI.create(URI_FORMAT.formatted(station.getStationNumber()))); logger.debug("Downloaded {} bytes of image data", imageData.length); @@ -55,22 +43,13 @@ public ZonedDateTime importWeatherForecast(MeteoMediaStation station) throws IOE try { // Process the image and extract sunshine data - Map sunshineDurationPerHour = converter.extractSunshineDuration(converter.loadImageFromFile(tempFile.toFile()), - startDate); - - logger.info("Extracted sunshine data for {} hours", sunshineDurationPerHour.size()); - - // Store the data in the database - storeSunshineData(sunshineDurationPerHour, station); - - logger.info("Successfully imported weather forecast data for {}", startDate); + return converter.extractSunshineDuration(converter.loadImageFromFile(tempFile.toFile()), startDate); } finally { // Clean up temporary file Files.deleteIfExists(tempFile); } - return startDate; } private byte[] downloadImage(URI imageUrl) throws IOException, InterruptedException @@ -110,29 +89,4 @@ private Path saveTempImage(byte[] imageData, ZonedDateTime startDate) throws IOE logger.debug("Saved temporary image file: {}", tempFile); return tempFile; } - - private void storeSunshineData(Map sunshineDurationPerHour, MeteoMediaStation station) - { - for (Map.Entry entry : sunshineDurationPerHour.entrySet()) - { - ZonedDateTime dateTime = entry.getKey(); - Integer sunshineMinutes = entry.getValue(); - - // Convert sunshine minutes (0-60) to probability (0.0-1.0) - double sunProbability = Math.min(1.0, sunshineMinutes / 60.0); - - WeatherForecastPO forecast = new WeatherForecastPO(); - forecast.setSunProbability(new Percentage(sunProbability)); - - try - { - weatherDao.update(new WeatherForecastKey(station.name(), dateTime), forecast); - logger.debug("Stored weather data for {}: {}% sun probability", dateTime, Math.round(sunProbability * 100)); - } - catch(Exception e) - { - logger.error("Failed to store weather data for " + dateTime, e); - } - } - } } diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporter.java new file mode 100644 index 0000000..98777ec --- /dev/null +++ b/forecast/src/main/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporter.java @@ -0,0 +1,183 @@ +package de.hatoka.eos.forecast; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.hatoka.eos.forecast.openmeteo.OpenMeteoResponse; +import de.hatoka.eos.persistence.capi.MeteoMediaStation; +import jakarta.inject.Singleton; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Downloads weather forecast data from OpenMeteo API and processes it to extract sunshine duration data. + * + * OpenMeteo API Documentation: https://open-meteo.com/en/docs + * + * Example API call: + * https://api.open-meteo.com/v1/forecast?latitude=51.0262&longitude=11.5164&hourly=sunshine_duration&forecast_days=3&timezone=UTC + */ +@Singleton +public class OpenMeteoWeatherForecastImporter extends AbstractWeatherForecastImporter +{ + private static final String API_BASE_URL = "https://api.open-meteo.com/v1/forecast"; + private static final String FORECAST_DAYS = "3"; + private static final String TIMEZONE = "UTC"; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected Map downloadAndProcessWeatherData(MeteoMediaStation station, ZonedDateTime startDate) + throws IOException, InterruptedException + { + // Build the OpenMeteo API URL + String apiUrl = buildApiUrl(station); + logger.debug("Requesting OpenMeteo API: {}", apiUrl); + + // Download the JSON data from OpenMeteo API + String jsonResponse = downloadJsonData(URI.create(apiUrl)); + logger.debug("Downloaded {} characters of JSON data", jsonResponse.length()); + + // Parse the JSON response + OpenMeteoResponse response = objectMapper.readValue(jsonResponse, OpenMeteoResponse.class); + + // Convert the response to our internal format + return convertToSunshineDurationMap(response); + } + + /** + * Builds the OpenMeteo API URL for the given station. + * + * @param station the weather station + * @return the complete API URL + */ + private String buildApiUrl(MeteoMediaStation station) + { + return String.format("%s?latitude=%.4f&longitude=%.4f&hourly=sunshine_duration&forecast_days=%s&timezone=%s", + API_BASE_URL, + station.getLatitude(), + station.getLongitude(), + FORECAST_DAYS, + TIMEZONE); + } + + /** + * Downloads JSON data from the OpenMeteo API. + * + * @param apiUrl the API URL to call + * @return the JSON response as a string + * @throws IOException if the HTTP request fails + * @throws InterruptedException if the request is interrupted + */ + private String downloadJsonData(URI apiUrl) throws IOException, InterruptedException + { + try (HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .followRedirects(HttpClient.Redirect.NORMAL) + .build()) + { + HttpRequest request = HttpRequest.newBuilder() + .uri(apiUrl) + .timeout(Duration.ofSeconds(30)) + .header("User-Agent", "EOS-Weather-Forecast-Importer/1.0") + .header("Accept", "application/json") + .GET() + .build(); + + logger.debug("Sending HTTP request to OpenMeteo API: {}", apiUrl); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) + { + throw new IOException("OpenMeteo API request failed with status code: " + response.statusCode() + + ", response: " + response.body()); + } + + return response.body(); + } + } + + /** + * Converts the OpenMeteo API response to our internal sunshine duration format. + * + * @param response the OpenMeteo API response + * @return map of datetime to sunshine duration in minutes + */ + private Map convertToSunshineDurationMap(OpenMeteoResponse response) + { + Map result = new HashMap<>(); + + if (response.getHourly() == null) + { + logger.warn("No hourly data in OpenMeteo response"); + return result; + } + + List times = response.getHourly().getTime(); + List sunshineDurations = response.getHourly().getSunshineDuration(); + + if (times == null || sunshineDurations == null) + { + logger.warn("Missing time or sunshine_duration data in OpenMeteo response"); + return result; + } + + if (times.size() != sunshineDurations.size()) + { + logger.warn("Time and sunshine_duration arrays have different sizes: {} vs {}", + times.size(), sunshineDurations.size()); + return result; + } + + // Parse each time/sunshine pair + for (int i = 0; i < times.size(); i++) + { + try + { + String timeString = times.get(i); + ZonedDateTime dateTime; + + // Handle OpenMeteo time format which might be with or without timezone + if (timeString.contains("T") && !timeString.contains("Z") && !timeString.contains("+") && !timeString.substring(timeString.indexOf("T")).contains("-")) + { + // Format like "2025-11-05T15:00" - add UTC timezone + dateTime = ZonedDateTime.parse(timeString + "Z", DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + else + { + // Format with timezone information + dateTime = ZonedDateTime.parse(timeString, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + + // Convert sunshine duration from seconds to minutes + // OpenMeteo returns sunshine duration in seconds for the past hour + Double sunshineSeconds = sunshineDurations.get(i); + int sunshineMinutes = sunshineSeconds != null ? (int) Math.round(sunshineSeconds / 60.0) : 0; + + // Ensure the value is within valid range (0-60 minutes) + sunshineMinutes = Math.max(0, Math.min(60, sunshineMinutes)); + + result.put(dateTime, sunshineMinutes); + + logger.trace("Parsed {}: {} minutes sunshine", dateTime, sunshineMinutes); + } + catch (Exception e) + { + logger.warn("Failed to parse time entry at index {}: '{}', sunshine: {}", + i, times.get(i), sunshineDurations.get(i), e); + } + } + + logger.info("Converted {} OpenMeteo data points to sunshine duration map", result.size()); + return result; + } +} diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/openmeteo/HourlyData.java b/forecast/src/main/java/de/hatoka/eos/forecast/openmeteo/HourlyData.java new file mode 100644 index 0000000..d1e5f27 --- /dev/null +++ b/forecast/src/main/java/de/hatoka/eos/forecast/openmeteo/HourlyData.java @@ -0,0 +1,43 @@ +package de.hatoka.eos.forecast.openmeteo; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Hourly weather data from OpenMeteo API. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class HourlyData +{ + @JsonProperty("time") + private List time; + + @JsonProperty("sunshine_duration") + private List sunshineDuration; + + public HourlyData() + { + } + + public List getTime() + { + return time; + } + + public void setTime(List time) + { + this.time = time; + } + + public List getSunshineDuration() + { + return sunshineDuration; + } + + public void setSunshineDuration(List sunshineDuration) + { + this.sunshineDuration = sunshineDuration; + } +} \ No newline at end of file diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoResponse.java b/forecast/src/main/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoResponse.java new file mode 100644 index 0000000..f01ceda --- /dev/null +++ b/forecast/src/main/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoResponse.java @@ -0,0 +1,69 @@ +package de.hatoka.eos.forecast.openmeteo; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Data Transfer Object for OpenMeteo API response. + * Represents the weather forecast response from the OpenMeteo API. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class OpenMeteoResponse +{ + @JsonProperty("latitude") + private double latitude; + + @JsonProperty("longitude") + private double longitude; + + @JsonProperty("timezone") + private String timezone; + + @JsonProperty("hourly") + private HourlyData hourly; + + // Constructors, getters, and setters + public OpenMeteoResponse() + { + } + + public double getLatitude() + { + return latitude; + } + + public void setLatitude(double latitude) + { + this.latitude = latitude; + } + + public double getLongitude() + { + return longitude; + } + + public void setLongitude(double longitude) + { + this.longitude = longitude; + } + + public String getTimezone() + { + return timezone; + } + + public void setTimezone(String timezone) + { + this.timezone = timezone; + } + + public HourlyData getHourly() + { + return hourly; + } + + public void setHourly(HourlyData hourly) + { + this.hourly = hourly; + } +} \ No newline at end of file diff --git a/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java index 37bb6ab..213a924 100644 --- a/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java +++ b/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java @@ -2,6 +2,7 @@ import de.hatoka.eos.persistence.capi.MeteoMediaStation; import de.hatoka.eos.persistence.capi.WeatherForcastDAO; +import de.hatoka.eos.persistence.capi.WeatherForecastKey; import de.hatoka.eos.persistence.capi.WeatherForecastPO; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; @@ -31,7 +32,7 @@ void testImportFromValidUrl() throws IOException, InterruptedException { // this importer can only import data of today and tomorrow ZonedDateTime testDate = importer.importWeatherForecast(MeteoMediaStation.APOLDA); - WeatherForecastPO retrieved = weatherDao.get(testDate.plusHours(1)); + WeatherForecastPO retrieved = weatherDao.get(WeatherForecastKey.valueOf(MeteoMediaStation.APOLDA, testDate.plusHours(1))); assertNotNull(retrieved); assertNotNull(retrieved.getSunProbability()); importer.importWeatherForecast(MeteoMediaStation.LEIPZIG_STADTWERKE); diff --git a/forecast/src/test/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporterTest.java new file mode 100644 index 0000000..cb3c550 --- /dev/null +++ b/forecast/src/test/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporterTest.java @@ -0,0 +1,70 @@ +package de.hatoka.eos.forecast; + +import de.hatoka.eos.persistence.capi.MeteoMediaStation; +import de.hatoka.eos.persistence.capi.WeatherForcastDAO; +import de.hatoka.eos.persistence.capi.WeatherForecastKey; +import de.hatoka.eos.persistence.capi.WeatherForecastPO; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test for OpenMeteoWeatherForecastImporter functionality. + * Note: This test requires network access to download actual weather data from OpenMeteo API. + * For CI/CD environments, you might want to mock the HTTP client or use test resources. + */ +@QuarkusTest +class OpenMeteoWeatherForecastImporterTest +{ + @Inject + OpenMeteoWeatherForecastImporter importer; + + @Inject + WeatherForcastDAO weatherDao; + + @Test + void testImportFromOpenMeteoApi() throws IOException, InterruptedException + { + // Test importing data from OpenMeteo API + ZonedDateTime testDate = importer.importWeatherForecast(MeteoMediaStation.APOLDA); + + // Verify that data was imported successfully + assertNotNull(testDate, "Import should return a valid start date"); + + // Check if data was stored in the database (test the next hour) + WeatherForecastPO retrieved = weatherDao.get(WeatherForecastKey.valueOf(MeteoMediaStation.APOLDA, testDate.plusHours(1))); + assertNotNull(retrieved, "Weather data should be stored in database"); + assertNotNull(retrieved.getSunProbability(), "Sun probability should not be null"); + + // Verify the sun probability is within valid range (0.0 to 1.0) + double sunProb = retrieved.getSunProbability().value(); + assertTrue(sunProb >= 0.0 && sunProb <= 1.0, + "Sun probability should be between 0.0 and 1.0, but was: " + sunProb); + + // Test importing for a different station + ZonedDateTime testDate2 = importer.importWeatherForecast(MeteoMediaStation.LEIPZIG_STADTWERKE); + assertNotNull(testDate2, "Import for Leipzig should also return a valid start date"); + } + + @Test + void testStationCoordinates() + { + // Verify that our stations have valid coordinates for OpenMeteo API + assertNotEquals(0.0, MeteoMediaStation.APOLDA.getLatitude(), "APOLDA should have valid latitude"); + assertNotEquals(0.0, MeteoMediaStation.APOLDA.getLongitude(), "APOLDA should have valid longitude"); + + assertNotEquals(0.0, MeteoMediaStation.LEIPZIG_STADTWERKE.getLatitude(), "LEIPZIG_STADTWERKE should have valid latitude"); + assertNotEquals(0.0, MeteoMediaStation.LEIPZIG_STADTWERKE.getLongitude(), "LEIPZIG_STADTWERKE should have valid longitude"); + + // Check that coordinates are reasonable for Germany + assertTrue(MeteoMediaStation.APOLDA.getLatitude() > 47.0 && MeteoMediaStation.APOLDA.getLatitude() < 55.0, + "APOLDA latitude should be within Germany bounds"); + assertTrue(MeteoMediaStation.APOLDA.getLongitude() > 5.0 && MeteoMediaStation.APOLDA.getLongitude() < 15.0, + "APOLDA longitude should be within Germany bounds"); + } +} \ No newline at end of file diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/MeteoMediaStation.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/MeteoMediaStation.java index d448b95..88d8658 100644 --- a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/MeteoMediaStation.java +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/MeteoMediaStation.java @@ -1,19 +1,36 @@ package de.hatoka.eos.persistence.capi; +/** + * Weather stations with their MeteoMedia station numbers and geographic coordinates for OpenMeteo API. + */ public enum MeteoMediaStation { - APOLDA("095550"), - LEIPZIG_STADTWERKE("104700"); + APOLDA("095550", 51.0262, 11.5164), + LEIPZIG_STADTWERKE("104700", 51.3397, 12.3731); private final String stationNumber; + private final double latitude; + private final double longitude; - MeteoMediaStation(String stationNumber) + MeteoMediaStation(String stationNumber, double latitude, double longitude) { this.stationNumber = stationNumber; + this.latitude = latitude; + this.longitude = longitude; } public String getStationNumber() { return stationNumber; } + + public double getLatitude() + { + return latitude; + } + + public double getLongitude() + { + return longitude; + } } From 2d4840ab3db5e2a3945cf65da4fdadafe1f89b27 Mon Sep 17 00:00:00 2001 From: Thomas Bergmann Date: Mon, 3 Nov 2025 18:45:05 +0100 Subject: [PATCH 06/16] rework: WeatherDataSource --- .../forecast/InfluxWeatherForecast.java | 5 +- .../AbstractWeatherForecastImporter.java | 18 ++-- .../MeteoMediaWeatherForecastImporter.java | 11 ++- .../OpenMeteoWeatherForecastImporter.java | 85 ++++++++++--------- ...MeteoMediaWeatherForecastImporterTest.java | 9 +- .../OpenMeteoWeatherForecastImporterTest.java | 33 +++---- .../persistence/capi/WeatherDataSource.java | 29 +++++++ .../persistence/capi/WeatherForecastKey.java | 6 +- ...oMediaStation.java => WeatherStation.java} | 4 +- .../influx/InfluxWeatherForecastDao.java | 9 +- .../influx/InfluxWeatherForecastDaoTest.java | 7 +- 11 files changed, 134 insertions(+), 82 deletions(-) create mode 100644 persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherDataSource.java rename persistence-api/src/main/java/de/hatoka/eos/persistence/capi/{MeteoMediaStation.java => WeatherStation.java} (86%) diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/InfluxWeatherForecast.java b/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/InfluxWeatherForecast.java index 7ede610..8fcca2d 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/InfluxWeatherForecast.java +++ b/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/InfluxWeatherForecast.java @@ -2,10 +2,9 @@ import de.hatoka.eos.devices.capi.business.forecast.WeatherForecast; import de.hatoka.eos.persistence.capi.WeatherForecastKey; +import de.hatoka.eos.persistence.capi.WeatherDataSource; import de.hatoka.eos.persistence.influx.InfluxWeatherForecastDao; import de.hatoka.eos.units.capi.Percentage; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; import java.time.ZonedDateTime; @@ -25,6 +24,6 @@ public InfluxWeatherForecast(String station, InfluxWeatherForecastDao forecastDA @Override public Percentage getSunProbability(ZonedDateTime time) { - return forecastDAO.get(new WeatherForecastKey(station, time)).getSunProbability(); + return forecastDAO.get(new WeatherForecastKey(station, time, WeatherDataSource.METEOMEDIA)).getSunProbability(); } } diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java index 64f6c66..05f0f82 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java @@ -1,9 +1,10 @@ package de.hatoka.eos.forecast; -import de.hatoka.eos.persistence.capi.MeteoMediaStation; +import de.hatoka.eos.persistence.capi.WeatherStation; import de.hatoka.eos.persistence.capi.WeatherForcastDAO; import de.hatoka.eos.persistence.capi.WeatherForecastKey; import de.hatoka.eos.persistence.capi.WeatherForecastPO; +import de.hatoka.eos.persistence.capi.WeatherDataSource; import de.hatoka.eos.units.capi.Percentage; import jakarta.inject.Inject; import org.slf4j.Logger; @@ -25,6 +26,13 @@ public abstract class AbstractWeatherForecastImporter @Inject protected WeatherForcastDAO weatherDao; + /** + * Gets the source identifier for this importer. + * + * @return the source identifier (e.g., WeatherDataSource.METEOMEDIA, WeatherDataSource.OPENMETEO) + */ + protected abstract WeatherDataSource getSource(); + /** * Imports weather forecast data for the specified station. * @@ -33,7 +41,7 @@ public abstract class AbstractWeatherForecastImporter * @throws IOException if there's an error downloading or processing the data * @throws InterruptedException if the operation is interrupted */ - public ZonedDateTime importWeatherForecast(MeteoMediaStation station) throws IOException, InterruptedException + public ZonedDateTime importWeatherForecast(WeatherStation station) throws IOException, InterruptedException { ZonedDateTime startDate = ZonedDateTime.now().toLocalDate().atStartOfDay(ZoneId.of("UTC")); logger.info("Starting weather forecast import for station: {}", station.name()); @@ -69,7 +77,7 @@ public ZonedDateTime importWeatherForecast(MeteoMediaStation station) throws IOE * @throws IOException if there's an error downloading or processing the data * @throws InterruptedException if the operation is interrupted */ - protected abstract Map downloadAndProcessWeatherData(MeteoMediaStation station, ZonedDateTime startDate) + protected abstract Map downloadAndProcessWeatherData(WeatherStation station, ZonedDateTime startDate) throws IOException, InterruptedException; /** @@ -79,7 +87,7 @@ protected abstract Map downloadAndProcessWeatherData(Met * @param sunshineDurationPerHour map of datetime to sunshine minutes * @param station the weather station this data belongs to */ - protected void storeSunshineData(Map sunshineDurationPerHour, MeteoMediaStation station) + protected void storeSunshineData(Map sunshineDurationPerHour, WeatherStation station) { for (Map.Entry entry : sunshineDurationPerHour.entrySet()) { @@ -94,7 +102,7 @@ protected void storeSunshineData(Map sunshineDurationPer try { - weatherDao.update(new WeatherForecastKey(station.name(), dateTime), forecast); + weatherDao.update(new WeatherForecastKey(station.name(), dateTime, getSource()), forecast); logger.debug("Stored weather data for {}: {}% sun probability", dateTime, Math.round(sunProbability * 100)); } catch(Exception e) diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java index 8d02de4..4f4761e 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java @@ -1,6 +1,7 @@ package de.hatoka.eos.forecast; -import de.hatoka.eos.persistence.capi.MeteoMediaStation; +import de.hatoka.eos.persistence.capi.WeatherStation; +import de.hatoka.eos.persistence.capi.WeatherDataSource; import jakarta.inject.Inject; import jakarta.inject.Singleton; @@ -30,7 +31,13 @@ public class MeteoMediaWeatherForecastImporter extends AbstractWeatherForecastIm MeteoMediaWeatherSunshineDurationConverter converter; @Override - protected Map downloadAndProcessWeatherData(MeteoMediaStation station, ZonedDateTime startDate) + protected WeatherDataSource getSource() + { + return WeatherDataSource.METEOMEDIA; + } + + @Override + protected Map downloadAndProcessWeatherData(WeatherStation station, ZonedDateTime startDate) throws IOException, InterruptedException { // Download the PNG image diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporter.java index 98777ec..03e07fc 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporter.java @@ -2,7 +2,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.hatoka.eos.forecast.openmeteo.OpenMeteoResponse; -import de.hatoka.eos.persistence.capi.MeteoMediaStation; +import de.hatoka.eos.persistence.capi.WeatherStation; +import de.hatoka.eos.persistence.capi.WeatherDataSource; import jakarta.inject.Singleton; import java.io.IOException; @@ -19,24 +20,31 @@ /** * Downloads weather forecast data from OpenMeteo API and processes it to extract sunshine duration data. - * + * * OpenMeteo API Documentation: https://open-meteo.com/en/docs - * - * Example API call: - * https://api.open-meteo.com/v1/forecast?latitude=51.0262&longitude=11.5164&hourly=sunshine_duration&forecast_days=3&timezone=UTC + * + * Example API call: https://api.open-meteo.com/v1/forecast?latitude=51.0262&longitude=11.5164&hourly=sunshine_duration&forecast_days=3&timezone=UTC */ @Singleton public class OpenMeteoWeatherForecastImporter extends AbstractWeatherForecastImporter { private static final String API_BASE_URL = "https://api.open-meteo.com/v1/forecast"; + private static final String API_QUERY = API_BASE_URL + "?latitude=%.4f&longitude=%.4f&hourly=sunshine_duration&forecast_days=%s&timezone=%s"; + private static final String FORECAST_DAYS = "3"; private static final String TIMEZONE = "UTC"; - + private final ObjectMapper objectMapper = new ObjectMapper(); @Override - protected Map downloadAndProcessWeatherData(MeteoMediaStation station, ZonedDateTime startDate) - throws IOException, InterruptedException + protected WeatherDataSource getSource() + { + return WeatherDataSource.OPENMETEO; + } + + @Override + protected Map downloadAndProcessWeatherData(WeatherStation station, ZonedDateTime startDate) + throws IOException, InterruptedException { // Build the OpenMeteo API URL String apiUrl = buildApiUrl(station); @@ -55,23 +63,18 @@ protected Map downloadAndProcessWeatherData(MeteoMediaSt /** * Builds the OpenMeteo API URL for the given station. - * + * * @param station the weather station * @return the complete API URL */ - private String buildApiUrl(MeteoMediaStation station) + private String buildApiUrl(WeatherStation station) { - return String.format("%s?latitude=%.4f&longitude=%.4f&hourly=sunshine_duration&forecast_days=%s&timezone=%s", - API_BASE_URL, - station.getLatitude(), - station.getLongitude(), - FORECAST_DAYS, - TIMEZONE); + return API_QUERY.formatted(station.getLatitude(), station.getLongitude(), FORECAST_DAYS, TIMEZONE); } /** * Downloads JSON data from the OpenMeteo API. - * + * * @param apiUrl the API URL to call * @return the JSON response as a string * @throws IOException if the HTTP request fails @@ -98,46 +101,44 @@ private String downloadJsonData(URI apiUrl) throws IOException, InterruptedExcep if (response.statusCode() != 200) { - throw new IOException("OpenMeteo API request failed with status code: " + response.statusCode() + - ", response: " + response.body()); + throw new IOException("OpenMeteo API request failed with status code: " + response.statusCode() + ", response: " + response.body()); } - + return response.body(); } } /** * Converts the OpenMeteo API response to our internal sunshine duration format. - * + * * @param response the OpenMeteo API response * @return map of datetime to sunshine duration in minutes */ private Map convertToSunshineDurationMap(OpenMeteoResponse response) { Map result = new HashMap<>(); - + if (response.getHourly() == null) { logger.warn("No hourly data in OpenMeteo response"); return result; } - + List times = response.getHourly().getTime(); List sunshineDurations = response.getHourly().getSunshineDuration(); - + if (times == null || sunshineDurations == null) { logger.warn("Missing time or sunshine_duration data in OpenMeteo response"); return result; } - + if (times.size() != sunshineDurations.size()) { - logger.warn("Time and sunshine_duration arrays have different sizes: {} vs {}", - times.size(), sunshineDurations.size()); + logger.warn("Time and sunshine_duration arrays have different sizes: {} vs {}", times.size(), sunshineDurations.size()); return result; } - + // Parse each time/sunshine pair for (int i = 0; i < times.size(); i++) { @@ -145,9 +146,10 @@ private Map convertToSunshineDurationMap(OpenMeteoRespon { String timeString = times.get(i); ZonedDateTime dateTime; - + // Handle OpenMeteo time format which might be with or without timezone - if (timeString.contains("T") && !timeString.contains("Z") && !timeString.contains("+") && !timeString.substring(timeString.indexOf("T")).contains("-")) + if (timeString.contains("T") && !timeString.contains("Z") && !timeString.contains("+") && !timeString.substring( + timeString.indexOf("T")).contains("-")) { // Format like "2025-11-05T15:00" - add UTC timezone dateTime = ZonedDateTime.parse(timeString + "Z", DateTimeFormatter.ISO_OFFSET_DATE_TIME); @@ -157,26 +159,27 @@ private Map convertToSunshineDurationMap(OpenMeteoRespon // Format with timezone information dateTime = ZonedDateTime.parse(timeString, DateTimeFormatter.ISO_OFFSET_DATE_TIME); } - + // Convert sunshine duration from seconds to minutes // OpenMeteo returns sunshine duration in seconds for the past hour Double sunshineSeconds = sunshineDurations.get(i); - int sunshineMinutes = sunshineSeconds != null ? (int) Math.round(sunshineSeconds / 60.0) : 0; - - // Ensure the value is within valid range (0-60 minutes) - sunshineMinutes = Math.max(0, Math.min(60, sunshineMinutes)); - + int sunshineMinutes = sunshineSeconds != null ? (int)Math.round(sunshineSeconds / 60.0) : 0; + + if (sunshineMinutes > 60) + { + throw new IllegalArgumentException("Sunshine minutes exceed 60: " + sunshineMinutes); + } + result.put(dateTime, sunshineMinutes); - + logger.trace("Parsed {}: {} minutes sunshine", dateTime, sunshineMinutes); } - catch (Exception e) + catch(Exception e) { - logger.warn("Failed to parse time entry at index {}: '{}', sunshine: {}", - i, times.get(i), sunshineDurations.get(i), e); + logger.warn("Failed to parse time entry at index {}: '{}', sunshine: {}", i, times.get(i), sunshineDurations.get(i), e); } } - + logger.info("Converted {} OpenMeteo data points to sunshine duration map", result.size()); return result; } diff --git a/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java index 213a924..7f267c5 100644 --- a/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java +++ b/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java @@ -1,6 +1,7 @@ package de.hatoka.eos.forecast; -import de.hatoka.eos.persistence.capi.MeteoMediaStation; +import de.hatoka.eos.persistence.capi.WeatherStation; +import de.hatoka.eos.persistence.capi.WeatherDataSource; import de.hatoka.eos.persistence.capi.WeatherForcastDAO; import de.hatoka.eos.persistence.capi.WeatherForecastKey; import de.hatoka.eos.persistence.capi.WeatherForecastPO; @@ -31,10 +32,10 @@ class MeteoMediaWeatherForecastImporterTest void testImportFromValidUrl() throws IOException, InterruptedException { // this importer can only import data of today and tomorrow - ZonedDateTime testDate = importer.importWeatherForecast(MeteoMediaStation.APOLDA); - WeatherForecastPO retrieved = weatherDao.get(WeatherForecastKey.valueOf(MeteoMediaStation.APOLDA, testDate.plusHours(1))); + ZonedDateTime testDate = importer.importWeatherForecast(WeatherStation.APOLDA); + WeatherForecastPO retrieved = weatherDao.get(WeatherForecastKey.valueOf(WeatherStation.APOLDA, testDate.plusHours(1), WeatherDataSource.METEOMEDIA)); assertNotNull(retrieved); assertNotNull(retrieved.getSunProbability()); - importer.importWeatherForecast(MeteoMediaStation.LEIPZIG_STADTWERKE); + importer.importWeatherForecast(WeatherStation.LEIPZIG_STADTWERKE); } } diff --git a/forecast/src/test/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporterTest.java index cb3c550..9061db4 100644 --- a/forecast/src/test/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporterTest.java +++ b/forecast/src/test/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporterTest.java @@ -1,6 +1,7 @@ package de.hatoka.eos.forecast; -import de.hatoka.eos.persistence.capi.MeteoMediaStation; +import de.hatoka.eos.persistence.capi.WeatherStation; +import de.hatoka.eos.persistence.capi.WeatherDataSource; import de.hatoka.eos.persistence.capi.WeatherForcastDAO; import de.hatoka.eos.persistence.capi.WeatherForecastKey; import de.hatoka.eos.persistence.capi.WeatherForecastPO; @@ -31,13 +32,13 @@ class OpenMeteoWeatherForecastImporterTest void testImportFromOpenMeteoApi() throws IOException, InterruptedException { // Test importing data from OpenMeteo API - ZonedDateTime testDate = importer.importWeatherForecast(MeteoMediaStation.APOLDA); + ZonedDateTime testDate = importer.importWeatherForecast(WeatherStation.APOLDA); // Verify that data was imported successfully assertNotNull(testDate, "Import should return a valid start date"); // Check if data was stored in the database (test the next hour) - WeatherForecastPO retrieved = weatherDao.get(WeatherForecastKey.valueOf(MeteoMediaStation.APOLDA, testDate.plusHours(1))); + WeatherForecastPO retrieved = weatherDao.get(WeatherForecastKey.valueOf(WeatherStation.APOLDA, testDate.plusHours(1), WeatherDataSource.OPENMETEO)); assertNotNull(retrieved, "Weather data should be stored in database"); assertNotNull(retrieved.getSunProbability(), "Sun probability should not be null"); @@ -47,24 +48,24 @@ void testImportFromOpenMeteoApi() throws IOException, InterruptedException "Sun probability should be between 0.0 and 1.0, but was: " + sunProb); // Test importing for a different station - ZonedDateTime testDate2 = importer.importWeatherForecast(MeteoMediaStation.LEIPZIG_STADTWERKE); + ZonedDateTime testDate2 = importer.importWeatherForecast(WeatherStation.LEIPZIG_STADTWERKE); assertNotNull(testDate2, "Import for Leipzig should also return a valid start date"); } @Test void testStationCoordinates() { - // Verify that our stations have valid coordinates for OpenMeteo API - assertNotEquals(0.0, MeteoMediaStation.APOLDA.getLatitude(), "APOLDA should have valid latitude"); - assertNotEquals(0.0, MeteoMediaStation.APOLDA.getLongitude(), "APOLDA should have valid longitude"); - - assertNotEquals(0.0, MeteoMediaStation.LEIPZIG_STADTWERKE.getLatitude(), "LEIPZIG_STADTWERKE should have valid latitude"); - assertNotEquals(0.0, MeteoMediaStation.LEIPZIG_STADTWERKE.getLongitude(), "LEIPZIG_STADTWERKE should have valid longitude"); - - // Check that coordinates are reasonable for Germany - assertTrue(MeteoMediaStation.APOLDA.getLatitude() > 47.0 && MeteoMediaStation.APOLDA.getLatitude() < 55.0, - "APOLDA latitude should be within Germany bounds"); - assertTrue(MeteoMediaStation.APOLDA.getLongitude() > 5.0 && MeteoMediaStation.APOLDA.getLongitude() < 15.0, - "APOLDA longitude should be within Germany bounds"); + // Test that all weather stations have valid coordinates + for (WeatherStation station : WeatherStation.values()) + { + assertTrue(station.getLatitude() >= -90.0 && station.getLatitude() <= 90.0, + "Station " + station.name() + " has invalid latitude: " + station.getLatitude()); + assertTrue(station.getLongitude() >= -180.0 && station.getLongitude() <= 180.0, + "Station " + station.name() + " has invalid longitude: " + station.getLongitude()); + assertNotNull(station.getStationNumber(), + "Station " + station.name() + " should have a station number"); + assertFalse(station.getStationNumber().isEmpty(), + "Station " + station.name() + " should have a non-empty station number"); + } } } \ No newline at end of file diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherDataSource.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherDataSource.java new file mode 100644 index 0000000..3d0bde8 --- /dev/null +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherDataSource.java @@ -0,0 +1,29 @@ +package de.hatoka.eos.persistence.capi; + +/** + * Enumeration of weather data sources. + */ +public enum WeatherDataSource +{ + METEOMEDIA("meteomedia"), + OPENMETEO("openmeteo"), + TEST("test"); + + private final String identifier; + + WeatherDataSource(String identifier) + { + this.identifier = identifier; + } + + public String getIdentifier() + { + return identifier; + } + + @Override + public String toString() + { + return identifier; + } +} \ No newline at end of file diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastKey.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastKey.java index 4bfca5f..efc8fcf 100644 --- a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastKey.java +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastKey.java @@ -2,10 +2,10 @@ import java.time.ZonedDateTime; -public record WeatherForecastKey(String station, ZonedDateTime time) +public record WeatherForecastKey(String station, ZonedDateTime time, WeatherDataSource source) { - public static WeatherForecastKey valueOf(MeteoMediaStation station, ZonedDateTime time) + public static WeatherForecastKey valueOf(WeatherStation station, ZonedDateTime time, WeatherDataSource source) { - return new WeatherForecastKey(station.name(), time); + return new WeatherForecastKey(station.name(), time, source); } } diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/MeteoMediaStation.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherStation.java similarity index 86% rename from persistence-api/src/main/java/de/hatoka/eos/persistence/capi/MeteoMediaStation.java rename to persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherStation.java index 88d8658..cd79478 100644 --- a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/MeteoMediaStation.java +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherStation.java @@ -3,7 +3,7 @@ /** * Weather stations with their MeteoMedia station numbers and geographic coordinates for OpenMeteo API. */ -public enum MeteoMediaStation +public enum WeatherStation { APOLDA("095550", 51.0262, 11.5164), LEIPZIG_STADTWERKE("104700", 51.3397, 12.3731); @@ -12,7 +12,7 @@ public enum MeteoMediaStation private final double latitude; private final double longitude; - MeteoMediaStation(String stationNumber, double latitude, double longitude) + WeatherStation(String stationNumber, double latitude, double longitude) { this.stationNumber = stationNumber; this.latitude = latitude; diff --git a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java index ce12e94..2050600 100644 --- a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java +++ b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java @@ -26,16 +26,18 @@ public class InfluxWeatherForecastDao implements WeatherForcastDAO { private static final String WEATHER_MEASUREMENT = "weather_forecast"; private static final String COLUMN_STATION = "station"; + private static final String COLUMN_SOURCE = "source"; public static final String GET_QUERY = """ from(bucket: "%s") |> range(start: %s, stop: %s) |> filter(fn: (r) => r["_measurement"] == "%s") |> filter(fn: (r) => r["station"] == "%s") + |> filter(fn: (r) => r["source"] == "%s") |> filter(fn: (r) => r["_field"] == "%s") |> last() """; public static final String DELETE_PREDICATE = """ - _measurement="%s" AND station="%s" + _measurement="%s" AND station="%s" AND source="%s" """; private final WriteApiBlocking writeApi; @@ -62,6 +64,7 @@ public void update(WeatherForecastKey key, WeatherForecastPO data) Point point = Point.measurement(WEATHER_MEASUREMENT) .time(key.time().toInstant(), WritePrecision.S) .addTag(COLUMN_STATION, key.station()) + .addTag(COLUMN_SOURCE, key.source().getIdentifier()) .addField(WeatherForecastPO.COLUMN_SUN_PROBABILITY, data.getSunProbability().value()); writeApi.writePoint(point); @@ -79,7 +82,7 @@ public void delete(WeatherForecastKey key) { // Delete data within a 1-minute window around the specified time deleteApi.delete(key.time().minusMinutes(1).toOffsetDateTime(), key.time().plusMinutes(1).toOffsetDateTime(), - DELETE_PREDICATE.formatted(WEATHER_MEASUREMENT, key.station()), bucketName, influxdbOrg); + DELETE_PREDICATE.formatted(WEATHER_MEASUREMENT, key.station(), key.source().getIdentifier()), bucketName, influxdbOrg); } catch(Exception e) { @@ -96,7 +99,7 @@ private String formatTimeForFlux(ZonedDateTime time) public WeatherForecastPO get(WeatherForecastKey key) { String flux = String.format(GET_QUERY, bucketName, formatTimeForFlux(key.time().minusMinutes(1)), - formatTimeForFlux(key.time().plusMinutes(1)), WEATHER_MEASUREMENT, key.station(), WeatherForecastPO.COLUMN_SUN_PROBABILITY); + formatTimeForFlux(key.time().plusMinutes(1)), WEATHER_MEASUREMENT, key.station(), key.source().getIdentifier(), WeatherForecastPO.COLUMN_SUN_PROBABILITY); for (FluxTable table : queryApi.query(flux)) { for (FluxRecord record : table.getRecords()) diff --git a/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java b/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java index b2c1a85..224408b 100644 --- a/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java +++ b/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java @@ -1,6 +1,7 @@ package de.hatoka.eos.persistence.influx; -import de.hatoka.eos.persistence.capi.MeteoMediaStation; +import de.hatoka.eos.persistence.capi.WeatherStation; +import de.hatoka.eos.persistence.capi.WeatherDataSource; import de.hatoka.eos.persistence.capi.WeatherForecastKey; import de.hatoka.eos.persistence.capi.WeatherForecastPO; import de.hatoka.eos.units.capi.Percentage; @@ -49,7 +50,7 @@ private static WeatherForecastPO createForecast(Percentage sunProbability) private static WeatherForecastKey getKey(ZonedDateTime time) { - return WeatherForecastKey.valueOf(MeteoMediaStation.APOLDA, time); + return WeatherForecastKey.valueOf(WeatherStation.APOLDA, time, WeatherDataSource.TEST); } @Test @@ -221,7 +222,7 @@ void shouldHandleDeleteOfNonExistentData() void shouldDeleteOnlyStation() { // Given - store data at multiple time points - WeatherForecastKey leipzigKey = WeatherForecastKey.valueOf(MeteoMediaStation.LEIPZIG_STADTWERKE, TEST_TIME); + WeatherForecastKey leipzigKey = WeatherForecastKey.valueOf(WeatherStation.LEIPZIG_STADTWERKE, TEST_TIME, WeatherDataSource.TEST); dao.update(getKey(TEST_TIME), createForecast(new Percentage(0.3))); dao.update(leipzigKey, createForecast(new Percentage(0.6))); From b26ebb573307050012dc03374106731b9b97bb92 Mon Sep 17 00:00:00 2001 From: Thomas Bergmann Date: Sun, 16 Nov 2025 14:48:18 +0100 Subject: [PATCH 07/16] rework: project devices to simulation --- forecast/README-forecast-import.md | 92 +++++++++++++++++++ forecast/run-forecast-import.sh | 18 ++++ .../forecast/ForecastImportApplication.java | 53 +++++++++++ .../MeteoMediaWeatherForecastImporter.java | 2 + .../src/main/resources/application.properties | 11 +-- optimization/build.gradle.kts | 2 +- .../capi/business/OptimizationGoal.java | 2 +- .../capi/business/OptimizationResult.java | 2 +- .../optimization/capi/business/Optimizer.java | 2 +- .../capi/goals/CarChargeGoal.java | 6 +- .../capi/goals/GridUsingGoal.java | 2 +- .../capi/goals/OptimizationGoals.java | 2 +- .../internal/business/OptimizerImpl.java | 8 +- .../OptimizationConfigurationLoader.java | 2 +- .../capi/goals/CarChargeGoalTest.java | 10 +- .../capi/goals/GridUsingGoalTest.java | 4 +- .../capi/goals/OptimizationGoalsTest.java | 10 +- .../internal/business/OptimizerTest.java | 4 +- settings.gradle | 2 +- {devices => simulation}/build.gradle.kts | 0 .../capi/business/config/CarUsageProfile.java | 2 +- .../capi/business/config/CsvPriceConfig.java | 2 +- .../capi/business/config/DeviceConfig.java | 12 +-- .../capi/business/config/FlatPriceConfig.java | 2 +- .../capi/business/config/GridConfig.java | 8 +- .../business/config/GridPriceConfigType.java | 2 +- .../config/ImportExportChargeConfig.java | 2 +- .../business/config/InstallationConfig.java | 2 +- .../business/config/SimulationConfig.java | 2 +- .../config/SimulationTimeSettings.java | 2 +- .../capi/business/device/Device.java | 8 +- .../capi/business/device/DeviceFactory.java | 4 +- .../capi/business/device/DeviceRef.java | 2 +- .../capi/business/device/DeviceState.java | 2 +- .../capi/business/device/DeviceType.java | 12 +-- .../forecast/EnergyPriceForecast.java | 2 +- .../capi/business/forecast/Forecasts.java | 6 +- .../business/forecast/WeatherForecast.java | 2 +- .../metrics/ForecastMetricsExporter.java | 6 +- .../metrics/SimulationMetricsExporter.java | 4 +- .../business/simulation/EnergySystem.java | 2 +- .../simulation/SimulationRequest.java | 10 +- .../business/simulation/SimulationResult.java | 7 +- .../business/simulation/SimulationStep.java | 4 +- .../simulation/SimulationStepResult.java | 4 +- .../capi/business/simulation/Simulator.java | 2 +- .../business/statistics/HourlyEfficiency.java | 2 +- .../statistics/SolarPanelStatistics.java | 2 +- .../SolarPanelStatisticsConfig.java | 2 +- .../business/config/ConfigurationLoader.java | 6 +- .../internal/business/devices/Battery.java | 17 ++-- .../business/devices/DeviceFactoryImpl.java | 14 ++- .../business/devices/ElectricCar.java | 16 ++-- .../internal/business/devices/Grid.java | 12 +-- .../internal/business/devices/NoisyUsage.java | 14 +-- .../internal/business/devices/SolarPanel.java | 16 ++-- .../forecast/CsvStockEnergyPriceProvider.java | 6 +- .../business/forecast/FlatPriceService.java | 6 +- .../business/forecast/FlatWeatherService.java | 4 +- .../forecast/InfluxWeatherForecast.java | 4 +- .../forecast/StockEnergyPriceConverter.java | 2 +- .../metrics/InfluxDBMetricsExporter.java | 18 ++-- .../business/simulation/Simulation.java | 14 +-- .../business/simulation/SimulatorImpl.java | 12 +-- .../statistics/CurvePanelStatistic.java | 4 +- .../statistics/SolarPanelStatisticsImpl.java | 8 +- .../SolarPanelStatisticsLoader.java | 4 +- .../src/main/resources/META-INF/beans.xml | 0 .../src/main/resources/application.properties | 0 .../internal/business/DateTooling.java | 2 +- .../ConfigurationDeviceBuilderTest.java | 10 +- .../config/ConfigurationLoaderTest.java | 12 +-- .../business/config/CsvPriceConfigTest.java | 12 +-- .../SolarPanelStatisticsLoaderTest.java | 8 +- .../business/devices/BatteryTest.java | 14 +-- .../business/devices/ElectricCarTest.java | 18 ++-- .../internal/business/devices/GridTest.java | 12 +-- .../business/devices/NoisyUsageTest.java | 14 +-- .../business/devices/SolarPanelTest.java | 14 +-- .../forecast/FlatWeatherServiceTest.java | 6 +- .../StockEnergyPriceConverterTest.java | 2 +- .../metrics/PriceForecastExporterTest.java | 12 +-- .../SimulationMetricsExporterTest.java | 24 ++--- .../business/simulation/SimulationTest.java | 32 +++---- .../statistics/CurvePanelStatisticTest.java | 4 +- .../src/test/resources/application.properties | 0 .../resources/solar-panel-statistics.yaml | 0 .../stockprices_DE_LU_week_32-33.csv | 0 .../stockprices_DE_LU_week_32_2025.svg | 0 .../stockprices_DE_LU_week_33_2025.svg | 0 .../test-installation-100percent.yaml | 0 .../test-installation-for-csv-prices.yaml | 0 .../test-installation-for-grafana.yaml | 0 .../resources/test-installation-with-car.yaml | 0 .../test-installation-without-car.yaml | 0 .../resources/test-simulation-summer.yaml | 0 .../test-simulation-with-csv-prices.yaml | 0 97 files changed, 443 insertions(+), 283 deletions(-) create mode 100644 forecast/README-forecast-import.md create mode 100755 forecast/run-forecast-import.sh create mode 100644 forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java rename {devices => simulation}/build.gradle.kts (100%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/config/CarUsageProfile.java (96%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/config/CsvPriceConfig.java (95%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/config/DeviceConfig.java (95%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/config/FlatPriceConfig.java (94%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/config/GridConfig.java (80%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/config/GridPriceConfigType.java (68%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/config/ImportExportChargeConfig.java (92%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/config/InstallationConfig.java (94%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/config/SimulationConfig.java (91%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/config/SimulationTimeSettings.java (97%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/device/Device.java (52%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/device/DeviceFactory.java (81%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/device/DeviceRef.java (50%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/device/DeviceState.java (93%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/device/DeviceType.java (63%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/forecast/EnergyPriceForecast.java (92%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/forecast/Forecasts.java (61%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/forecast/WeatherForecast.java (91%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/metrics/ForecastMetricsExporter.java (77%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/metrics/SimulationMetricsExporter.java (72%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/simulation/EnergySystem.java (98%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/simulation/SimulationRequest.java (58%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/simulation/SimulationResult.java (66%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/simulation/SimulationStep.java (86%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/simulation/SimulationStepResult.java (88%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/simulation/Simulator.java (82%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/statistics/HourlyEfficiency.java (94%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/statistics/SolarPanelStatistics.java (74%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/capi/business/statistics/SolarPanelStatisticsConfig.java (91%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/config/ConfigurationLoader.java (86%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/devices/Battery.java (92%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/devices/DeviceFactoryImpl.java (76%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/devices/ElectricCar.java (83%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/devices/Grid.java (79%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/devices/NoisyUsage.java (65%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/devices/SolarPanel.java (68%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/forecast/CsvStockEnergyPriceProvider.java (94%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/forecast/FlatPriceService.java (79%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/forecast/FlatWeatherService.java (87%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/forecast/InfluxWeatherForecast.java (86%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/forecast/StockEnergyPriceConverter.java (99%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/metrics/InfluxDBMetricsExporter.java (93%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/simulation/Simulation.java (87%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/simulation/SimulatorImpl.java (50%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/statistics/CurvePanelStatistic.java (92%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/statistics/SolarPanelStatisticsImpl.java (83%) rename {devices/src/main/java/de/hatoka/eos/devices => simulation/src/main/java/de/hatoka/eos/simulation}/internal/business/statistics/SolarPanelStatisticsLoader.java (83%) rename {devices => simulation}/src/main/resources/META-INF/beans.xml (100%) rename {devices => simulation}/src/main/resources/application.properties (100%) rename {devices/src/test/java/de/hatoka/eos/devices => simulation/src/test/java/de/hatoka/eos/simulation}/internal/business/DateTooling.java (93%) rename {devices/src/test/java/de/hatoka/eos/devices => simulation/src/test/java/de/hatoka/eos/simulation}/internal/business/config/ConfigurationDeviceBuilderTest.java (74%) rename {devices/src/test/java/de/hatoka/eos/devices => simulation/src/test/java/de/hatoka/eos/simulation}/internal/business/config/ConfigurationLoaderTest.java (90%) rename {devices/src/test/java/de/hatoka/eos/devices => simulation/src/test/java/de/hatoka/eos/simulation}/internal/business/config/CsvPriceConfigTest.java (86%) rename {devices/src/test/java/de/hatoka/eos/devices => simulation/src/test/java/de/hatoka/eos/simulation}/internal/business/config/SolarPanelStatisticsLoaderTest.java (87%) rename {devices/src/test/java/de/hatoka/eos/devices => simulation/src/test/java/de/hatoka/eos/simulation}/internal/business/devices/BatteryTest.java (97%) rename {devices/src/test/java/de/hatoka/eos/devices => simulation/src/test/java/de/hatoka/eos/simulation}/internal/business/devices/ElectricCarTest.java (96%) rename {devices/src/test/java/de/hatoka/eos/devices => simulation/src/test/java/de/hatoka/eos/simulation}/internal/business/devices/GridTest.java (91%) rename {devices/src/test/java/de/hatoka/eos/devices => simulation/src/test/java/de/hatoka/eos/simulation}/internal/business/devices/NoisyUsageTest.java (81%) rename {devices/src/test/java/de/hatoka/eos/devices => simulation/src/test/java/de/hatoka/eos/simulation}/internal/business/devices/SolarPanelTest.java (90%) rename {devices/src/test/java/de/hatoka/eos/devices => simulation/src/test/java/de/hatoka/eos/simulation}/internal/business/forecast/FlatWeatherServiceTest.java (92%) rename {devices/src/test/java/de/hatoka/eos/devices => simulation/src/test/java/de/hatoka/eos/simulation}/internal/business/forecast/StockEnergyPriceConverterTest.java (99%) rename {devices/src/test/java/de/hatoka/eos/devices => simulation/src/test/java/de/hatoka/eos/simulation}/internal/business/metrics/PriceForecastExporterTest.java (82%) rename {devices/src/test/java/de/hatoka/eos/devices => simulation/src/test/java/de/hatoka/eos/simulation}/internal/business/metrics/SimulationMetricsExporterTest.java (82%) rename {devices/src/test/java/de/hatoka/eos/devices => simulation/src/test/java/de/hatoka/eos/simulation}/internal/business/simulation/SimulationTest.java (94%) rename {devices/src/test/java/de/hatoka/eos/devices => simulation/src/test/java/de/hatoka/eos/simulation}/internal/business/statistics/CurvePanelStatisticTest.java (94%) rename {devices => simulation}/src/test/resources/application.properties (100%) rename {devices => simulation}/src/test/resources/solar-panel-statistics.yaml (100%) rename {devices => simulation}/src/test/resources/stockprices_DE_LU_week_32-33.csv (100%) rename {devices => simulation}/src/test/resources/stockprices_DE_LU_week_32_2025.svg (100%) rename {devices => simulation}/src/test/resources/stockprices_DE_LU_week_33_2025.svg (100%) rename {devices => simulation}/src/test/resources/test-installation-100percent.yaml (100%) rename {devices => simulation}/src/test/resources/test-installation-for-csv-prices.yaml (100%) rename {devices => simulation}/src/test/resources/test-installation-for-grafana.yaml (100%) rename {devices => simulation}/src/test/resources/test-installation-with-car.yaml (100%) rename {devices => simulation}/src/test/resources/test-installation-without-car.yaml (100%) rename {devices => simulation}/src/test/resources/test-simulation-summer.yaml (100%) rename {devices => simulation}/src/test/resources/test-simulation-with-csv-prices.yaml (100%) diff --git a/forecast/README-forecast-import.md b/forecast/README-forecast-import.md new file mode 100644 index 0000000..78000c5 --- /dev/null +++ b/forecast/README-forecast-import.md @@ -0,0 +1,92 @@ +# Forecast Import Application + +This Quarkus command-line application imports weather forecast data from multiple sources into InfluxDB. + +## Features + +- **MeteoMedia Importer**: Downloads weather forecast PNG images and extracts sunshine duration data (covers today and tomorrow) +- **OpenMeteo Importer**: Fetches weather forecast data from the OpenMeteo REST API (covers up to 3 days) + +## Prerequisites + +1. **InfluxDB** must be running and accessible at `http://localhost:8086` + - You can start it using Docker: `docker-compose up -d influxdb` + - Or configure a different URL in `src/main/resources/application.properties` + +2. **Java 17+** must be installed + +## Building + +```bash +# Build the application +./gradlew :forecast:quarkusBuild + +# Or build everything +./gradlew build +``` + +## Running + +### Option 1: Using the convenience script +```bash +cd forecast +./run-forecast-import.sh +``` + +### Option 2: Direct Java execution +```bash +cd forecast +java -jar build/quarkus-app/quarkus-run.jar +``` + +### Option 3: Using Gradle +```bash +./gradlew :forecast:quarkusRun +``` + +## Configuration + +Configuration is in `forecast/src/main/resources/application.properties`: + +```properties +# InfluxDB Configuration +eos.influxdb.url=http://localhost:8086 +eos.influxdb.token=eos-token-12345678901234567890 +eos.influxdb.org=eos +eos.influxdb.bucket=forecast + +# Quarkus Configuration +quarkus.log.level=INFO +quarkus.log.category."de.hatoka.eos".level=DEBUG +``` + +## Weather Stations + +The application imports data for these weather stations: +- **APOLDA**: MeteoMedia station +- **LEIPZIG_STADTWERKE**: MeteoMedia station + +Both stations are imported from both MeteoMedia and OpenMeteo sources. + +## Output + +The application: +1. Downloads weather forecast data from MeteoMedia (PNG images) +2. Downloads weather forecast data from OpenMeteo (JSON via REST API) +3. Converts the data to sunshine duration (minutes per hour) +4. Stores the data in InfluxDB under the configured bucket +5. Exits with code 0 on success, 1 on failure + +## Logs + +Logs are written to stdout with DEBUG level for the `de.hatoka.eos` package. + +## Development + +To run in development mode with live reload: +```bash +./gradlew :forecast:quarkusDev +``` + +Press `q` to quit dev mode. + diff --git a/forecast/run-forecast-import.sh b/forecast/run-forecast-import.sh new file mode 100755 index 0000000..9e89052 --- /dev/null +++ b/forecast/run-forecast-import.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Script to run the ForecastImportApplication + +cd "$(dirname "$0")" + +# Check if InfluxDB is running +if ! curl -s http://localhost:8086/health > /dev/null 2>&1; then + echo "WARNING: InfluxDB does not appear to be running at http://localhost:8086" + echo "You can start it with: docker-compose up -d influxdb" + echo "Continuing anyway..." +fi + +echo "Starting Weather Forecast Import..." +java -jar build/quarkus-app/quarkus-run.jar + +echo "" +echo "Forecast import completed with exit code: $?" + diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java b/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java new file mode 100644 index 0000000..c24b6bc --- /dev/null +++ b/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java @@ -0,0 +1,53 @@ +package de.hatoka.eos.forecast; + +import de.hatoka.eos.persistence.capi.WeatherStation; +import io.quarkus.runtime.Quarkus; +import io.quarkus.runtime.QuarkusApplication; +import io.quarkus.runtime.annotations.QuarkusMain; +import jakarta.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@QuarkusMain +public class ForecastImportApplication implements QuarkusApplication +{ + private static final Logger logger = LoggerFactory.getLogger(ForecastImportApplication.class); + + @Inject + MeteoMediaWeatherForecastImporter meteoMediaImporter; + + @Inject + OpenMeteoWeatherForecastImporter openMeteoImporter; + + public static void main(String[] args) + { + Quarkus.run(ForecastImportApplication.class, args); + } + + @Override + public int run(String... args) throws Exception + { + logger.info("Starting weather forecast import..."); + try + { + // Import from MeteoMedia (can only import data of today and tomorrow) + logger.debug("Importing from MeteoMedia..."); + meteoMediaImporter.importWeatherForecast(WeatherStation.APOLDA); + meteoMediaImporter.importWeatherForecast(WeatherStation.LEIPZIG_STADTWERKE); + logger.info("Import from MeteoMedia finished."); + + // Import from OpenMeteo (can import up to 3 days) + logger.debug("Importing from OpenMeteo..."); + openMeteoImporter.importWeatherForecast(WeatherStation.APOLDA); + openMeteoImporter.importWeatherForecast(WeatherStation.LEIPZIG_STADTWERKE); + logger.info("Import from OpenMeteo finished."); + + return 0; + } + catch (Exception e) + { + logger.error("Weather forecast import failed", e); + return 1; + } + } +} diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java index 4f4761e..49559e8 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java @@ -1,5 +1,7 @@ package de.hatoka.eos.forecast; +import de.hatoka.eos.persistence.capi.WeatherForecastKey; +import de.hatoka.eos.persistence.capi.WeatherForecastPO; import de.hatoka.eos.persistence.capi.WeatherStation; import de.hatoka.eos.persistence.capi.WeatherDataSource; import jakarta.inject.Inject; diff --git a/forecast/src/main/resources/application.properties b/forecast/src/main/resources/application.properties index c260bcc..b4466ee 100644 --- a/forecast/src/main/resources/application.properties +++ b/forecast/src/main/resources/application.properties @@ -1,11 +1,10 @@ quarkus.profile=production -# InfluxDB Metrics Configuration -eos.metrics.influxdb.enabled=true -eos.metrics.influxdb.url=http://localhost:8086 -eos.metrics.influxdb.token=eos-token-12345678901234567890 -eos.metrics.influxdb.org=eos -eos.metrics.influxdb.bucket=energy_simulation +# InfluxDB Configuration +eos.influxdb.url=http://localhost:8086 +eos.influxdb.token=eos-token-12345678901234567890 +eos.influxdb.org=eos +eos.influxdb.bucket=forecast # Quarkus Configuration quarkus.log.level=INFO diff --git a/optimization/build.gradle.kts b/optimization/build.gradle.kts index 3c2f92e..c884ae3 100644 --- a/optimization/build.gradle.kts +++ b/optimization/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } dependencies { - implementation(project(":devices")) + implementation(project(":simulation")) implementation(project(":units")) implementation("org.slf4j:slf4j-api") diff --git a/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/OptimizationGoal.java b/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/OptimizationGoal.java index 4fcb9e5..150fa42 100644 --- a/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/OptimizationGoal.java +++ b/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/OptimizationGoal.java @@ -1,6 +1,6 @@ package de.hatoka.eos.optimization.capi.business; -import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; import de.hatoka.eos.units.capi.Money; /** diff --git a/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/OptimizationResult.java b/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/OptimizationResult.java index 674f35f..b7ad246 100644 --- a/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/OptimizationResult.java +++ b/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/OptimizationResult.java @@ -1,6 +1,6 @@ package de.hatoka.eos.optimization.capi.business; -import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; import de.hatoka.eos.units.capi.Money; /** diff --git a/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/Optimizer.java b/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/Optimizer.java index cfaf86d..89ce7f8 100644 --- a/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/Optimizer.java +++ b/optimization/src/main/java/de/hatoka/eos/optimization/capi/business/Optimizer.java @@ -1,6 +1,6 @@ package de.hatoka.eos.optimization.capi.business; -import de.hatoka.eos.devices.capi.business.config.InstallationConfig; +import de.hatoka.eos.simulation.capi.business.config.InstallationConfig; import de.hatoka.eos.optimization.capi.goals.OptimizationGoals; /** diff --git a/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/CarChargeGoal.java b/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/CarChargeGoal.java index af508c2..2bfd137 100644 --- a/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/CarChargeGoal.java +++ b/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/CarChargeGoal.java @@ -1,9 +1,9 @@ package de.hatoka.eos.optimization.capi.goals; import com.fasterxml.jackson.annotation.JsonProperty; -import de.hatoka.eos.devices.capi.business.device.DeviceState; -import de.hatoka.eos.devices.capi.business.device.DeviceType; -import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.device.DeviceType; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; import de.hatoka.eos.units.capi.Money; import de.hatoka.eos.units.capi.Percentage; import de.hatoka.eos.optimization.capi.business.OptimizationGoal; diff --git a/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/GridUsingGoal.java b/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/GridUsingGoal.java index eefac37..b5c9d87 100644 --- a/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/GridUsingGoal.java +++ b/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/GridUsingGoal.java @@ -1,7 +1,7 @@ package de.hatoka.eos.optimization.capi.goals; import com.fasterxml.jackson.annotation.JsonProperty; -import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; import de.hatoka.eos.units.capi.Money; import de.hatoka.eos.optimization.capi.business.OptimizationGoal; diff --git a/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/OptimizationGoals.java b/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/OptimizationGoals.java index 11dba6c..1c2c8cb 100644 --- a/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/OptimizationGoals.java +++ b/optimization/src/main/java/de/hatoka/eos/optimization/capi/goals/OptimizationGoals.java @@ -1,7 +1,7 @@ package de.hatoka.eos.optimization.capi.goals; import com.fasterxml.jackson.annotation.JsonProperty; -import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; import de.hatoka.eos.units.capi.Money; import de.hatoka.eos.optimization.capi.business.OptimizationGoal; diff --git a/optimization/src/main/java/de/hatoka/eos/optimization/internal/business/OptimizerImpl.java b/optimization/src/main/java/de/hatoka/eos/optimization/internal/business/OptimizerImpl.java index eee3c03..3ee814c 100644 --- a/optimization/src/main/java/de/hatoka/eos/optimization/internal/business/OptimizerImpl.java +++ b/optimization/src/main/java/de/hatoka/eos/optimization/internal/business/OptimizerImpl.java @@ -1,9 +1,9 @@ package de.hatoka.eos.optimization.internal.business; -import de.hatoka.eos.devices.capi.business.config.InstallationConfig; -import de.hatoka.eos.devices.capi.business.device.DeviceFactory; -import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; -import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; +import de.hatoka.eos.simulation.capi.business.config.InstallationConfig; +import de.hatoka.eos.simulation.capi.business.device.DeviceFactory; +import de.hatoka.eos.simulation.capi.business.simulation.EnergySystem; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; import de.hatoka.eos.optimization.capi.business.OptimizationResult; import de.hatoka.eos.optimization.capi.business.Optimizer; import de.hatoka.eos.optimization.capi.goals.OptimizationGoals; diff --git a/optimization/src/main/java/de/hatoka/eos/optimization/internal/business/config/OptimizationConfigurationLoader.java b/optimization/src/main/java/de/hatoka/eos/optimization/internal/business/config/OptimizationConfigurationLoader.java index cae69d3..9887c16 100644 --- a/optimization/src/main/java/de/hatoka/eos/optimization/internal/business/config/OptimizationConfigurationLoader.java +++ b/optimization/src/main/java/de/hatoka/eos/optimization/internal/business/config/OptimizationConfigurationLoader.java @@ -1,6 +1,6 @@ package de.hatoka.eos.optimization.internal.business.config; -import de.hatoka.eos.devices.internal.business.config.ConfigurationLoader; +import de.hatoka.eos.simulation.internal.business.config.ConfigurationLoader; import de.hatoka.eos.optimization.capi.goals.OptimizationGoals; import jakarta.inject.Inject; import jakarta.inject.Singleton; diff --git a/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/CarChargeGoalTest.java b/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/CarChargeGoalTest.java index 8b7c4ae..1abd4c6 100644 --- a/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/CarChargeGoalTest.java +++ b/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/CarChargeGoalTest.java @@ -1,10 +1,10 @@ package de.hatoka.eos.optimization.capi.goals; -import de.hatoka.eos.devices.capi.business.device.DeviceRef; -import de.hatoka.eos.devices.capi.business.device.DeviceState; -import de.hatoka.eos.devices.capi.business.device.DeviceType; -import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; -import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; +import de.hatoka.eos.simulation.capi.business.device.DeviceRef; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.device.DeviceType; +import de.hatoka.eos.simulation.capi.business.simulation.EnergySystem; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; import de.hatoka.eos.units.capi.Energy; import de.hatoka.eos.units.capi.Money; import de.hatoka.eos.units.capi.Percentage; diff --git a/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/GridUsingGoalTest.java b/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/GridUsingGoalTest.java index f015550..f5cdd4f 100644 --- a/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/GridUsingGoalTest.java +++ b/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/GridUsingGoalTest.java @@ -1,7 +1,7 @@ package de.hatoka.eos.optimization.capi.goals; -import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; -import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; +import de.hatoka.eos.simulation.capi.business.simulation.EnergySystem; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; import de.hatoka.eos.units.capi.Energy; import de.hatoka.eos.units.capi.Money; import org.junit.jupiter.api.Test; diff --git a/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/OptimizationGoalsTest.java b/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/OptimizationGoalsTest.java index f492a57..d901d0d 100644 --- a/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/OptimizationGoalsTest.java +++ b/optimization/src/test/java/de/hatoka/eos/optimization/capi/goals/OptimizationGoalsTest.java @@ -1,10 +1,10 @@ package de.hatoka.eos.optimization.capi.goals; -import de.hatoka.eos.devices.capi.business.device.DeviceRef; -import de.hatoka.eos.devices.capi.business.device.DeviceState; -import de.hatoka.eos.devices.capi.business.device.DeviceType; -import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; -import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; +import de.hatoka.eos.simulation.capi.business.device.DeviceRef; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.device.DeviceType; +import de.hatoka.eos.simulation.capi.business.simulation.EnergySystem; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; import de.hatoka.eos.units.capi.Energy; import de.hatoka.eos.units.capi.Money; import de.hatoka.eos.units.capi.Percentage; diff --git a/optimization/src/test/java/de/hatoka/eos/optimization/internal/business/OptimizerTest.java b/optimization/src/test/java/de/hatoka/eos/optimization/internal/business/OptimizerTest.java index 385ee0b..d51d938 100644 --- a/optimization/src/test/java/de/hatoka/eos/optimization/internal/business/OptimizerTest.java +++ b/optimization/src/test/java/de/hatoka/eos/optimization/internal/business/OptimizerTest.java @@ -1,8 +1,8 @@ package de.hatoka.eos.optimization.internal.business; -import de.hatoka.eos.devices.capi.business.config.InstallationConfig; +import de.hatoka.eos.simulation.capi.business.config.InstallationConfig; import de.hatoka.eos.units.capi.Money; -import de.hatoka.eos.devices.internal.business.config.ConfigurationLoader; +import de.hatoka.eos.simulation.internal.business.config.ConfigurationLoader; import de.hatoka.eos.optimization.capi.business.OptimizationResult; import de.hatoka.eos.optimization.capi.business.Optimizer; import de.hatoka.eos.optimization.capi.goals.OptimizationGoals; diff --git a/settings.gradle b/settings.gradle index f07b756..cc3f717 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,7 +16,7 @@ include 'versions-test' include 'units' include 'persistence-api' include 'persistence-influx' -include 'devices' +include 'simulation' include 'optimization' include 'forecast' // include 'service-eos' diff --git a/devices/build.gradle.kts b/simulation/build.gradle.kts similarity index 100% rename from devices/build.gradle.kts rename to simulation/build.gradle.kts diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/CarUsageProfile.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/CarUsageProfile.java similarity index 96% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/config/CarUsageProfile.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/CarUsageProfile.java index 6cb4711..50ccae2 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/CarUsageProfile.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/CarUsageProfile.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.config; +package de.hatoka.eos.simulation.capi.business.config; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/CsvPriceConfig.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/CsvPriceConfig.java similarity index 95% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/config/CsvPriceConfig.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/CsvPriceConfig.java index 5919d11..a9c1d27 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/CsvPriceConfig.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/CsvPriceConfig.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.config; +package de.hatoka.eos.simulation.capi.business.config; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/DeviceConfig.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/DeviceConfig.java similarity index 95% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/config/DeviceConfig.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/DeviceConfig.java index f3a3d01..591c333 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/DeviceConfig.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/DeviceConfig.java @@ -1,19 +1,19 @@ -package de.hatoka.eos.devices.capi.business.config; +package de.hatoka.eos.simulation.capi.business.config; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import de.hatoka.eos.devices.capi.business.device.DeviceType; -import de.hatoka.eos.devices.capi.business.statistics.SolarPanelStatistics; +import de.hatoka.eos.simulation.capi.business.device.DeviceType; +import de.hatoka.eos.simulation.capi.business.statistics.SolarPanelStatistics; import de.hatoka.eos.units.capi.Energy; import de.hatoka.eos.units.capi.Percentage; import de.hatoka.eos.units.capi.PercentageJsonConverter; import de.hatoka.eos.units.capi.Power; -import de.hatoka.eos.devices.internal.business.statistics.CurvePanelStatistic; -import de.hatoka.eos.devices.internal.business.statistics.SolarPanelStatisticsImpl; -import de.hatoka.eos.devices.internal.business.statistics.SolarPanelStatisticsLoader; +import de.hatoka.eos.simulation.internal.business.statistics.CurvePanelStatistic; +import de.hatoka.eos.simulation.internal.business.statistics.SolarPanelStatisticsImpl; +import de.hatoka.eos.simulation.internal.business.statistics.SolarPanelStatisticsLoader; import java.io.IOException; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/FlatPriceConfig.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/FlatPriceConfig.java similarity index 94% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/config/FlatPriceConfig.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/FlatPriceConfig.java index fb6becb..398b227 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/FlatPriceConfig.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/FlatPriceConfig.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.config; +package de.hatoka.eos.simulation.capi.business.config; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/GridConfig.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridConfig.java similarity index 80% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/config/GridConfig.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridConfig.java index 954ee6a..1952443 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/GridConfig.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridConfig.java @@ -1,11 +1,11 @@ -package de.hatoka.eos.devices.capi.business.config; +package de.hatoka.eos.simulation.capi.business.config; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; -import de.hatoka.eos.devices.capi.business.forecast.EnergyPriceForecast; -import de.hatoka.eos.devices.internal.business.forecast.FlatPriceService; -import de.hatoka.eos.devices.internal.business.forecast.CsvStockEnergyPriceProvider; +import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; +import de.hatoka.eos.simulation.internal.business.forecast.FlatPriceService; +import de.hatoka.eos.simulation.internal.business.forecast.CsvStockEnergyPriceProvider; /** * The GridConfig is the container for grid pricing configuration. The type can be CSV or FLAT and selects the related configuration. diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/GridPriceConfigType.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridPriceConfigType.java similarity index 68% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/config/GridPriceConfigType.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridPriceConfigType.java index f8717ba..5b35092 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/GridPriceConfigType.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridPriceConfigType.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.config; +package de.hatoka.eos.simulation.capi.business.config; /** * The GridPriceConfigType defines how prices are imported diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/ImportExportChargeConfig.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/ImportExportChargeConfig.java similarity index 92% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/config/ImportExportChargeConfig.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/ImportExportChargeConfig.java index d8e93cb..c40f3ae 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/ImportExportChargeConfig.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/ImportExportChargeConfig.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.config; +package de.hatoka.eos.simulation.capi.business.config; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/InstallationConfig.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/InstallationConfig.java similarity index 94% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/config/InstallationConfig.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/InstallationConfig.java index 3d86b11..89608c5 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/InstallationConfig.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/InstallationConfig.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.config; +package de.hatoka.eos.simulation.capi.business.config; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/SimulationConfig.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/SimulationConfig.java similarity index 91% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/config/SimulationConfig.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/SimulationConfig.java index 33efa85..e2de806 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/SimulationConfig.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/SimulationConfig.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.config; +package de.hatoka.eos.simulation.capi.business.config; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/SimulationTimeSettings.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/SimulationTimeSettings.java similarity index 97% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/config/SimulationTimeSettings.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/SimulationTimeSettings.java index 0bf8378..9b72bb7 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/config/SimulationTimeSettings.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/SimulationTimeSettings.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.config; +package de.hatoka.eos.simulation.capi.business.config; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/device/Device.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/device/Device.java similarity index 52% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/device/Device.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/device/Device.java index 0cbfc9a..5e1f4d8 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/device/Device.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/device/Device.java @@ -1,8 +1,8 @@ -package de.hatoka.eos.devices.capi.business.device; +package de.hatoka.eos.simulation.capi.business.device; -import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; +import de.hatoka.eos.simulation.capi.business.simulation.EnergySystem; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStepResult; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStep; /** * Business object for import job management focused on the importing behavior diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/device/DeviceFactory.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/device/DeviceFactory.java similarity index 81% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/device/DeviceFactory.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/device/DeviceFactory.java index 9efd4ff..f69fe07 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/device/DeviceFactory.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/device/DeviceFactory.java @@ -1,6 +1,6 @@ -package de.hatoka.eos.devices.capi.business.device; +package de.hatoka.eos.simulation.capi.business.device; -import de.hatoka.eos.devices.capi.business.config.DeviceConfig; +import de.hatoka.eos.simulation.capi.business.config.DeviceConfig; import java.util.HashMap; import java.util.List; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/device/DeviceRef.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/device/DeviceRef.java similarity index 50% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/device/DeviceRef.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/device/DeviceRef.java index 1408f10..1d1da8f 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/device/DeviceRef.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/device/DeviceRef.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.device; +package de.hatoka.eos.simulation.capi.business.device; public record DeviceRef(DeviceType type, String id) { diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/device/DeviceState.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/device/DeviceState.java similarity index 93% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/device/DeviceState.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/device/DeviceState.java index 81bbccf..29ff991 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/device/DeviceState.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/device/DeviceState.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.device; +package de.hatoka.eos.simulation.capi.business.device; import de.hatoka.eos.units.capi.Energy; import de.hatoka.eos.units.capi.Percentage; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/device/DeviceType.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/device/DeviceType.java similarity index 63% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/device/DeviceType.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/device/DeviceType.java index 5781a86..697b4f4 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/device/DeviceType.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/device/DeviceType.java @@ -1,10 +1,10 @@ -package de.hatoka.eos.devices.capi.business.device; +package de.hatoka.eos.simulation.capi.business.device; -import de.hatoka.eos.devices.internal.business.devices.Battery; -import de.hatoka.eos.devices.internal.business.devices.ElectricCar; -import de.hatoka.eos.devices.internal.business.devices.Grid; -import de.hatoka.eos.devices.internal.business.devices.NoisyUsage; -import de.hatoka.eos.devices.internal.business.devices.SolarPanel; +import de.hatoka.eos.simulation.internal.business.devices.Battery; +import de.hatoka.eos.simulation.internal.business.devices.ElectricCar; +import de.hatoka.eos.simulation.internal.business.devices.Grid; +import de.hatoka.eos.simulation.internal.business.devices.NoisyUsage; +import de.hatoka.eos.simulation.internal.business.devices.SolarPanel; public enum DeviceType { diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/EnergyPriceForecast.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/forecast/EnergyPriceForecast.java similarity index 92% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/EnergyPriceForecast.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/forecast/EnergyPriceForecast.java index d427538..9e09d31 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/EnergyPriceForecast.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/forecast/EnergyPriceForecast.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.forecast; +package de.hatoka.eos.simulation.capi.business.forecast; import de.hatoka.eos.units.capi.Money; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/Forecasts.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/forecast/Forecasts.java similarity index 61% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/Forecasts.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/forecast/Forecasts.java index db7f0c1..70e96e2 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/Forecasts.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/forecast/Forecasts.java @@ -1,7 +1,7 @@ -package de.hatoka.eos.devices.capi.business.forecast; +package de.hatoka.eos.simulation.capi.business.forecast; -import de.hatoka.eos.devices.internal.business.forecast.FlatPriceService; -import de.hatoka.eos.devices.internal.business.forecast.FlatWeatherService; +import de.hatoka.eos.simulation.internal.business.forecast.FlatPriceService; +import de.hatoka.eos.simulation.internal.business.forecast.FlatWeatherService; /** * Collection of all forecasts diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/WeatherForecast.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/forecast/WeatherForecast.java similarity index 91% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/WeatherForecast.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/forecast/WeatherForecast.java index 5d37136..cac234f 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/forecast/WeatherForecast.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/forecast/WeatherForecast.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.forecast; +package de.hatoka.eos.simulation.capi.business.forecast; import de.hatoka.eos.units.capi.Percentage; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/metrics/ForecastMetricsExporter.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/metrics/ForecastMetricsExporter.java similarity index 77% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/metrics/ForecastMetricsExporter.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/metrics/ForecastMetricsExporter.java index d45de9c..c32d56e 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/metrics/ForecastMetricsExporter.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/metrics/ForecastMetricsExporter.java @@ -1,7 +1,7 @@ -package de.hatoka.eos.devices.capi.business.metrics; +package de.hatoka.eos.simulation.capi.business.metrics; -import de.hatoka.eos.devices.capi.business.forecast.EnergyPriceForecast; -import de.hatoka.eos.devices.capi.business.forecast.WeatherForecast; +import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; +import de.hatoka.eos.simulation.capi.business.forecast.WeatherForecast; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/metrics/SimulationMetricsExporter.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/metrics/SimulationMetricsExporter.java similarity index 72% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/metrics/SimulationMetricsExporter.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/metrics/SimulationMetricsExporter.java index b67e802..737d3c2 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/metrics/SimulationMetricsExporter.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/metrics/SimulationMetricsExporter.java @@ -1,6 +1,6 @@ -package de.hatoka.eos.devices.capi.business.metrics; +package de.hatoka.eos.simulation.capi.business.metrics; -import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; /** * Interface for exporting simulation metrics to time-series databases like InfluxDB. diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/EnergySystem.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/EnergySystem.java similarity index 98% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/EnergySystem.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/EnergySystem.java index 8576fdd..59ab862 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/EnergySystem.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/EnergySystem.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.simulation; +package de.hatoka.eos.simulation.capi.business.simulation; import de.hatoka.eos.units.capi.Energy; import de.hatoka.eos.units.capi.Money; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/SimulationRequest.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/SimulationRequest.java similarity index 58% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/SimulationRequest.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/SimulationRequest.java index 256ca4d..471ef58 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/SimulationRequest.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/SimulationRequest.java @@ -1,9 +1,9 @@ -package de.hatoka.eos.devices.capi.business.simulation; +package de.hatoka.eos.simulation.capi.business.simulation; -import de.hatoka.eos.devices.capi.business.device.Device; -import de.hatoka.eos.devices.capi.business.device.DeviceRef; -import de.hatoka.eos.devices.capi.business.device.DeviceState; -import de.hatoka.eos.devices.capi.business.forecast.Forecasts; +import de.hatoka.eos.simulation.capi.business.device.Device; +import de.hatoka.eos.simulation.capi.business.device.DeviceRef; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.forecast.Forecasts; import java.time.Duration; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/SimulationResult.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/SimulationResult.java similarity index 66% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/SimulationResult.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/SimulationResult.java index bf71df1..d2108ff 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/SimulationResult.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/SimulationResult.java @@ -1,9 +1,8 @@ -package de.hatoka.eos.devices.capi.business.simulation; +package de.hatoka.eos.simulation.capi.business.simulation; -import de.hatoka.eos.devices.capi.business.device.DeviceRef; -import de.hatoka.eos.devices.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.device.DeviceRef; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; -import java.time.ZonedDateTime; import java.util.Map; /** diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/SimulationStep.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/SimulationStep.java similarity index 86% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/SimulationStep.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/SimulationStep.java index 9c939bd..ed843ed 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/SimulationStep.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/SimulationStep.java @@ -1,6 +1,6 @@ -package de.hatoka.eos.devices.capi.business.simulation; +package de.hatoka.eos.simulation.capi.business.simulation; -import de.hatoka.eos.devices.capi.business.forecast.Forecasts; +import de.hatoka.eos.simulation.capi.business.forecast.Forecasts; import java.time.Duration; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/SimulationStepResult.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/SimulationStepResult.java similarity index 88% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/SimulationStepResult.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/SimulationStepResult.java index 98edc0f..e61e386 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/SimulationStepResult.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/SimulationStepResult.java @@ -1,6 +1,6 @@ -package de.hatoka.eos.devices.capi.business.simulation; +package de.hatoka.eos.simulation.capi.business.simulation; -import de.hatoka.eos.devices.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; /** * SimulationStepResult is the result of a simulation step of a device diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/Simulator.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/Simulator.java similarity index 82% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/Simulator.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/Simulator.java index 12d238b..68ab436 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/simulation/Simulator.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/simulation/Simulator.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.simulation; +package de.hatoka.eos.simulation.capi.business.simulation; /** * Simulator can execute multiple simulations diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/HourlyEfficiency.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/statistics/HourlyEfficiency.java similarity index 94% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/HourlyEfficiency.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/statistics/HourlyEfficiency.java index df2ae80..bcb6519 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/HourlyEfficiency.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/statistics/HourlyEfficiency.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.statistics; +package de.hatoka.eos.simulation.capi.business.statistics; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/SolarPanelStatistics.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/statistics/SolarPanelStatistics.java similarity index 74% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/SolarPanelStatistics.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/statistics/SolarPanelStatistics.java index 3bcac72..4af8d1d 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/SolarPanelStatistics.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/statistics/SolarPanelStatistics.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.statistics; +package de.hatoka.eos.simulation.capi.business.statistics; import de.hatoka.eos.units.capi.Percentage; diff --git a/devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/SolarPanelStatisticsConfig.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/statistics/SolarPanelStatisticsConfig.java similarity index 91% rename from devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/SolarPanelStatisticsConfig.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/statistics/SolarPanelStatisticsConfig.java index 8874a89..91e9326 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/capi/business/statistics/SolarPanelStatisticsConfig.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/statistics/SolarPanelStatisticsConfig.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.capi.business.statistics; +package de.hatoka.eos.simulation.capi.business.statistics; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/config/ConfigurationLoader.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/config/ConfigurationLoader.java similarity index 86% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/config/ConfigurationLoader.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/config/ConfigurationLoader.java index 746dc7c..3a75f79 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/config/ConfigurationLoader.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/config/ConfigurationLoader.java @@ -1,10 +1,10 @@ -package de.hatoka.eos.devices.internal.business.config; +package de.hatoka.eos.simulation.internal.business.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import de.hatoka.eos.devices.capi.business.config.InstallationConfig; -import de.hatoka.eos.devices.capi.business.config.SimulationConfig; +import de.hatoka.eos.simulation.capi.business.config.InstallationConfig; +import de.hatoka.eos.simulation.capi.business.config.SimulationConfig; import jakarta.inject.Singleton; import java.io.IOException; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/Battery.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/Battery.java similarity index 92% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/Battery.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/Battery.java index caaab5e..8fdff07 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/Battery.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/Battery.java @@ -1,14 +1,13 @@ -package de.hatoka.eos.devices.internal.business.devices; - -import de.hatoka.eos.devices.capi.business.config.DeviceConfig; -import de.hatoka.eos.devices.capi.business.device.Device; -import de.hatoka.eos.devices.capi.business.device.DeviceState; -import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; +package de.hatoka.eos.simulation.internal.business.devices; + +import de.hatoka.eos.simulation.capi.business.config.DeviceConfig; +import de.hatoka.eos.simulation.capi.business.device.Device; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.simulation.EnergySystem; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStep; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStepResult; import de.hatoka.eos.units.capi.Energy; import de.hatoka.eos.units.capi.Percentage; -import de.hatoka.eos.units.capi.Power; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/DeviceFactoryImpl.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/DeviceFactoryImpl.java similarity index 76% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/DeviceFactoryImpl.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/DeviceFactoryImpl.java index 95099bf..728765a 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/DeviceFactoryImpl.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/DeviceFactoryImpl.java @@ -1,12 +1,10 @@ -package de.hatoka.eos.devices.internal.business.devices; +package de.hatoka.eos.simulation.internal.business.devices; -import de.hatoka.eos.devices.capi.business.config.DeviceConfig; -import de.hatoka.eos.devices.capi.business.config.InstallationConfig; -import de.hatoka.eos.devices.capi.business.device.Device; -import de.hatoka.eos.devices.capi.business.device.DeviceFactory; -import de.hatoka.eos.devices.capi.business.device.DeviceRef; -import de.hatoka.eos.devices.capi.business.device.DeviceType; -import de.hatoka.eos.devices.internal.business.statistics.SolarPanelStatisticsLoader; +import de.hatoka.eos.simulation.capi.business.config.DeviceConfig; +import de.hatoka.eos.simulation.capi.business.device.Device; +import de.hatoka.eos.simulation.capi.business.device.DeviceFactory; +import de.hatoka.eos.simulation.capi.business.device.DeviceRef; +import de.hatoka.eos.simulation.capi.business.device.DeviceType; import jakarta.inject.Singleton; import java.lang.reflect.Constructor; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/ElectricCar.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/ElectricCar.java similarity index 83% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/ElectricCar.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/ElectricCar.java index 1295a32..e6c6e3b 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/ElectricCar.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/ElectricCar.java @@ -1,12 +1,12 @@ -package de.hatoka.eos.devices.internal.business.devices; +package de.hatoka.eos.simulation.internal.business.devices; -import de.hatoka.eos.devices.capi.business.config.CarUsageProfile; -import de.hatoka.eos.devices.capi.business.config.DeviceConfig; -import de.hatoka.eos.devices.capi.business.device.Device; -import de.hatoka.eos.devices.capi.business.device.DeviceState; -import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; +import de.hatoka.eos.simulation.capi.business.config.CarUsageProfile; +import de.hatoka.eos.simulation.capi.business.config.DeviceConfig; +import de.hatoka.eos.simulation.capi.business.device.Device; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.simulation.EnergySystem; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStep; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStepResult; import de.hatoka.eos.units.capi.Energy; import java.time.LocalTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/Grid.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/Grid.java similarity index 79% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/Grid.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/Grid.java index 308f0d9..6cb083b 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/Grid.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/Grid.java @@ -1,10 +1,10 @@ -package de.hatoka.eos.devices.internal.business.devices; +package de.hatoka.eos.simulation.internal.business.devices; -import de.hatoka.eos.devices.capi.business.config.DeviceConfig; -import de.hatoka.eos.devices.capi.business.device.Device; -import de.hatoka.eos.devices.capi.business.device.DeviceState; -import de.hatoka.eos.devices.capi.business.forecast.EnergyPriceForecast; -import de.hatoka.eos.devices.capi.business.simulation.*; +import de.hatoka.eos.simulation.capi.business.config.DeviceConfig; +import de.hatoka.eos.simulation.capi.business.device.Device; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; +import de.hatoka.eos.simulation.capi.business.simulation.*; import de.hatoka.eos.units.capi.Energy; import de.hatoka.eos.units.capi.Money; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/NoisyUsage.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/NoisyUsage.java similarity index 65% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/NoisyUsage.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/NoisyUsage.java index 9ee8c64..bf39dad 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/NoisyUsage.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/NoisyUsage.java @@ -1,11 +1,11 @@ -package de.hatoka.eos.devices.internal.business.devices; +package de.hatoka.eos.simulation.internal.business.devices; -import de.hatoka.eos.devices.capi.business.config.DeviceConfig; -import de.hatoka.eos.devices.capi.business.device.Device; -import de.hatoka.eos.devices.capi.business.device.DeviceState; -import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; +import de.hatoka.eos.simulation.capi.business.config.DeviceConfig; +import de.hatoka.eos.simulation.capi.business.device.Device; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.simulation.EnergySystem; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStepResult; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStep; import de.hatoka.eos.units.capi.Power; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/SolarPanel.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/SolarPanel.java similarity index 68% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/SolarPanel.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/SolarPanel.java index 0934a8d..fd9ff07 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/devices/SolarPanel.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/devices/SolarPanel.java @@ -1,12 +1,12 @@ -package de.hatoka.eos.devices.internal.business.devices; +package de.hatoka.eos.simulation.internal.business.devices; -import de.hatoka.eos.devices.capi.business.config.DeviceConfig; -import de.hatoka.eos.devices.capi.business.device.Device; -import de.hatoka.eos.devices.capi.business.device.DeviceState; -import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; -import de.hatoka.eos.devices.capi.business.statistics.SolarPanelStatistics; +import de.hatoka.eos.simulation.capi.business.config.DeviceConfig; +import de.hatoka.eos.simulation.capi.business.device.Device; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.simulation.EnergySystem; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStepResult; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStep; +import de.hatoka.eos.simulation.capi.business.statistics.SolarPanelStatistics; import de.hatoka.eos.units.capi.Percentage; import de.hatoka.eos.units.capi.Power; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/CsvStockEnergyPriceProvider.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/CsvStockEnergyPriceProvider.java similarity index 94% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/CsvStockEnergyPriceProvider.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/CsvStockEnergyPriceProvider.java index 021b17f..191292c 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/CsvStockEnergyPriceProvider.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/CsvStockEnergyPriceProvider.java @@ -1,7 +1,7 @@ -package de.hatoka.eos.devices.internal.business.forecast; +package de.hatoka.eos.simulation.internal.business.forecast; -import de.hatoka.eos.devices.capi.business.config.CsvPriceConfig; -import de.hatoka.eos.devices.capi.business.forecast.EnergyPriceForecast; +import de.hatoka.eos.simulation.capi.business.config.CsvPriceConfig; +import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; import de.hatoka.eos.units.capi.Money; import java.io.BufferedReader; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/FlatPriceService.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/FlatPriceService.java similarity index 79% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/FlatPriceService.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/FlatPriceService.java index 2a31a1c..927d68f 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/FlatPriceService.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/FlatPriceService.java @@ -1,7 +1,7 @@ -package de.hatoka.eos.devices.internal.business.forecast; +package de.hatoka.eos.simulation.internal.business.forecast; -import de.hatoka.eos.devices.capi.business.config.FlatPriceConfig; -import de.hatoka.eos.devices.capi.business.forecast.EnergyPriceForecast; +import de.hatoka.eos.simulation.capi.business.config.FlatPriceConfig; +import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; import de.hatoka.eos.units.capi.Money; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/FlatWeatherService.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/FlatWeatherService.java similarity index 87% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/FlatWeatherService.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/FlatWeatherService.java index 1c831d8..88c3ee1 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/FlatWeatherService.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/FlatWeatherService.java @@ -1,6 +1,6 @@ -package de.hatoka.eos.devices.internal.business.forecast; +package de.hatoka.eos.simulation.internal.business.forecast; -import de.hatoka.eos.devices.capi.business.forecast.WeatherForecast; +import de.hatoka.eos.simulation.capi.business.forecast.WeatherForecast; import de.hatoka.eos.units.capi.Percentage; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/InfluxWeatherForecast.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/InfluxWeatherForecast.java similarity index 86% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/InfluxWeatherForecast.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/InfluxWeatherForecast.java index 8fcca2d..7caa66c 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/InfluxWeatherForecast.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/InfluxWeatherForecast.java @@ -1,6 +1,6 @@ -package de.hatoka.eos.devices.internal.business.forecast; +package de.hatoka.eos.simulation.internal.business.forecast; -import de.hatoka.eos.devices.capi.business.forecast.WeatherForecast; +import de.hatoka.eos.simulation.capi.business.forecast.WeatherForecast; import de.hatoka.eos.persistence.capi.WeatherForecastKey; import de.hatoka.eos.persistence.capi.WeatherDataSource; import de.hatoka.eos.persistence.influx.InfluxWeatherForecastDao; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/StockEnergyPriceConverter.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/StockEnergyPriceConverter.java similarity index 99% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/StockEnergyPriceConverter.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/StockEnergyPriceConverter.java index b2d039e..18357ee 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/forecast/StockEnergyPriceConverter.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/StockEnergyPriceConverter.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.internal.business.forecast; +package de.hatoka.eos.simulation.internal.business.forecast; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/metrics/InfluxDBMetricsExporter.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/metrics/InfluxDBMetricsExporter.java similarity index 93% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/metrics/InfluxDBMetricsExporter.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/metrics/InfluxDBMetricsExporter.java index c2474d8..c60b332 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/metrics/InfluxDBMetricsExporter.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/metrics/InfluxDBMetricsExporter.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.internal.business.metrics; +package de.hatoka.eos.simulation.internal.business.metrics; import com.influxdb.client.DeleteApi; import com.influxdb.client.InfluxDBClient; @@ -7,14 +7,14 @@ import com.influxdb.client.domain.DeletePredicateRequest; import com.influxdb.client.domain.WritePrecision; import com.influxdb.client.write.Point; -import de.hatoka.eos.devices.capi.business.device.DeviceRef; -import de.hatoka.eos.devices.capi.business.device.DeviceState; -import de.hatoka.eos.devices.capi.business.device.DeviceType; -import de.hatoka.eos.devices.capi.business.forecast.EnergyPriceForecast; -import de.hatoka.eos.devices.capi.business.forecast.WeatherForecast; -import de.hatoka.eos.devices.capi.business.metrics.ForecastMetricsExporter; -import de.hatoka.eos.devices.capi.business.metrics.SimulationMetricsExporter; -import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; +import de.hatoka.eos.simulation.capi.business.device.DeviceRef; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.device.DeviceType; +import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; +import de.hatoka.eos.simulation.capi.business.forecast.WeatherForecast; +import de.hatoka.eos.simulation.capi.business.metrics.ForecastMetricsExporter; +import de.hatoka.eos.simulation.capi.business.metrics.SimulationMetricsExporter; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import jakarta.inject.Singleton; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/simulation/Simulation.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/simulation/Simulation.java similarity index 87% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/simulation/Simulation.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/simulation/Simulation.java index 5ed5988..ccd6939 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/simulation/Simulation.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/simulation/Simulation.java @@ -1,11 +1,11 @@ -package de.hatoka.eos.devices.internal.business.simulation; +package de.hatoka.eos.simulation.internal.business.simulation; -import de.hatoka.eos.devices.capi.business.device.Device; -import de.hatoka.eos.devices.capi.business.device.DeviceRef; -import de.hatoka.eos.devices.capi.business.device.DeviceState; -import de.hatoka.eos.devices.capi.business.metrics.ForecastMetricsExporter; -import de.hatoka.eos.devices.capi.business.metrics.SimulationMetricsExporter; -import de.hatoka.eos.devices.capi.business.simulation.*; +import de.hatoka.eos.simulation.capi.business.device.Device; +import de.hatoka.eos.simulation.capi.business.device.DeviceRef; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.metrics.ForecastMetricsExporter; +import de.hatoka.eos.simulation.capi.business.metrics.SimulationMetricsExporter; +import de.hatoka.eos.simulation.capi.business.simulation.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/simulation/SimulatorImpl.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/simulation/SimulatorImpl.java similarity index 50% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/simulation/SimulatorImpl.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/simulation/SimulatorImpl.java index fb34765..a4cd437 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/simulation/SimulatorImpl.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/simulation/SimulatorImpl.java @@ -1,10 +1,10 @@ -package de.hatoka.eos.devices.internal.business.simulation; +package de.hatoka.eos.simulation.internal.business.simulation; -import de.hatoka.eos.devices.capi.business.metrics.ForecastMetricsExporter; -import de.hatoka.eos.devices.capi.business.metrics.SimulationMetricsExporter; -import de.hatoka.eos.devices.capi.business.simulation.SimulationRequest; -import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; -import de.hatoka.eos.devices.capi.business.simulation.Simulator; +import de.hatoka.eos.simulation.capi.business.metrics.ForecastMetricsExporter; +import de.hatoka.eos.simulation.capi.business.metrics.SimulationMetricsExporter; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationRequest; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; +import de.hatoka.eos.simulation.capi.business.simulation.Simulator; import jakarta.inject.Inject; import jakarta.inject.Singleton; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/CurvePanelStatistic.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/statistics/CurvePanelStatistic.java similarity index 92% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/CurvePanelStatistic.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/statistics/CurvePanelStatistic.java index aa8d08c..3ae6543 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/CurvePanelStatistic.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/statistics/CurvePanelStatistic.java @@ -1,6 +1,6 @@ -package de.hatoka.eos.devices.internal.business.statistics; +package de.hatoka.eos.simulation.internal.business.statistics; -import de.hatoka.eos.devices.capi.business.statistics.SolarPanelStatistics; +import de.hatoka.eos.simulation.capi.business.statistics.SolarPanelStatistics; import de.hatoka.eos.units.capi.Percentage; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/SolarPanelStatisticsImpl.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/statistics/SolarPanelStatisticsImpl.java similarity index 83% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/SolarPanelStatisticsImpl.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/statistics/SolarPanelStatisticsImpl.java index 66b2f09..b95c47b 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/SolarPanelStatisticsImpl.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/statistics/SolarPanelStatisticsImpl.java @@ -1,8 +1,8 @@ -package de.hatoka.eos.devices.internal.business.statistics; +package de.hatoka.eos.simulation.internal.business.statistics; -import de.hatoka.eos.devices.capi.business.statistics.HourlyEfficiency; -import de.hatoka.eos.devices.capi.business.statistics.SolarPanelStatisticsConfig; -import de.hatoka.eos.devices.capi.business.statistics.SolarPanelStatistics; +import de.hatoka.eos.simulation.capi.business.statistics.HourlyEfficiency; +import de.hatoka.eos.simulation.capi.business.statistics.SolarPanelStatisticsConfig; +import de.hatoka.eos.simulation.capi.business.statistics.SolarPanelStatistics; import de.hatoka.eos.units.capi.Percentage; import java.time.ZonedDateTime; diff --git a/devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/SolarPanelStatisticsLoader.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/statistics/SolarPanelStatisticsLoader.java similarity index 83% rename from devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/SolarPanelStatisticsLoader.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/statistics/SolarPanelStatisticsLoader.java index 29e120b..7fb27f4 100644 --- a/devices/src/main/java/de/hatoka/eos/devices/internal/business/statistics/SolarPanelStatisticsLoader.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/statistics/SolarPanelStatisticsLoader.java @@ -1,8 +1,8 @@ -package de.hatoka.eos.devices.internal.business.statistics; +package de.hatoka.eos.simulation.internal.business.statistics; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import de.hatoka.eos.devices.capi.business.statistics.SolarPanelStatisticsConfig; +import de.hatoka.eos.simulation.capi.business.statistics.SolarPanelStatisticsConfig; import jakarta.inject.Singleton; import java.io.IOException; diff --git a/devices/src/main/resources/META-INF/beans.xml b/simulation/src/main/resources/META-INF/beans.xml similarity index 100% rename from devices/src/main/resources/META-INF/beans.xml rename to simulation/src/main/resources/META-INF/beans.xml diff --git a/devices/src/main/resources/application.properties b/simulation/src/main/resources/application.properties similarity index 100% rename from devices/src/main/resources/application.properties rename to simulation/src/main/resources/application.properties diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/DateTooling.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/DateTooling.java similarity index 93% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/DateTooling.java rename to simulation/src/test/java/de/hatoka/eos/simulation/internal/business/DateTooling.java index 7f1e0e1..22e422d 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/DateTooling.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/DateTooling.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.internal.business; +package de.hatoka.eos.simulation.internal.business; import java.time.LocalDateTime; import java.time.ZoneId; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/ConfigurationDeviceBuilderTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/config/ConfigurationDeviceBuilderTest.java similarity index 74% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/config/ConfigurationDeviceBuilderTest.java rename to simulation/src/test/java/de/hatoka/eos/simulation/internal/business/config/ConfigurationDeviceBuilderTest.java index 10fd219..2f97061 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/ConfigurationDeviceBuilderTest.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/config/ConfigurationDeviceBuilderTest.java @@ -1,9 +1,9 @@ -package de.hatoka.eos.devices.internal.business.config; +package de.hatoka.eos.simulation.internal.business.config; -import de.hatoka.eos.devices.capi.business.device.Device; -import de.hatoka.eos.devices.capi.business.device.DeviceFactory; -import de.hatoka.eos.devices.capi.business.device.DeviceRef; -import de.hatoka.eos.devices.capi.business.device.DeviceType; +import de.hatoka.eos.simulation.capi.business.device.Device; +import de.hatoka.eos.simulation.capi.business.device.DeviceFactory; +import de.hatoka.eos.simulation.capi.business.device.DeviceRef; +import de.hatoka.eos.simulation.capi.business.device.DeviceType; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/ConfigurationLoaderTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/config/ConfigurationLoaderTest.java similarity index 90% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/config/ConfigurationLoaderTest.java rename to simulation/src/test/java/de/hatoka/eos/simulation/internal/business/config/ConfigurationLoaderTest.java index 430b035..9a9f174 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/ConfigurationLoaderTest.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/config/ConfigurationLoaderTest.java @@ -1,11 +1,11 @@ -package de.hatoka.eos.devices.internal.business.config; +package de.hatoka.eos.simulation.internal.business.config; -import de.hatoka.eos.devices.capi.business.config.DeviceConfig; -import de.hatoka.eos.devices.capi.business.config.InstallationConfig; -import de.hatoka.eos.devices.capi.business.config.SimulationConfig; -import de.hatoka.eos.devices.capi.business.device.DeviceType; +import de.hatoka.eos.simulation.capi.business.config.DeviceConfig; +import de.hatoka.eos.simulation.capi.business.config.InstallationConfig; +import de.hatoka.eos.simulation.capi.business.config.SimulationConfig; +import de.hatoka.eos.simulation.capi.business.device.DeviceType; import de.hatoka.eos.units.capi.Money; -import de.hatoka.eos.devices.internal.business.DateTooling; +import de.hatoka.eos.simulation.internal.business.DateTooling; import org.junit.jupiter.api.Test; import java.io.IOException; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/CsvPriceConfigTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/config/CsvPriceConfigTest.java similarity index 86% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/config/CsvPriceConfigTest.java rename to simulation/src/test/java/de/hatoka/eos/simulation/internal/business/config/CsvPriceConfigTest.java index ed6987a..e43dc40 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/CsvPriceConfigTest.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/config/CsvPriceConfigTest.java @@ -1,8 +1,8 @@ -package de.hatoka.eos.devices.internal.business.config; +package de.hatoka.eos.simulation.internal.business.config; -import de.hatoka.eos.devices.capi.business.config.CsvPriceConfig; -import de.hatoka.eos.devices.capi.business.config.GridConfig; -import de.hatoka.eos.devices.capi.business.config.InstallationConfig; +import de.hatoka.eos.simulation.capi.business.config.CsvPriceConfig; +import de.hatoka.eos.simulation.capi.business.config.GridConfig; +import de.hatoka.eos.simulation.capi.business.config.InstallationConfig; import de.hatoka.eos.units.capi.Money; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; @@ -49,10 +49,10 @@ public void testCsvStockEnergyPriceProviderCreation() throws IOException InstallationConfig config = configurationLoader.load("test-installation-for-csv-prices.yaml"); GridConfig gridConfig = config.getGrid(); - de.hatoka.eos.devices.capi.business.forecast.EnergyPriceForecast priceProvider = gridConfig.getEnergyPriceProvider(); + de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast priceProvider = gridConfig.getEnergyPriceProvider(); assertNotNull(priceProvider); - assertInstanceOf(de.hatoka.eos.devices.internal.business.forecast.CsvStockEnergyPriceProvider.class, priceProvider); + assertInstanceOf(de.hatoka.eos.simulation.internal.business.forecast.CsvStockEnergyPriceProvider.class, priceProvider); // Test actual price retrieval for a known date/time from the CSV // CSV contains data for 2025/08/04 at hour 0 with price 81.05 EUR/MWh diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/SolarPanelStatisticsLoaderTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/config/SolarPanelStatisticsLoaderTest.java similarity index 87% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/config/SolarPanelStatisticsLoaderTest.java rename to simulation/src/test/java/de/hatoka/eos/simulation/internal/business/config/SolarPanelStatisticsLoaderTest.java index 3ff4bc8..8f6f968 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/config/SolarPanelStatisticsLoaderTest.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/config/SolarPanelStatisticsLoaderTest.java @@ -1,9 +1,9 @@ -package de.hatoka.eos.devices.internal.business.config; +package de.hatoka.eos.simulation.internal.business.config; -import de.hatoka.eos.devices.capi.business.statistics.HourlyEfficiency; -import de.hatoka.eos.devices.capi.business.statistics.SolarPanelStatisticsConfig; +import de.hatoka.eos.simulation.capi.business.statistics.HourlyEfficiency; +import de.hatoka.eos.simulation.capi.business.statistics.SolarPanelStatisticsConfig; import de.hatoka.eos.units.capi.Percentage; -import de.hatoka.eos.devices.internal.business.statistics.SolarPanelStatisticsLoader; +import de.hatoka.eos.simulation.internal.business.statistics.SolarPanelStatisticsLoader; import org.junit.jupiter.api.Test; import java.io.IOException; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/BatteryTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/devices/BatteryTest.java similarity index 97% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/BatteryTest.java rename to simulation/src/test/java/de/hatoka/eos/simulation/internal/business/devices/BatteryTest.java index 78c7f6c..724a4e7 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/BatteryTest.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/devices/BatteryTest.java @@ -1,14 +1,14 @@ -package de.hatoka.eos.devices.internal.business.devices; +package de.hatoka.eos.simulation.internal.business.devices; -import de.hatoka.eos.devices.capi.business.config.DeviceConfig; -import de.hatoka.eos.devices.capi.business.device.DeviceState; -import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; +import de.hatoka.eos.simulation.capi.business.config.DeviceConfig; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.simulation.EnergySystem; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStep; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStepResult; import de.hatoka.eos.units.capi.Energy; import de.hatoka.eos.units.capi.Percentage; import de.hatoka.eos.units.capi.Power; -import de.hatoka.eos.devices.internal.business.DateTooling; +import de.hatoka.eos.simulation.internal.business.DateTooling; import org.junit.jupiter.api.Test; import java.time.Duration; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/ElectricCarTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/devices/ElectricCarTest.java similarity index 96% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/ElectricCarTest.java rename to simulation/src/test/java/de/hatoka/eos/simulation/internal/business/devices/ElectricCarTest.java index 5b4c0c6..c759930 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/ElectricCarTest.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/devices/ElectricCarTest.java @@ -1,15 +1,15 @@ -package de.hatoka.eos.devices.internal.business.devices; - -import de.hatoka.eos.devices.capi.business.config.CarUsageProfile; -import de.hatoka.eos.devices.capi.business.config.DeviceConfig; -import de.hatoka.eos.devices.capi.business.device.DeviceState; -import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; +package de.hatoka.eos.simulation.internal.business.devices; + +import de.hatoka.eos.simulation.capi.business.config.CarUsageProfile; +import de.hatoka.eos.simulation.capi.business.config.DeviceConfig; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.simulation.EnergySystem; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStep; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStepResult; import de.hatoka.eos.units.capi.Energy; import de.hatoka.eos.units.capi.Percentage; import de.hatoka.eos.units.capi.Power; -import de.hatoka.eos.devices.internal.business.DateTooling; +import de.hatoka.eos.simulation.internal.business.DateTooling; import org.junit.jupiter.api.Test; import java.time.DayOfWeek; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/GridTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/devices/GridTest.java similarity index 91% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/GridTest.java rename to simulation/src/test/java/de/hatoka/eos/simulation/internal/business/devices/GridTest.java index a6ab691..9cb746a 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/GridTest.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/devices/GridTest.java @@ -1,12 +1,12 @@ -package de.hatoka.eos.devices.internal.business.devices; +package de.hatoka.eos.simulation.internal.business.devices; -import de.hatoka.eos.devices.capi.business.device.DeviceState; -import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.simulation.EnergySystem; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStep; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStepResult; import de.hatoka.eos.units.capi.Energy; import de.hatoka.eos.units.capi.Money; -import de.hatoka.eos.devices.internal.business.DateTooling; +import de.hatoka.eos.simulation.internal.business.DateTooling; import org.junit.jupiter.api.Test; import java.time.Duration; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/NoisyUsageTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/devices/NoisyUsageTest.java similarity index 81% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/NoisyUsageTest.java rename to simulation/src/test/java/de/hatoka/eos/simulation/internal/business/devices/NoisyUsageTest.java index 4570173..4f09448 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/NoisyUsageTest.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/devices/NoisyUsageTest.java @@ -1,13 +1,13 @@ -package de.hatoka.eos.devices.internal.business.devices; +package de.hatoka.eos.simulation.internal.business.devices; -import de.hatoka.eos.devices.capi.business.config.DeviceConfig; -import de.hatoka.eos.devices.capi.business.device.DeviceState; -import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; +import de.hatoka.eos.simulation.capi.business.config.DeviceConfig; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.simulation.EnergySystem; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStep; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStepResult; import de.hatoka.eos.units.capi.Energy; import de.hatoka.eos.units.capi.Power; -import de.hatoka.eos.devices.internal.business.DateTooling; +import de.hatoka.eos.simulation.internal.business.DateTooling; import org.junit.jupiter.api.Test; import java.time.Duration; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/SolarPanelTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/devices/SolarPanelTest.java similarity index 90% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/SolarPanelTest.java rename to simulation/src/test/java/de/hatoka/eos/simulation/internal/business/devices/SolarPanelTest.java index 4010df0..85f7322 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/devices/SolarPanelTest.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/devices/SolarPanelTest.java @@ -1,14 +1,14 @@ -package de.hatoka.eos.devices.internal.business.devices; +package de.hatoka.eos.simulation.internal.business.devices; -import de.hatoka.eos.devices.capi.business.config.DeviceConfig; -import de.hatoka.eos.devices.capi.business.device.DeviceState; -import de.hatoka.eos.devices.capi.business.simulation.EnergySystem; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStep; -import de.hatoka.eos.devices.capi.business.simulation.SimulationStepResult; +import de.hatoka.eos.simulation.capi.business.config.DeviceConfig; +import de.hatoka.eos.simulation.capi.business.device.DeviceState; +import de.hatoka.eos.simulation.capi.business.simulation.EnergySystem; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStep; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationStepResult; import de.hatoka.eos.units.capi.Energy; import de.hatoka.eos.units.capi.Percentage; import de.hatoka.eos.units.capi.Power; -import de.hatoka.eos.devices.internal.business.DateTooling; +import de.hatoka.eos.simulation.internal.business.DateTooling; import org.junit.jupiter.api.Test; import java.time.Duration; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/forecast/FlatWeatherServiceTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/forecast/FlatWeatherServiceTest.java similarity index 92% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/forecast/FlatWeatherServiceTest.java rename to simulation/src/test/java/de/hatoka/eos/simulation/internal/business/forecast/FlatWeatherServiceTest.java index 98882b9..80bbbb2 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/forecast/FlatWeatherServiceTest.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/forecast/FlatWeatherServiceTest.java @@ -1,8 +1,8 @@ -package de.hatoka.eos.devices.internal.business.forecast; +package de.hatoka.eos.simulation.internal.business.forecast; -import de.hatoka.eos.devices.capi.business.forecast.WeatherForecast; +import de.hatoka.eos.simulation.capi.business.forecast.WeatherForecast; import de.hatoka.eos.units.capi.Percentage; -import de.hatoka.eos.devices.internal.business.DateTooling; +import de.hatoka.eos.simulation.internal.business.DateTooling; import org.junit.jupiter.api.Test; import java.util.List; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/forecast/StockEnergyPriceConverterTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/forecast/StockEnergyPriceConverterTest.java similarity index 99% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/forecast/StockEnergyPriceConverterTest.java rename to simulation/src/test/java/de/hatoka/eos/simulation/internal/business/forecast/StockEnergyPriceConverterTest.java index 4ae1500..3b485bb 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/forecast/StockEnergyPriceConverterTest.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/forecast/StockEnergyPriceConverterTest.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.devices.internal.business.forecast; +package de.hatoka.eos.simulation.internal.business.forecast; import org.junit.jupiter.api.Test; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/metrics/PriceForecastExporterTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics/PriceForecastExporterTest.java similarity index 82% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/metrics/PriceForecastExporterTest.java rename to simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics/PriceForecastExporterTest.java index 6957a55..55ac6ec 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/metrics/PriceForecastExporterTest.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics/PriceForecastExporterTest.java @@ -1,10 +1,10 @@ -package de.hatoka.eos.devices.internal.business.metrics; +package de.hatoka.eos.simulation.internal.business.metrics; -import de.hatoka.eos.devices.capi.business.config.InstallationConfig; -import de.hatoka.eos.devices.capi.business.forecast.EnergyPriceForecast; -import de.hatoka.eos.devices.capi.business.metrics.ForecastMetricsExporter; -import de.hatoka.eos.devices.internal.business.DateTooling; -import de.hatoka.eos.devices.internal.business.config.ConfigurationLoader; +import de.hatoka.eos.simulation.capi.business.config.InstallationConfig; +import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; +import de.hatoka.eos.simulation.capi.business.metrics.ForecastMetricsExporter; +import de.hatoka.eos.simulation.internal.business.DateTooling; +import de.hatoka.eos.simulation.internal.business.config.ConfigurationLoader; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/metrics/SimulationMetricsExporterTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics/SimulationMetricsExporterTest.java similarity index 82% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/metrics/SimulationMetricsExporterTest.java rename to simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics/SimulationMetricsExporterTest.java index 11220ca..9e0709f 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/metrics/SimulationMetricsExporterTest.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics/SimulationMetricsExporterTest.java @@ -1,15 +1,15 @@ -package de.hatoka.eos.devices.internal.business.metrics; - -import de.hatoka.eos.devices.capi.business.config.InstallationConfig; -import de.hatoka.eos.devices.capi.business.config.SimulationConfig; -import de.hatoka.eos.devices.capi.business.device.DeviceFactory; -import de.hatoka.eos.devices.capi.business.forecast.Forecasts; -import de.hatoka.eos.devices.capi.business.metrics.SimulationMetricsExporter; -import de.hatoka.eos.devices.capi.business.simulation.SimulationRequest; -import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; -import de.hatoka.eos.devices.capi.business.simulation.Simulator; -import de.hatoka.eos.devices.internal.business.config.ConfigurationLoader; -import de.hatoka.eos.devices.internal.business.forecast.FlatWeatherService; +package de.hatoka.eos.simulation.internal.business.metrics; + +import de.hatoka.eos.simulation.capi.business.config.InstallationConfig; +import de.hatoka.eos.simulation.capi.business.config.SimulationConfig; +import de.hatoka.eos.simulation.capi.business.device.DeviceFactory; +import de.hatoka.eos.simulation.capi.business.forecast.Forecasts; +import de.hatoka.eos.simulation.capi.business.metrics.SimulationMetricsExporter; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationRequest; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; +import de.hatoka.eos.simulation.capi.business.simulation.Simulator; +import de.hatoka.eos.simulation.internal.business.config.ConfigurationLoader; +import de.hatoka.eos.simulation.internal.business.forecast.FlatWeatherService; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/simulation/SimulationTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/simulation/SimulationTest.java similarity index 94% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/simulation/SimulationTest.java rename to simulation/src/test/java/de/hatoka/eos/simulation/internal/business/simulation/SimulationTest.java index cecc8ed..3ad40fd 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/simulation/SimulationTest.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/simulation/SimulationTest.java @@ -1,23 +1,23 @@ -package de.hatoka.eos.devices.internal.business.simulation; - -import de.hatoka.eos.devices.capi.business.config.DeviceConfig; -import de.hatoka.eos.devices.capi.business.config.InstallationConfig; -import de.hatoka.eos.devices.capi.business.config.SimulationConfig; -import de.hatoka.eos.devices.capi.business.device.*; -import de.hatoka.eos.devices.capi.business.forecast.Forecasts; -import de.hatoka.eos.devices.capi.business.simulation.SimulationRequest; -import de.hatoka.eos.devices.capi.business.simulation.SimulationResult; -import de.hatoka.eos.devices.capi.business.simulation.Simulator; +package de.hatoka.eos.simulation.internal.business.simulation; + +import de.hatoka.eos.simulation.capi.business.config.DeviceConfig; +import de.hatoka.eos.simulation.capi.business.config.InstallationConfig; +import de.hatoka.eos.simulation.capi.business.config.SimulationConfig; +import de.hatoka.eos.simulation.capi.business.device.*; +import de.hatoka.eos.simulation.capi.business.forecast.Forecasts; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationRequest; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; +import de.hatoka.eos.simulation.capi.business.simulation.Simulator; import de.hatoka.eos.units.capi.Energy; import de.hatoka.eos.units.capi.Money; import de.hatoka.eos.units.capi.Percentage; import de.hatoka.eos.units.capi.Power; -import de.hatoka.eos.devices.internal.business.DateTooling; -import de.hatoka.eos.devices.internal.business.config.ConfigurationLoader; -import de.hatoka.eos.devices.internal.business.devices.Battery; -import de.hatoka.eos.devices.internal.business.devices.Grid; -import de.hatoka.eos.devices.internal.business.devices.NoisyUsage; -import de.hatoka.eos.devices.internal.business.devices.SolarPanel; +import de.hatoka.eos.simulation.internal.business.DateTooling; +import de.hatoka.eos.simulation.internal.business.config.ConfigurationLoader; +import de.hatoka.eos.simulation.internal.business.devices.Battery; +import de.hatoka.eos.simulation.internal.business.devices.Grid; +import de.hatoka.eos.simulation.internal.business.devices.NoisyUsage; +import de.hatoka.eos.simulation.internal.business.devices.SolarPanel; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; diff --git a/devices/src/test/java/de/hatoka/eos/devices/internal/business/statistics/CurvePanelStatisticTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/statistics/CurvePanelStatisticTest.java similarity index 94% rename from devices/src/test/java/de/hatoka/eos/devices/internal/business/statistics/CurvePanelStatisticTest.java rename to simulation/src/test/java/de/hatoka/eos/simulation/internal/business/statistics/CurvePanelStatisticTest.java index e856110..afc3c9b 100644 --- a/devices/src/test/java/de/hatoka/eos/devices/internal/business/statistics/CurvePanelStatisticTest.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/statistics/CurvePanelStatisticTest.java @@ -1,7 +1,7 @@ -package de.hatoka.eos.devices.internal.business.statistics; +package de.hatoka.eos.simulation.internal.business.statistics; import de.hatoka.eos.units.capi.Percentage; -import de.hatoka.eos.devices.internal.business.DateTooling; +import de.hatoka.eos.simulation.internal.business.DateTooling; import org.junit.jupiter.api.Test; import java.util.List; diff --git a/devices/src/test/resources/application.properties b/simulation/src/test/resources/application.properties similarity index 100% rename from devices/src/test/resources/application.properties rename to simulation/src/test/resources/application.properties diff --git a/devices/src/test/resources/solar-panel-statistics.yaml b/simulation/src/test/resources/solar-panel-statistics.yaml similarity index 100% rename from devices/src/test/resources/solar-panel-statistics.yaml rename to simulation/src/test/resources/solar-panel-statistics.yaml diff --git a/devices/src/test/resources/stockprices_DE_LU_week_32-33.csv b/simulation/src/test/resources/stockprices_DE_LU_week_32-33.csv similarity index 100% rename from devices/src/test/resources/stockprices_DE_LU_week_32-33.csv rename to simulation/src/test/resources/stockprices_DE_LU_week_32-33.csv diff --git a/devices/src/test/resources/stockprices_DE_LU_week_32_2025.svg b/simulation/src/test/resources/stockprices_DE_LU_week_32_2025.svg similarity index 100% rename from devices/src/test/resources/stockprices_DE_LU_week_32_2025.svg rename to simulation/src/test/resources/stockprices_DE_LU_week_32_2025.svg diff --git a/devices/src/test/resources/stockprices_DE_LU_week_33_2025.svg b/simulation/src/test/resources/stockprices_DE_LU_week_33_2025.svg similarity index 100% rename from devices/src/test/resources/stockprices_DE_LU_week_33_2025.svg rename to simulation/src/test/resources/stockprices_DE_LU_week_33_2025.svg diff --git a/devices/src/test/resources/test-installation-100percent.yaml b/simulation/src/test/resources/test-installation-100percent.yaml similarity index 100% rename from devices/src/test/resources/test-installation-100percent.yaml rename to simulation/src/test/resources/test-installation-100percent.yaml diff --git a/devices/src/test/resources/test-installation-for-csv-prices.yaml b/simulation/src/test/resources/test-installation-for-csv-prices.yaml similarity index 100% rename from devices/src/test/resources/test-installation-for-csv-prices.yaml rename to simulation/src/test/resources/test-installation-for-csv-prices.yaml diff --git a/devices/src/test/resources/test-installation-for-grafana.yaml b/simulation/src/test/resources/test-installation-for-grafana.yaml similarity index 100% rename from devices/src/test/resources/test-installation-for-grafana.yaml rename to simulation/src/test/resources/test-installation-for-grafana.yaml diff --git a/devices/src/test/resources/test-installation-with-car.yaml b/simulation/src/test/resources/test-installation-with-car.yaml similarity index 100% rename from devices/src/test/resources/test-installation-with-car.yaml rename to simulation/src/test/resources/test-installation-with-car.yaml diff --git a/devices/src/test/resources/test-installation-without-car.yaml b/simulation/src/test/resources/test-installation-without-car.yaml similarity index 100% rename from devices/src/test/resources/test-installation-without-car.yaml rename to simulation/src/test/resources/test-installation-without-car.yaml diff --git a/devices/src/test/resources/test-simulation-summer.yaml b/simulation/src/test/resources/test-simulation-summer.yaml similarity index 100% rename from devices/src/test/resources/test-simulation-summer.yaml rename to simulation/src/test/resources/test-simulation-summer.yaml diff --git a/devices/src/test/resources/test-simulation-with-csv-prices.yaml b/simulation/src/test/resources/test-simulation-with-csv-prices.yaml similarity index 100% rename from devices/src/test/resources/test-simulation-with-csv-prices.yaml rename to simulation/src/test/resources/test-simulation-with-csv-prices.yaml From 93ae303f2faa50c9742581e12de58cd4f7839b18 Mon Sep 17 00:00:00 2001 From: Thomas Bergmann Date: Sun, 16 Nov 2025 15:43:51 +0100 Subject: [PATCH 08/16] rework: restructure forecast --- .../AbstractWeatherForecastImporter.java | 10 +- .../forecast/ForecastImportApplication.java | 4 +- .../energycharts/EnergyChartsImporter.java | 73 + .../StockEnergyPriceConverter.java | 2 +- .../MeteoMediaWeatherForecastImporter.java | 13 +- ...MediaWeatherSunshineDurationConverter.java | 2 +- .../OpenMeteoWeatherForecastImporter.java | 8 +- .../PriceForecastExporterTest.java | 6 +- .../StockEnergyPriceConverterTest.java | 9 +- ...MeteoMediaWeatherForecastImporterTest.java | 12 +- ...aWeatherSunshineDurationConverterTest.java | 2 +- .../OpenMeteoWeatherForecastImporterTest.java | 12 +- .../resources/energycharts_2025_11_16.json | 13186 ++++++++++++++++ .../stockprices_DE_LU_week_32-33.csv | 0 .../stockprices_DE_LU_week_32_2025.svg | 0 .../stockprices_DE_LU_week_33_2025.svg | 0 .../persistence/capi/WeatherForcastDAO.java | 5 - .../capi/{ => weather}/WeatherDataSource.java | 2 +- .../capi/weather/WeatherForcastDAO.java | 7 + .../{ => weather}/WeatherForecastKey.java | 2 +- .../capi/{ => weather}/WeatherForecastPO.java | 2 +- .../capi/{ => weather}/WeatherStation.java | 2 +- .../influx/InfluxWeatherForecastDao.java | 6 +- .../influx/InfluxWeatherForecastDaoTest.java | 8 +- .../metrics/ForecastMetricsExporter.java | 28 - .../forecast/CsvStockEnergyPriceProvider.java | 132 - ...rForecast.java => DaoWeatherForecast.java} | 15 +- .../business/simulation/Simulation.java | 6 +- .../business/simulation/SimulatorImpl.java | 5 +- 29 files changed, 13327 insertions(+), 232 deletions(-) create mode 100644 forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java rename {simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast => forecast/src/main/java/de/hatoka/eos/forecast/energycharts}/StockEnergyPriceConverter.java (99%) rename forecast/src/main/java/de/hatoka/eos/forecast/{ => meteomedia}/MeteoMediaWeatherForecastImporter.java (90%) rename forecast/src/main/java/de/hatoka/eos/forecast/{ => meteomedia}/MeteoMediaWeatherSunshineDurationConverter.java (99%) rename forecast/src/main/java/de/hatoka/eos/forecast/{ => openmeteo}/OpenMeteoWeatherForecastImporter.java (96%) rename {simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics => forecast/src/test/java/de/hatoka/eos/forecast/energycharts}/PriceForecastExporterTest.java (91%) rename {simulation/src/test/java/de/hatoka/eos/simulation/internal/business/forecast => forecast/src/test/java/de/hatoka/eos/forecast/energycharts}/StockEnergyPriceConverterTest.java (95%) rename forecast/src/test/java/de/hatoka/eos/forecast/{ => meteomedia}/MeteoMediaWeatherForecastImporterTest.java (76%) rename forecast/src/test/java/de/hatoka/eos/forecast/{ => meteomedia}/MeteoMediaWeatherSunshineDurationConverterTest.java (99%) rename forecast/src/test/java/de/hatoka/eos/forecast/{ => openmeteo}/OpenMeteoWeatherForecastImporterTest.java (88%) create mode 100644 forecast/src/test/resources/energycharts_2025_11_16.json rename {simulation => forecast}/src/test/resources/stockprices_DE_LU_week_32-33.csv (100%) rename {simulation => forecast}/src/test/resources/stockprices_DE_LU_week_32_2025.svg (100%) rename {simulation => forecast}/src/test/resources/stockprices_DE_LU_week_33_2025.svg (100%) delete mode 100644 persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForcastDAO.java rename persistence-api/src/main/java/de/hatoka/eos/persistence/capi/{ => weather}/WeatherDataSource.java (90%) create mode 100644 persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForcastDAO.java rename persistence-api/src/main/java/de/hatoka/eos/persistence/capi/{ => weather}/WeatherForecastKey.java (87%) rename persistence-api/src/main/java/de/hatoka/eos/persistence/capi/{ => weather}/WeatherForecastPO.java (89%) rename persistence-api/src/main/java/de/hatoka/eos/persistence/capi/{ => weather}/WeatherStation.java (94%) delete mode 100644 simulation/src/main/java/de/hatoka/eos/simulation/capi/business/metrics/ForecastMetricsExporter.java delete mode 100644 simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/CsvStockEnergyPriceProvider.java rename simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/{InfluxWeatherForecast.java => DaoWeatherForecast.java} (58%) diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java index 05f0f82..d791a93 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java @@ -1,10 +1,10 @@ package de.hatoka.eos.forecast; -import de.hatoka.eos.persistence.capi.WeatherStation; -import de.hatoka.eos.persistence.capi.WeatherForcastDAO; -import de.hatoka.eos.persistence.capi.WeatherForecastKey; -import de.hatoka.eos.persistence.capi.WeatherForecastPO; -import de.hatoka.eos.persistence.capi.WeatherDataSource; +import de.hatoka.eos.persistence.capi.weather.WeatherStation; +import de.hatoka.eos.persistence.capi.weather.WeatherForcastDAO; +import de.hatoka.eos.persistence.capi.weather.WeatherForecastKey; +import de.hatoka.eos.persistence.capi.weather.WeatherForecastPO; +import de.hatoka.eos.persistence.capi.weather.WeatherDataSource; import de.hatoka.eos.units.capi.Percentage; import jakarta.inject.Inject; import org.slf4j.Logger; diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java b/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java index c24b6bc..55538c2 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java @@ -1,6 +1,8 @@ package de.hatoka.eos.forecast; -import de.hatoka.eos.persistence.capi.WeatherStation; +import de.hatoka.eos.forecast.meteomedia.MeteoMediaWeatherForecastImporter; +import de.hatoka.eos.forecast.openmeteo.OpenMeteoWeatherForecastImporter; +import de.hatoka.eos.persistence.capi.weather.WeatherStation; import io.quarkus.runtime.Quarkus; import io.quarkus.runtime.QuarkusApplication; import io.quarkus.runtime.annotations.QuarkusMain; diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java new file mode 100644 index 0000000..6e4d42f --- /dev/null +++ b/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java @@ -0,0 +1,73 @@ +package de.hatoka.eos.forecast.energycharts; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; + +public class EnergyChartsImporter +{ + private static final String URI_FORMAT = "https://energy-charts.info/charts/price_spot_market/data/de/week_15min_%s_%s.json"; // day format parameter week, year + private static final Logger logger = LoggerFactory.getLogger(EnergyChartsImporter.class); + + protected Map downloadAndProcessWeatherData(ZonedDateTime startDate) + throws IOException, InterruptedException + { + Double[] imageData = downloadData(URI.create(URI_FORMAT.formatted(startDate.getYear(), getWeekOfYear(startDate)))); + // for each 15min while imageData is available + ZonedDateTime dateOfPrice = getStartOfWeek(startDate); + Map result = new HashMap<>(); + for(Double price : imageData) + { + result.put(dateOfPrice, price); + dateOfPrice = dateOfPrice.plusMinutes(15); + } + return result; + } + + private ZonedDateTime getStartOfWeek(ZonedDateTime startDate) + { + // 2025/11/10 for 2025/11/16 + return startDate; + } + + private Integer getWeekOfYear(ZonedDateTime startDate) + { + return 46; + } + + private Double[] downloadData(URI dataUrl) throws IOException, InterruptedException + { + try (HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .followRedirects(HttpClient.Redirect.NORMAL) + .build()) + { + HttpRequest request = HttpRequest.newBuilder() + .uri(dataUrl) + .timeout(Duration.ofSeconds(30)) + .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36") + .header("Accept", "application/json") + .GET() + .build(); + + logger.debug("Sending HTTP request to: {}", dataUrl); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); + + if (response.statusCode() != 200) + { + throw new IOException("HTTP request failed with status code: " + response.statusCode()); + } + return response.body(); + } + } +} diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/StockEnergyPriceConverter.java b/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/StockEnergyPriceConverter.java similarity index 99% rename from simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/StockEnergyPriceConverter.java rename to forecast/src/main/java/de/hatoka/eos/forecast/energycharts/StockEnergyPriceConverter.java index 18357ee..5a7b2d4 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/StockEnergyPriceConverter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/StockEnergyPriceConverter.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.simulation.internal.business.forecast; +package de.hatoka.eos.forecast.energycharts; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherForecastImporter.java similarity index 90% rename from forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java rename to forecast/src/main/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherForecastImporter.java index 49559e8..dd0ec4a 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherForecastImporter.java @@ -1,9 +1,8 @@ -package de.hatoka.eos.forecast; +package de.hatoka.eos.forecast.meteomedia; -import de.hatoka.eos.persistence.capi.WeatherForecastKey; -import de.hatoka.eos.persistence.capi.WeatherForecastPO; -import de.hatoka.eos.persistence.capi.WeatherStation; -import de.hatoka.eos.persistence.capi.WeatherDataSource; +import de.hatoka.eos.forecast.AbstractWeatherForecastImporter; +import de.hatoka.eos.persistence.capi.weather.WeatherStation; +import de.hatoka.eos.persistence.capi.weather.WeatherDataSource; import jakarta.inject.Inject; import jakarta.inject.Singleton; @@ -15,6 +14,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.text.SimpleDateFormat; import java.time.Duration; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -42,8 +42,9 @@ protected WeatherDataSource getSource() protected Map downloadAndProcessWeatherData(WeatherStation station, ZonedDateTime startDate) throws IOException, InterruptedException { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy_MM_dd"); // Download the PNG image - byte[] imageData = downloadImage(URI.create(URI_FORMAT.formatted(station.getStationNumber()))); + byte[] imageData = downloadImage(URI.create(URI_FORMAT.formatted(dateFormat.format(startDate)))); logger.debug("Downloaded {} bytes of image data", imageData.length); // Save to temporary file for processing diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherSunshineDurationConverter.java b/forecast/src/main/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherSunshineDurationConverter.java similarity index 99% rename from forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherSunshineDurationConverter.java rename to forecast/src/main/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherSunshineDurationConverter.java index 73197f8..a876575 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/MeteoMediaWeatherSunshineDurationConverter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherSunshineDurationConverter.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.forecast; +package de.hatoka.eos.forecast.meteomedia; import jakarta.inject.Singleton; import org.apache.commons.csv.CSVFormat; diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoWeatherForecastImporter.java similarity index 96% rename from forecast/src/main/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporter.java rename to forecast/src/main/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoWeatherForecastImporter.java index 03e07fc..da1ed35 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoWeatherForecastImporter.java @@ -1,9 +1,9 @@ -package de.hatoka.eos.forecast; +package de.hatoka.eos.forecast.openmeteo; import com.fasterxml.jackson.databind.ObjectMapper; -import de.hatoka.eos.forecast.openmeteo.OpenMeteoResponse; -import de.hatoka.eos.persistence.capi.WeatherStation; -import de.hatoka.eos.persistence.capi.WeatherDataSource; +import de.hatoka.eos.forecast.AbstractWeatherForecastImporter; +import de.hatoka.eos.persistence.capi.weather.WeatherStation; +import de.hatoka.eos.persistence.capi.weather.WeatherDataSource; import jakarta.inject.Singleton; import java.io.IOException; diff --git a/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics/PriceForecastExporterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/energycharts/PriceForecastExporterTest.java similarity index 91% rename from simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics/PriceForecastExporterTest.java rename to forecast/src/test/java/de/hatoka/eos/forecast/energycharts/PriceForecastExporterTest.java index 55ac6ec..10ef4cf 100644 --- a/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics/PriceForecastExporterTest.java +++ b/forecast/src/test/java/de/hatoka/eos/forecast/energycharts/PriceForecastExporterTest.java @@ -1,8 +1,7 @@ -package de.hatoka.eos.simulation.internal.business.metrics; +package de.hatoka.eos.forecast.energycharts; import de.hatoka.eos.simulation.capi.business.config.InstallationConfig; import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; -import de.hatoka.eos.simulation.capi.business.metrics.ForecastMetricsExporter; import de.hatoka.eos.simulation.internal.business.DateTooling; import de.hatoka.eos.simulation.internal.business.config.ConfigurationLoader; import io.quarkus.test.junit.QuarkusTest; @@ -23,9 +22,6 @@ public class PriceForecastExporterTest @Inject private ConfigurationLoader configurationLoader; - @Inject - private ForecastMetricsExporter metricsExporter; - @Test public void testExportEnergyPrice() throws IOException { diff --git a/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/forecast/StockEnergyPriceConverterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/energycharts/StockEnergyPriceConverterTest.java similarity index 95% rename from simulation/src/test/java/de/hatoka/eos/simulation/internal/business/forecast/StockEnergyPriceConverterTest.java rename to forecast/src/test/java/de/hatoka/eos/forecast/energycharts/StockEnergyPriceConverterTest.java index 3b485bb..8212c08 100644 --- a/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/forecast/StockEnergyPriceConverterTest.java +++ b/forecast/src/test/java/de/hatoka/eos/forecast/energycharts/StockEnergyPriceConverterTest.java @@ -1,5 +1,6 @@ -package de.hatoka.eos.simulation.internal.business.forecast; +package de.hatoka.eos.forecast.energycharts; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -35,7 +36,7 @@ void testStartDate() throws IOException LocalDate actualDate = dateExtractorResult.startDate(); LocalDate expectedDate = LocalDate.of(2025, 8, 11); assertEquals(expectedDate, actualDate); - assertEquals(6, dateExtractorResult.numberOfDays(), "incomplete week"); + Assertions.assertEquals(6, dateExtractorResult.numberOfDays(), "incomplete week"); } @Test @@ -119,8 +120,8 @@ void testExtractPriceRange() throws IOException StockEnergyPriceConverter.PriceRange priceRange = converter.extractPriceRange(svgContent); // Verify the expected price range - assertEquals(-60.0, priceRange.min(), 0.01, "Min price should be -60 EUR/MWh"); - assertEquals(300.0, priceRange.max(), 0.01, "Max price should be 300 EUR/MWh"); + Assertions.assertEquals(-60.0, priceRange.min(), 0.01, "Min price should be -60 EUR/MWh"); + Assertions.assertEquals(300.0, priceRange.max(), 0.01, "Max price should be 300 EUR/MWh"); } @Test diff --git a/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherForecastImporterTest.java similarity index 76% rename from forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java rename to forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherForecastImporterTest.java index 7f267c5..0985488 100644 --- a/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherForecastImporterTest.java +++ b/forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherForecastImporterTest.java @@ -1,10 +1,10 @@ -package de.hatoka.eos.forecast; +package de.hatoka.eos.forecast.meteomedia; -import de.hatoka.eos.persistence.capi.WeatherStation; -import de.hatoka.eos.persistence.capi.WeatherDataSource; -import de.hatoka.eos.persistence.capi.WeatherForcastDAO; -import de.hatoka.eos.persistence.capi.WeatherForecastKey; -import de.hatoka.eos.persistence.capi.WeatherForecastPO; +import de.hatoka.eos.persistence.capi.weather.WeatherStation; +import de.hatoka.eos.persistence.capi.weather.WeatherDataSource; +import de.hatoka.eos.persistence.capi.weather.WeatherForcastDAO; +import de.hatoka.eos.persistence.capi.weather.WeatherForecastKey; +import de.hatoka.eos.persistence.capi.weather.WeatherForecastPO; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; diff --git a/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherSunshineDurationConverterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherSunshineDurationConverterTest.java similarity index 99% rename from forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherSunshineDurationConverterTest.java rename to forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherSunshineDurationConverterTest.java index 949579a..aa0524d 100644 --- a/forecast/src/test/java/de/hatoka/eos/forecast/MeteoMediaWeatherSunshineDurationConverterTest.java +++ b/forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherSunshineDurationConverterTest.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.forecast; +package de.hatoka.eos.forecast.meteomedia; import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Assertions; diff --git a/forecast/src/test/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoWeatherForecastImporterTest.java similarity index 88% rename from forecast/src/test/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporterTest.java rename to forecast/src/test/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoWeatherForecastImporterTest.java index 9061db4..5956f58 100644 --- a/forecast/src/test/java/de/hatoka/eos/forecast/OpenMeteoWeatherForecastImporterTest.java +++ b/forecast/src/test/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoWeatherForecastImporterTest.java @@ -1,10 +1,10 @@ -package de.hatoka.eos.forecast; +package de.hatoka.eos.forecast.openmeteo; -import de.hatoka.eos.persistence.capi.WeatherStation; -import de.hatoka.eos.persistence.capi.WeatherDataSource; -import de.hatoka.eos.persistence.capi.WeatherForcastDAO; -import de.hatoka.eos.persistence.capi.WeatherForecastKey; -import de.hatoka.eos.persistence.capi.WeatherForecastPO; +import de.hatoka.eos.persistence.capi.weather.WeatherStation; +import de.hatoka.eos.persistence.capi.weather.WeatherDataSource; +import de.hatoka.eos.persistence.capi.weather.WeatherForcastDAO; +import de.hatoka.eos.persistence.capi.weather.WeatherForecastKey; +import de.hatoka.eos.persistence.capi.weather.WeatherForecastPO; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; diff --git a/forecast/src/test/resources/energycharts_2025_11_16.json b/forecast/src/test/resources/energycharts_2025_11_16.json new file mode 100644 index 0000000..46f8559 --- /dev/null +++ b/forecast/src/test/resources/energycharts_2025_11_16.json @@ -0,0 +1,13186 @@ +[ + { + "name": { + "en": "Hydro pumped storage consumption", + "de": "Pumpspeicher Verbrauch", + "fr": "Hydraulique STEP consommation", + "it": "Idroelettrico ad accumulazione con pompaggio consumo", + "es": "Consumo de almacenamiento por bombeo" + }, + "color": "rgb(50,50,150)", + "visible": false, + "showInLegend": true, + "showInNavigator": false, + "allowCsvDownloadForItem": true, + "yAxis": 0, + "data": [ + -956.439100183197, + -953.3163001825986, + -1311.3844002511833, + -2025.336500387934, + -1699.9965003256182, + -2519.2393004825367, + -2607.1007004993653, + -3152.341900603801, + -2733.300100523538, + -3337.119400639194, + -3780.323300724085, + -4259.546800815876, + -4285.047600820761, + -4315.525600826598, + -4385.167900839938, + -4558.3589008731105, + -4009.691000768018, + -3766.61950072146, + -3503.690200671099, + -3256.407600623734, + -3153.1658006039593, + -1099.469300210593, + -528.6673001012612, + -500.5620000958779, + -661.452300126695, + -72.79230001394268, + -16.14050000309156, + -7.120700001363903, + -4.477300000857584, + -1.8560000003554993, + -1.1291000002162686, + -2.273600000435487, + -4.451500000852643, + -4.510100000863868, + -4.493600000860707, + -5.7443000011002665, + -10.437900001999282, + -12.857500002462732, + -13.747400002633185, + -348.37140006672723, + -126.83800002429462, + -80.22770001536686, + -82.37590001577833, + -340.9959000653146, + -59.540900011404496, + -245.65750004705336, + -247.66960004743876, + -277.1513000530857, + -444.0131000850465, + -312.0302000597664, + -568.9782001089824, + -600.796800115077, + -506.4206000970001, + -599.4883001148263, + -859.0802001645487, + -710.2767001360468, + -1020.0070001953727, + -735.7675001409293, + -228.61180004378843, + -18.812600003603375, + -8.664000001659508, + -6.354700001217183, + -4.911000000940655, + -2.3671000004533957, + -1.0498000002010794, + -0.9700000001857944, + -0.9494000001818487, + -1.674700000320773, + -2.1021000004026376, + -1.0842000002076684, + -0.07330000001403993, + -0.07320000001402077, + -0.7497000001435979, + -0.6831000001308414, + -0.9823000001881502, + -1.0719000002053125, + -3.751400000718545, + -5.155600000987506, + -5.274400001010261, + -102.21560001957843, + -157.89630003024354, + -224.41400004298438, + -284.97810005458484, + -299.0448000572792, + -16.906500003238282, + -32.80550000628359, + -195.4923000374447, + -320.55840006139994, + -301.3760000577257, + -486.2082000931286, + -785.0125001503618, + -1511.3499002894848, + -1478.06800028311, + -1615.5260003094388, + -2310.8019004426124, + -2948.081100564677, + -1401.536500268451, + -1648.2476003157062, + -1975.2086003783324, + -2682.6761005138414, + -2382.701800456384, + -2810.324100538291, + -3418.2133006547265, + -3764.696200721092, + -3326.437700637148, + -3507.9556006719154, + -3814.5259007306367, + -3855.5156007384876, + -4424.103600847396, + -4571.7656008756785, + -4587.613500878714, + -4091.855200783756, + -4545.2541008706, + -4544.773000870508, + -4413.9894008454585, + -4368.6970008367825, + -3166.7231006065563, + -3755.96680071942, + -2857.14890054726, + -1488.7287002851517, + -1175.2669002251114, + -286.7176000549181, + -15.031600002879161, + -10.550800002020907, + -9.524500001824327, + -5.585200001069793, + -6.242400001195673, + -4.449900000852336, + -1.2597000002412837, + -1.2776000002447123, + -1.2416000002378167, + -1.236600000236859, + -1.738400000332974, + -10.056600001926247, + -7.073200001354804, + -7.147400001369017, + -7.082900001356663, + -10.186700001951166, + -11.042900002115163, + -21.550400004127777, + -268.49840005142835, + -290.6638000556739, + -300.02080005746615, + -307.40930005888134, + -386.0074000739361, + -328.6134000629428, + -318.7483000610532, + -263.72470005051395, + -856.6264001640787, + -738.2075001413966, + -469.8548000899963, + -524.0433001003756, + -562.2131001076866, + -317.76360006086463, + -124.15430002378059, + -39.09180000748766, + -243.7933000466963, + -243.23090004658857, + -58.20980001114954, + -20.337700003895495, + -27.994400005362063, + -45.70560000875447, + -62.41440001195489, + -9.547400001828715, + -24.63190000471801, + -1.021600000195678, + -1.0229000001959268, + -1.0550000002020752, + -1.0227000001958886, + -1.3240000002535999, + -1.6236000003109852, + -1.6810000003219798, + -2.7041000005179447, + -11.933700002285788, + -13.702300002624545, + -169.38520003244415, + -207.01490003965176, + -92.31190001768147, + -155.73300002982918, + -476.81560009132954, + -351.7255000673697, + -305.9658000586049, + -833.8306001597124, + -702.7355001346024, + -122.69190002350048, + -653.2272001251196, + -881.0939001687652, + -1264.771600242255, + -763.5220001462454, + -889.0447001702881, + -1284.5878002460506, + -2281.156000436934, + -1386.5651002655834, + -2009.0226003848093, + -1913.6021003665326, + -2199.5317004212993, + -2676.9972005127534, + -2946.304600564337, + -3221.3552006170203, + -3200.633700613051, + -2965.9840005681062, + -3039.1148005821137, + -3065.9358005872514, + -2909.4938005572862, + -2718.9525005207897, + -2899.9625005554603, + -2740.8689005249876, + -3488.5121006681916, + -3515.8198006734224, + -3459.3862006626127, + -3417.6140006546116, + -3532.692500676654, + -3455.446500661858, + -3151.163100603576, + -2287.2404004380996, + -1749.4262003350862, + -936.8180001794386, + -309.37920005925866, + -62.30700001193432, + -16.331300003128106, + -4.788100000917115, + -3.913400000749575, + -9.495600001818792, + -3.118900000597396, + -3.3358000006389408, + -2.1017000004025608, + -4.022300000770434, + -4.329900000829352, + -6.091500001166769, + -187.61890003593663, + -378.4310000724849, + -778.9616001492027, + -274.36700005255244, + -503.88990009651536, + -1616.0116003095318, + -1513.6285002899212, + -1007.7777001930303, + -1185.7941002271277, + -1202.9402002304118, + -1484.9270002844237, + -1307.1306002503684, + -1744.8007003342002, + -1739.0136003330917, + -1769.040000338843, + -1888.4696003617184, + -1771.7476003393615, + -1217.5827002332164, + -964.3168001847057, + -1223.3509002343212, + -231.7133000443825, + -18.959900003631592, + -4.799600000919318, + -2.5375000004860344, + -1.3958000002673523, + -1.3335000002554191, + -3.5172000006736863, + -5.508700001055139, + -4.871000000932995, + -77.43230001483143, + -7.58090000145205, + -12.90270000247139, + -4.972300000952397, + -4.435100000849502, + -5.313300001017712, + -5.378900001030277, + -169.4205000324509, + -228.08040004368664, + -227.89760004365164, + -227.87590004364748, + -227.9600000436636, + -239.58610004589045, + -468.4486000897269, + -434.1280000831531, + -895.4419001715135, + -1010.9159001936314, + -1094.5745002096553, + -833.9068001597269, + -977.9547001873179, + -1442.0032002762023, + -1599.9605003064573, + -1071.3665002052103, + -1425.1005002729646, + -1484.192800284283, + -1795.5119003439133, + -2020.3174003869726, + -2894.656200554444, + -3323.6676006366174, + -3685.280000705881, + -3817.617600731229, + -4343.786000832011, + -3774.7023007230086, + -3925.0556007518076, + -3675.9115007040864, + -4252.417800814511, + -4703.2200009008575, + -4423.653700847309, + -4540.0336008696, + -4779.6661009155005, + -4740.825300908061, + -4352.298600833642, + -4493.059400860603, + -4483.53610085878, + -4414.790000845612, + -4292.6863008222235, + -4316.759400826834, + -4404.725000843684, + -4096.074600784565, + -3988.720400764002, + -4224.437300809151, + -3508.76190067207, + -3229.6824006186152, + -2682.3368005137763, + -3620.01720069338, + -2333.608400446981, + -1629.4114003120983, + -1068.3623002046347, + -763.2849001462, + -632.5631001211615, + -354.48540006789835, + -350.79120006719074, + -349.39470006692324, + -297.1629000569187, + -220.1096000421599, + -227.8568000436438, + -220.8953000423104, + -281.68040005395324, + -939.6052001799725, + -1378.9757002641297, + -1579.9846003026312, + -2710.7269005192143, + -2708.299800518749, + -3072.843700588574, + -2690.833200515404, + -2655.4542005086273, + -2689.1104005150737, + -2744.01240052559, + -2962.053000567353, + -2959.7988005669217, + -2831.5439005423555, + -2262.9006004334374, + -2146.9225004112227, + -1720.2364003294952, + -1355.6152002596552, + -1264.765400242254, + -1248.0857002390592, + -1090.3025002088373, + -952.524200182447, + -329.5130000631151, + -471.1989000902537, + -233.86830004479526, + -29.984700005743285, + -122.13330002339349, + -220.63650004226085, + -499.86290009574407, + -602.6891001154395, + -416.76630007982766, + -14.974200002868166, + -6.297400001206207, + -6.089000001166291, + -6.059700001160679, + -21.90310000419533, + -18.558700003554744, + -15.343900002938978, + -16.71630000320185, + -113.27260002169629, + -35.15740000673407, + -23.252700004453835, + -15.976400003060128, + -260.0765000498152, + -59.8735000114682, + -66.0523000126517, + -49.47140000947578, + -31.04440000594626, + -116.19120002225532, + -288.9868000553527, + -317.60150006083353, + -46.732000008951076, + -66.80060001279503, + -343.2033000657374, + -659.4660001263145, + -107.69870002062868, + -276.4123000529442, + -574.2623001099945, + -712.240800136423, + -228.16800004370344, + -257.4302000493083, + -1423.4560002726494, + -1544.524300295839, + -532.1402001019264, + -629.9996001206705, + -1459.4960002795528, + -1526.9892002924803, + -1718.759400329212, + -1819.844400348574, + -1838.498200352147, + -1067.6463002044977, + -1034.7592001981984, + -869.3024001665067, + -868.1388001662838, + -885.6861001696449, + -933.5592001788145, + -896.6526001717453, + -926.910900177541, + -973.3735001864405, + -1285.2018002461684, + -1341.2281002568996, + -747.8608001432457, + -201.44070003858405, + -26.72650000511921, + -14.780200002831009, + -32.07140000614297, + -156.8153000300365, + -151.2500000289705, + -109.36820002094845, + -117.01060002241228, + -74.03190001418011, + -0.0716000000137143, + -1.579900000302615, + -8.503600001628785, + -105.30610002017039, + -22.22040000425611, + -31.52110000603757, + -11.723400002245507, + -10.048700001924734, + -5.848400001120206, + -7.035800001347641, + -16.59100000317785, + -192.9241000369528, + -16.286400003119507, + -12.262600002348787, + -24.093300004614843, + -196.23350003758668, + -20.292100003886762, + -26.720000005117964, + -16.749900003208285, + -25.97820000497588, + -20.780800003980364, + -16.961600003248837, + -17.848500003418714, + -38.26940000733014, + -88.14090001688255, + -34.0664000065251, + -23.35840000447408, + -10.280600001969152, + -49.761500009531346, + -166.27040003184752, + -59.13670001132707, + -30.423300005827297, + -11.866500002272916, + -3.620900000693549, + -1.5401000002949916, + -1.1835000002266882, + -4.077800000781064, + -6.690500001281502, + -8.999600001723788, + -8.665100001659718, + -95.5176000182955, + -191.7603000367299, + -316.26670006057793, + -361.31650006920677, + -242.9141000465279, + -278.7853000533987, + -437.94850008388494, + -721.669300138229, + -212.86990004077322, + -257.9075000493998, + -397.02360007604614, + -463.938100088863, + -119.20960002283347, + -563.6636001079644, + -1049.835400201086, + -1070.7967002051012, + -481.2850000921856, + -655.991400125649, + -1020.6999001955054, + -1778.5207003406588, + -1577.4646003021485, + -2052.0963003930597, + -1959.8230003753856, + -2016.7191003862836, + -1857.1439003557184, + -2034.8520003897568, + -2524.237500483494, + -2508.934400480563, + -2443.778800468083, + -2808.016800537849, + -2899.689300555408, + -3278.9590006280537, + -3019.25590057831, + -3635.4462006963354, + -3793.7913007266648, + -4044.3206007746517, + -4023.9272007707455, + -4075.4893007806218, + -4361.951600835491, + -4487.807400859597, + -4504.26360086275, + -4547.358400871004, + -4604.9325008820315, + -4966.928100951368, + -5061.251400969435, + -5143.824600985251, + -5207.297400997409, + -4667.740400894061, + -4949.9950009481245, + -4849.613900928898, + -4834.9796009260945, + -4386.775900840245, + -4138.0335007926005, + -3337.444700639256, + -2545.0472004874796, + -1587.4219003040557, + -1024.336000196202, + -1349.9925002585783, + -1173.5093002247745, + -1220.2115002337198, + -968.4889001855048, + -1100.4219002107754, + -2021.4862003871967, + -2188.764000419237, + -1097.4572002102077, + -1126.9980002158659, + -1914.201400366647, + -1978.4581003789551, + -2322.3804004448302, + -1327.2761002542272, + -1325.432200253874, + -1587.575300304085, + -1493.6321002860911, + -969.0777001856177, + -1500.161800287342, + -1485.0152002844407, + -1463.246800280271, + -1279.822200245138, + -799.5185001531403, + -553.229900105966, + -457.53910008763734, + -48.46340000928271, + -18.947200003629156, + -18.89010000361822, + -17.309900003315548, + -16.848000003227074, + -10.446900002001005, + -10.47120000200566, + -18.24820000349527, + -11.996200002297758, + -11.029900002112672, + -10.074900001929752, + -9.768200001871007, + -11.991600002296877, + -14.305100002740007, + -14.097600002700261, + -77.37550001482055, + -163.9590000314048, + -310.48840005947113, + -269.1212000515476, + -223.62130004283253, + -198.73230003806532, + -183.2635000351024, + -157.79400003022397, + -206.5427000395613, + -402.7683000771465, + -871.627900166952, + -679.4518001301426, + -386.6193000740533, + -310.86440005954313, + -285.6675000547169, + -350.009600067041, + -250.16770004791726, + -249.62110004781258, + -262.79860005033663, + -483.0576000925251, + -578.8292001108692, + -614.9230001177827, + -617.8502001183433, + -871.7168001669692, + -91.98490001761884, + -55.087200010551435, + -140.1975000268535, + -406.85730007792966, + -234.3728000448919, + -448.239400085856, + -1299.6388002489334, + -1330.5672002548577, + -1049.1961002009634, + -931.5618001784318, + -1149.6956002202135, + -1313.5111002515905, + -1090.1352002088051, + -1040.4926001992965, + -609.8144001168042, + -563.7929001079892, + -602.9785001154949, + -570.5690001092871, + -662.1174001268223, + -847.042400162243, + -392.8258000752421, + -417.71690008000974, + -422.78160008097984, + -438.3023000839527, + -1714.7605003284464, + -1644.3178003149535, + -1068.4756002046563, + -958.7563001836407, + -841.2027001611245, + -1027.359800196781, + -859.1116001645547, + -429.74540008231367, + -270.54450005182025, + -135.5981000259725, + -146.0166000279681, + -476.67250009130214, + -1294.9769002480405, + -889.0351001702863, + -1014.9676001944074, + -988.704700189377, + -281.6489000539472, + -110.33510002113364, + -84.19800001612732, + -83.23240001594237, + -51.94190000994898, + -19.030300003645074, + -18.01070000344978, + -19.28160000369321, + -18.343500003513523, + -17.96000000344007, + -17.683300003387068, + -17.80030000340948, + -76.50790001465437, + -81.59460001562867, + -150.42850002881318, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + "xAxisValues": [ + 1762729200000, + 1762730100000, + 1762731000000, + 1762731900000, + 1762732800000, + 1762733700000, + 1762734600000, + 1762735500000, + 1762736400000, + 1762737300000, + 1762738200000, + 1762739100000, + 1762740000000, + 1762740900000, + 1762741800000, + 1762742700000, + 1762743600000, + 1762744500000, + 1762745400000, + 1762746300000, + 1762747200000, + 1762748100000, + 1762749000000, + 1762749900000, + 1762750800000, + 1762751700000, + 1762752600000, + 1762753500000, + 1762754400000, + 1762755300000, + 1762756200000, + 1762757100000, + 1762758000000, + 1762758900000, + 1762759800000, + 1762760700000, + 1762761600000, + 1762762500000, + 1762763400000, + 1762764300000, + 1762765200000, + 1762766100000, + 1762767000000, + 1762767900000, + 1762768800000, + 1762769700000, + 1762770600000, + 1762771500000, + 1762772400000, + 1762773300000, + 1762774200000, + 1762775100000, + 1762776000000, + 1762776900000, + 1762777800000, + 1762778700000, + 1762779600000, + 1762780500000, + 1762781400000, + 1762782300000, + 1762783200000, + 1762784100000, + 1762785000000, + 1762785900000, + 1762786800000, + 1762787700000, + 1762788600000, + 1762789500000, + 1762790400000, + 1762791300000, + 1762792200000, + 1762793100000, + 1762794000000, + 1762794900000, + 1762795800000, + 1762796700000, + 1762797600000, + 1762798500000, + 1762799400000, + 1762800300000, + 1762801200000, + 1762802100000, + 1762803000000, + 1762803900000, + 1762804800000, + 1762805700000, + 1762806600000, + 1762807500000, + 1762808400000, + 1762809300000, + 1762810200000, + 1762811100000, + 1762812000000, + 1762812900000, + 1762813800000, + 1762814700000, + 1762815600000, + 1762816500000, + 1762817400000, + 1762818300000, + 1762819200000, + 1762820100000, + 1762821000000, + 1762821900000, + 1762822800000, + 1762823700000, + 1762824600000, + 1762825500000, + 1762826400000, + 1762827300000, + 1762828200000, + 1762829100000, + 1762830000000, + 1762830900000, + 1762831800000, + 1762832700000, + 1762833600000, + 1762834500000, + 1762835400000, + 1762836300000, + 1762837200000, + 1762838100000, + 1762839000000, + 1762839900000, + 1762840800000, + 1762841700000, + 1762842600000, + 1762843500000, + 1762844400000, + 1762845300000, + 1762846200000, + 1762847100000, + 1762848000000, + 1762848900000, + 1762849800000, + 1762850700000, + 1762851600000, + 1762852500000, + 1762853400000, + 1762854300000, + 1762855200000, + 1762856100000, + 1762857000000, + 1762857900000, + 1762858800000, + 1762859700000, + 1762860600000, + 1762861500000, + 1762862400000, + 1762863300000, + 1762864200000, + 1762865100000, + 1762866000000, + 1762866900000, + 1762867800000, + 1762868700000, + 1762869600000, + 1762870500000, + 1762871400000, + 1762872300000, + 1762873200000, + 1762874100000, + 1762875000000, + 1762875900000, + 1762876800000, + 1762877700000, + 1762878600000, + 1762879500000, + 1762880400000, + 1762881300000, + 1762882200000, + 1762883100000, + 1762884000000, + 1762884900000, + 1762885800000, + 1762886700000, + 1762887600000, + 1762888500000, + 1762889400000, + 1762890300000, + 1762891200000, + 1762892100000, + 1762893000000, + 1762893900000, + 1762894800000, + 1762895700000, + 1762896600000, + 1762897500000, + 1762898400000, + 1762899300000, + 1762900200000, + 1762901100000, + 1762902000000, + 1762902900000, + 1762903800000, + 1762904700000, + 1762905600000, + 1762906500000, + 1762907400000, + 1762908300000, + 1762909200000, + 1762910100000, + 1762911000000, + 1762911900000, + 1762912800000, + 1762913700000, + 1762914600000, + 1762915500000, + 1762916400000, + 1762917300000, + 1762918200000, + 1762919100000, + 1762920000000, + 1762920900000, + 1762921800000, + 1762922700000, + 1762923600000, + 1762924500000, + 1762925400000, + 1762926300000, + 1762927200000, + 1762928100000, + 1762929000000, + 1762929900000, + 1762930800000, + 1762931700000, + 1762932600000, + 1762933500000, + 1762934400000, + 1762935300000, + 1762936200000, + 1762937100000, + 1762938000000, + 1762938900000, + 1762939800000, + 1762940700000, + 1762941600000, + 1762942500000, + 1762943400000, + 1762944300000, + 1762945200000, + 1762946100000, + 1762947000000, + 1762947900000, + 1762948800000, + 1762949700000, + 1762950600000, + 1762951500000, + 1762952400000, + 1762953300000, + 1762954200000, + 1762955100000, + 1762956000000, + 1762956900000, + 1762957800000, + 1762958700000, + 1762959600000, + 1762960500000, + 1762961400000, + 1762962300000, + 1762963200000, + 1762964100000, + 1762965000000, + 1762965900000, + 1762966800000, + 1762967700000, + 1762968600000, + 1762969500000, + 1762970400000, + 1762971300000, + 1762972200000, + 1762973100000, + 1762974000000, + 1762974900000, + 1762975800000, + 1762976700000, + 1762977600000, + 1762978500000, + 1762979400000, + 1762980300000, + 1762981200000, + 1762982100000, + 1762983000000, + 1762983900000, + 1762984800000, + 1762985700000, + 1762986600000, + 1762987500000, + 1762988400000, + 1762989300000, + 1762990200000, + 1762991100000, + 1762992000000, + 1762992900000, + 1762993800000, + 1762994700000, + 1762995600000, + 1762996500000, + 1762997400000, + 1762998300000, + 1762999200000, + 1763000100000, + 1763001000000, + 1763001900000, + 1763002800000, + 1763003700000, + 1763004600000, + 1763005500000, + 1763006400000, + 1763007300000, + 1763008200000, + 1763009100000, + 1763010000000, + 1763010900000, + 1763011800000, + 1763012700000, + 1763013600000, + 1763014500000, + 1763015400000, + 1763016300000, + 1763017200000, + 1763018100000, + 1763019000000, + 1763019900000, + 1763020800000, + 1763021700000, + 1763022600000, + 1763023500000, + 1763024400000, + 1763025300000, + 1763026200000, + 1763027100000, + 1763028000000, + 1763028900000, + 1763029800000, + 1763030700000, + 1763031600000, + 1763032500000, + 1763033400000, + 1763034300000, + 1763035200000, + 1763036100000, + 1763037000000, + 1763037900000, + 1763038800000, + 1763039700000, + 1763040600000, + 1763041500000, + 1763042400000, + 1763043300000, + 1763044200000, + 1763045100000, + 1763046000000, + 1763046900000, + 1763047800000, + 1763048700000, + 1763049600000, + 1763050500000, + 1763051400000, + 1763052300000, + 1763053200000, + 1763054100000, + 1763055000000, + 1763055900000, + 1763056800000, + 1763057700000, + 1763058600000, + 1763059500000, + 1763060400000, + 1763061300000, + 1763062200000, + 1763063100000, + 1763064000000, + 1763064900000, + 1763065800000, + 1763066700000, + 1763067600000, + 1763068500000, + 1763069400000, + 1763070300000, + 1763071200000, + 1763072100000, + 1763073000000, + 1763073900000, + 1763074800000, + 1763075700000, + 1763076600000, + 1763077500000, + 1763078400000, + 1763079300000, + 1763080200000, + 1763081100000, + 1763082000000, + 1763082900000, + 1763083800000, + 1763084700000, + 1763085600000, + 1763086500000, + 1763087400000, + 1763088300000, + 1763089200000, + 1763090100000, + 1763091000000, + 1763091900000, + 1763092800000, + 1763093700000, + 1763094600000, + 1763095500000, + 1763096400000, + 1763097300000, + 1763098200000, + 1763099100000, + 1763100000000, + 1763100900000, + 1763101800000, + 1763102700000, + 1763103600000, + 1763104500000, + 1763105400000, + 1763106300000, + 1763107200000, + 1763108100000, + 1763109000000, + 1763109900000, + 1763110800000, + 1763111700000, + 1763112600000, + 1763113500000, + 1763114400000, + 1763115300000, + 1763116200000, + 1763117100000, + 1763118000000, + 1763118900000, + 1763119800000, + 1763120700000, + 1763121600000, + 1763122500000, + 1763123400000, + 1763124300000, + 1763125200000, + 1763126100000, + 1763127000000, + 1763127900000, + 1763128800000, + 1763129700000, + 1763130600000, + 1763131500000, + 1763132400000, + 1763133300000, + 1763134200000, + 1763135100000, + 1763136000000, + 1763136900000, + 1763137800000, + 1763138700000, + 1763139600000, + 1763140500000, + 1763141400000, + 1763142300000, + 1763143200000, + 1763144100000, + 1763145000000, + 1763145900000, + 1763146800000, + 1763147700000, + 1763148600000, + 1763149500000, + 1763150400000, + 1763151300000, + 1763152200000, + 1763153100000, + 1763154000000, + 1763154900000, + 1763155800000, + 1763156700000, + 1763157600000, + 1763158500000, + 1763159400000, + 1763160300000, + 1763161200000, + 1763162100000, + 1763163000000, + 1763163900000, + 1763164800000, + 1763165700000, + 1763166600000, + 1763167500000, + 1763168400000, + 1763169300000, + 1763170200000, + 1763171100000, + 1763172000000, + 1763172900000, + 1763173800000, + 1763174700000, + 1763175600000, + 1763176500000, + 1763177400000, + 1763178300000, + 1763179200000, + 1763180100000, + 1763181000000, + 1763181900000, + 1763182800000, + 1763183700000, + 1763184600000, + 1763185500000, + 1763186400000, + 1763187300000, + 1763188200000, + 1763189100000, + 1763190000000, + 1763190900000, + 1763191800000, + 1763192700000, + 1763193600000, + 1763194500000, + 1763195400000, + 1763196300000, + 1763197200000, + 1763198100000, + 1763199000000, + 1763199900000, + 1763200800000, + 1763201700000, + 1763202600000, + 1763203500000, + 1763204400000, + 1763205300000, + 1763206200000, + 1763207100000, + 1763208000000, + 1763208900000, + 1763209800000, + 1763210700000, + 1763211600000, + 1763212500000, + 1763213400000, + 1763214300000, + 1763215200000, + 1763216100000, + 1763217000000, + 1763217900000, + 1763218800000, + 1763219700000, + 1763220600000, + 1763221500000, + 1763222400000, + 1763223300000, + 1763224200000, + 1763225100000, + 1763226000000, + 1763226900000, + 1763227800000, + 1763228700000, + 1763229600000, + 1763230500000, + 1763231400000, + 1763232300000, + 1763233200000, + 1763234100000, + 1763235000000, + 1763235900000, + 1763236800000, + 1763237700000, + 1763238600000, + 1763239500000, + 1763240400000, + 1763241300000, + 1763242200000, + 1763243100000, + 1763244000000, + 1763244900000, + 1763245800000, + 1763246700000, + 1763247600000, + 1763248500000, + 1763249400000, + 1763250300000, + 1763251200000, + 1763252100000, + 1763253000000, + 1763253900000, + 1763254800000, + 1763255700000, + 1763256600000, + 1763257500000, + 1763258400000, + 1763259300000, + 1763260200000, + 1763261100000, + 1763262000000, + 1763262900000, + 1763263800000, + 1763264700000, + 1763265600000, + 1763266500000, + 1763267400000, + 1763268300000, + 1763269200000, + 1763270100000, + 1763271000000, + 1763271900000, + 1763272800000, + 1763273700000, + 1763274600000, + 1763275500000, + 1763276400000, + 1763277300000, + 1763278200000, + 1763279100000, + 1763280000000, + 1763280900000, + 1763281800000, + 1763282700000, + 1763283600000, + 1763284500000, + 1763285400000, + 1763286300000, + 1763287200000, + 1763288100000, + 1763289000000, + 1763289900000, + 1763290800000, + 1763291700000, + 1763292600000, + 1763293500000, + 1763294400000, + 1763295300000, + 1763296200000, + 1763297100000, + 1763298000000, + 1763298900000, + 1763299800000, + 1763300700000, + 1763301600000, + 1763302500000, + 1763303400000, + 1763304300000, + 1763305200000, + 1763306100000, + 1763307000000, + 1763307900000, + 1763308800000, + 1763309700000, + 1763310600000, + 1763311500000, + 1763312400000, + 1763313300000, + 1763314200000, + 1763315100000, + 1763316000000, + 1763316900000, + 1763317800000, + 1763318700000, + 1763319600000, + 1763320500000, + 1763321400000, + 1763322300000, + 1763323200000, + 1763324100000, + 1763325000000, + 1763325900000, + 1763326800000, + 1763327700000, + 1763328600000, + 1763329500000, + 1763330400000, + 1763331300000, + 1763332200000, + 1763333100000 + ], + "format": "Highcharts", + "xAxisFormat": "unixTime", + "timeZone": "UTC", + "y0AxisDecimalPlaces": 2, + "showNavigator": true, + "datasource": "ENTSO-E, Netztransparenz, EPEX SPOT", + "date": 1763300628246, + "stacking": "stacked_absolute_area", + "xAxisLabel": [ + { + "en": "Date", + "de": "Datum", + "fr": "Date", + "es": "Fecha", + "it": "Data" + } + ], + "y0AxisLabel": [ + { + "en": "Power (MW)", + "de": "Leistung (MW)", + "fr": "Puissance (MW)", + "es": "Potencia (MW)", + "it": "Prestazione (MW)" + } + ], + "y1AxisLabel": [ + { + "en": "Price (EUR/MWh, EUR/tCO2)", + "de": "Preis (EUR/MWh, EUR/tCO2)", + "fr": "Prix (EUR/MWh, EUR/tCO2)", + "it": "Prestazione (EUR/MWh, EUR/tCO2)", + "es": "Precio (EUR/MWh, EUR/tCO2)" + } + ], + "y1AxisDecimalPlaces": 2, + "y2AxisLabel": [ + { + "en": "Price ()", + "de": "Preis ()", + "fr": "Prix ()", + "it": "Prestazione ()", + "es": "Precio ()" + } + ], + "y2AxisDecimalPlaces": 2, + "chartTitle": [ + { + "en": "Electricity production and spot prices in Germany in week 46 2025", + "de": "Stromproduktion und B\u00f6rsenstrompreise in Deutschland in Woche 46 2025", + "fr": "Production \u00e9lectrique et prix d'\u00e9change (spot) de l'\u00e9lectricit\u00e9 en Allemagne pour semaine 46 2025", + "it": "La produzione di elettricit\u00e0 e spot price in Germania in settimana 46 2025", + "es": "Producci\u00f3n de electricidad y precios spot en Alemania en la semana 46 2025" + } + ] + }, + { + "name": { + "en": "Cross border electricity trading", + "de": "Grenz\u00fcberschreitender Stromhandel", + "fr": "Les \u00e9changes commerciaux aux fronti\u00e8res", + "es": "Comercio transfronterizo de electricidad", + "it": "Commercio transfrontaliero di energia elettrica" + }, + "color": "rgb(125,25,125)", + "type": "area", + "visible": false, + "showInLegend": true, + "showInNavigator": false, + "allowCsvDownloadForItem": true, + "yAxis": 0, + "data": [ + 6389.678750000001, + 6615.67875, + 6909.22475, + 7236.80275, + 6992.878250000001, + 7296.88525, + 7452.4442500000005, + 7595.22125, + 7342.116499999999, + 7382.5725, + 7708.149500000001, + 7790.812500000001, + 8093.428250000001, + 8127.757250000001, + 8183.41925, + 8202.174249999998, + 8692.0125, + 8416.2915, + 8231.1775, + 7858.4075, + 8110.9580000000005, + 6510.758, + 6256.158000000001, + 6948.258, + 9182.530250000002, + 9352.03025, + 10079.030249999998, + 9196.030250000002, + 10287.018499999998, + 9888.2675, + 9432.367499999998, + 9405.6675, + 10253.222249999999, + 10496.122249999999, + 10302.52225, + 10432.122249999999, + 9426.69825, + 10205.79825, + 10230.59825, + 10670.224250000001, + 9571.38025, + 9929.680250000003, + 10121.38025, + 10155.580249999999, + 8163.623999999999, + 8204.124, + 7978.833999999998, + 7905.925000000002, + 8029.751500000001, + 7846.8515, + 7708.1715, + 7562.4515, + 6709.7315, + 6744.4315, + 6893.231500000001, + 6795.131499999999, + 7348.5755, + 7213.5625, + 7223.462500000001, + 7060.5625, + 8117.670499999999, + 7806.470500000001, + 8022.370500000001, + 7855.1705, + 8478.31875, + 8283.845749999999, + 8112.527749999998, + 7862.927749999999, + 7944.879500000002, + 8205.6795, + 7923.108500000002, + 8095.279500000001, + 6391.5422499999995, + 6120.135249999999, + 6093.14225, + 6356.44225, + 5259.664749999999, + 4986.164749999999, + 4998.36475, + 5007.364750000001, + 4947.173000000001, + 5408.773, + 5122.673, + 5025.773000000001, + 3663.0697500000006, + 3367.269749999999, + 3083.8697500000003, + 3216.5697500000006, + 4019.3534999999993, + 4048.2534999999993, + 3947.353500000001, + 4056.0535, + 3086.43075, + 2990.8307499999996, + 3264.1307500000007, + 3387.33075, + 1227.4582500000006, + 1283.3582500000005, + 1330.7582500000003, + 1492.4582500000006, + 1023.6182500000004, + 1248.9182499999997, + 1298.0182499999999, + 1586.3182499999996, + 1053.2452499999997, + 1222.9452500000002, + 1224.8452499999999, + 1318.6452499999991, + 2651.0302500000003, + 2418.5302500000016, + 2497.480249999999, + 1720.7302499999994, + 3688.859749999999, + 3620.5597499999985, + 3396.059750000001, + 3214.259750000001, + 3420.3044999999997, + 3366.504500000001, + 3547.2045000000003, + 3460.3044999999997, + 4803.2725, + 4523.6725, + 4007.5725, + 3880.372499999999, + 3561.6465000000007, + 3530.4465, + 3546.1465, + 3532.9465000000005, + 3347.2215, + 3401.3214999999996, + 3309.2214999999987, + 3220.8215, + 473.5292499999995, + 944.8292499999994, + 1580.1292499999995, + 1365.2292499999999, + 1125.9807499999997, + 1191.5807500000003, + 843.9807500000003, + 585.8807500000004, + 330.1019999999996, + 176.20199999999997, + 90.0019999999988, + 470.7019999999992, + 895.9590000000004, + 785.3589999999997, + 1041.4589999999996, + 1411.1589999999999, + 2217.635, + 2492.5350000000003, + 2759.1349999999998, + 3143.0349999999994, + 2774.48425, + 3029.82225, + 3662.3222499999993, + 3758.02225, + 4881.733499999999, + 5425.933500000001, + 5808.7335, + 5957.233500000001, + 7099.2595, + 6843.259499999998, + 6583.459499999999, + 6619.059500000001, + 6482.7955, + 6432.194499999999, + 6514.395499999999, + 6667.995499999999, + 6427.6447499999995, + 7199.944750000001, + 7250.54475, + 7215.244749999999, + 7020.8942499999985, + 6889.49425, + 7077.79425, + 6897.194250000001, + 4912.012500000002, + 5610.3125, + 5220.4125, + 5276.612499999999, + 5298.2480000000005, + 5004.548, + 5499.648, + 5442.548000000001, + 5960.009999999999, + 5776.71, + 5346.21, + 5475.41, + 5168.020750000001, + 5460.2207499999995, + 5298.820749999998, + 5516.020749999999, + 5428.4235, + 5483.0235, + 5321.1235, + 5274.0235, + 6072.923250000002, + 6001.623250000001, + 5962.92325, + 5998.623250000001, + 6004.7927500000005, + 6017.692750000001, + 5860.49275, + 5910.892750000001, + 6062.602999999998, + 6151.503000000001, + 6020.503000000001, + 5955.202999999999, + 6156.74875, + 5856.94875, + 5983.748749999999, + 6109.748749999999, + 6669.334250000001, + 6561.13425, + 6613.7342499999995, + 6474.434249999999, + 5816.403, + 6032.703, + 5500.803, + 5081.703, + 4317.585999999999, + 4615.986000000001, + 4207.686000000001, + 3805.7860000000005, + 3654.26225, + 3454.152250000001, + 3059.632250000001, + 2154.292249999999, + 579.8257500000003, + 402.2257500000004, + 418.0257500000011, + 447.325750000001, + -674.9482500000004, + -455.9682500000002, + 27.231749999999916, + -202.36824999999968, + -1361.859, + -1296.959, + -1335.2590000000005, + -1294.5590000000002, + -1627.1592499999997, + -1768.1592500000008, + -1207.0592500000002, + -1083.7592500000003, + -1786.6675000000007, + -1582.5675000000008, + -1445.5675000000003, + -1263.0674999999999, + 558.60225, + -114.19775000000007, + 202.50225000000003, + 222.00224999999986, + 2576.9062500000005, + 2766.10625, + 2622.706250000001, + 2407.8062500000005, + 4327.37175, + 3905.97175, + 3272.17175, + 2681.1717499999995, + 2324.9670000000006, + 2375.2670000000003, + 2265.2670000000003, + 2263.567, + 2130.715, + 2179.5150000000003, + 2204.3149999999996, + 2208.2149999999997, + 1528.38775, + 1364.3877500000006, + 1221.5877500000001, + 1013.9877499999992, + 974.696499999999, + 1047.7965000000002, + 1224.6964999999996, + 1162.5964999999997, + 469.7877500000001, + 172.58774999999994, + 651.7477500000006, + 840.9877500000005, + 1215.3732499999999, + 1201.1732499999996, + 804.0732499999999, + 629.1732500000006, + 1220.49075, + 1185.49075, + 1162.2907499999992, + 1052.0907500000005, + -4.738749999999653, + -572.5387499999997, + -778.5387500000002, + -1000.5387500000003, + -373.1167500000003, + -587.7167499999989, + -735.0267500000003, + -1020.9567500000003, + -562.7694999999997, + -636.0794999999997, + -720.8995000000008, + -668.8995000000009, + -65.18324999999977, + 84.82674999999931, + 96.79675000000069, + 4.996750000000133, + 239.3799999999995, + 88.48000000000036, + -219.41999999999993, + 146.38000000000065, + 1401.813000000001, + 1049.3129999999996, + 1298.7129999999995, + 1398.4130000000002, + 3522.5205000000005, + 3489.6205, + 3463.9205, + 3407.720500000001, + 178.52424999999997, + 17.824250000000124, + -171.87574999999987, + -442.07574999999946, + -1183.0395000000003, + -1327.9395, + -1420.7395000000001, + -1512.0394999999996, + -4243.269499999999, + -4633.5695000000005, + -4552.2695, + -5358.6695, + -5658.006999999998, + -6194.107000000001, + -7033.006999999999, + -7315.307, + -8207.313, + -8329.513000000003, + -8302.313, + -7883.112999999999, + -7718.5914999999995, + -7344.991500000002, + -6896.391499999999, + -6726.7915, + -5060.958249999999, + -4892.95825, + -4667.758249999999, + -4168.358249999999, + -2907.9802500000005, + -2350.7802500000003, + -2243.9802499999996, + -2102.3802499999997, + -923.2827500000003, + -760.0827499999996, + -655.9827499999997, + -659.3827499999998, + -24.140249999999835, + 400.85974999999917, + 204.6597500000003, + -664.5402500000002, + -764.8555000000001, + -496.5555000000004, + -331.55550000000034, + -15.355499999999383, + -775.7904999999998, + -597.7904999999998, + -347.89049999999986, + 6.609500000000073, + 1769.7222500000003, + 1895.82225, + 1975.0222499999998, + 2178.922250000001, + 3567.434, + 3498.4340000000007, + 3279.034, + 3402.3340000000003, + 3903.58775, + 4122.887749999999, + 4196.98775, + 4029.7877499999995, + 4102.423, + 3938.5230000000006, + 4098.523000000001, + 4273.823, + 3870.8305, + 3994.3304999999996, + 4379.230500000001, + 4347.1305, + 3867.3205, + 3680.9204999999997, + 4932.5205, + 5066.0205, + 4356.616, + 4302.216, + 5042.215999999999, + 5194.316000000001, + 5765.66475, + 5754.16475, + 5871.164750000001, + 5669.164750000001, + 5798.663249999999, + 6026.663250000001, + 5930.563249999999, + 6055.863250000001, + 6031.345749999999, + 6011.145749999999, + 6249.04575, + 6352.845750000001, + 7428.630749999999, + 7274.030750000001, + 7088.830749999999, + 6514.330750000001, + 6553.738249999999, + 6477.938249999999, + 5715.138249999999, + 5040.13825, + 5719.746749999999, + 5735.646750000002, + 5811.246749999999, + 5696.3467500000015, + 5877.49025, + 5641.29025, + 5271.49025, + 4891.79025, + 1456.8147499999995, + 1178.71475, + 1754.3147499999993, + 1169.0147499999998, + -286.08800000000036, + 30.512000000000317, + 24.61200000000019, + -225.78800000000032, + 963.6839999999995, + 731.884, + 787.8839999999993, + 778.6840000000002, + 663.1417500000003, + 835.7417500000004, + 963.5417500000001, + 1099.3417500000007, + 1558.2739999999997, + 1584.7739999999997, + 1675.1739999999995, + 1680.474, + 1389.8059999999996, + 1443.5060000000003, + 1372.7059999999992, + 1423.7059999999997, + 2373.5655000000006, + 2161.5655, + 2394.5655000000006, + 2372.1654999999996, + 3421.582499999999, + 3541.6825000000013, + 3587.6825000000003, + 3646.6825000000003, + 4128.7565, + 4480.5565000000015, + 4835.7565, + 4882.356500000001, + 6357.377500000001, + 6458.1775, + 6879.5775, + 7107.877500000001, + 6588.590250000002, + 6564.590250000001, + 6631.490250000002, + 6683.990250000002, + 4544.045500000001, + 4115.7455, + 3834.6455, + 3922.6455000000005, + 3429.4180000000006, + 3304.218000000001, + 3401.0180000000005, + 3517.6180000000004, + 4182.2837500000005, + 3904.683750000001, + 3778.98375, + 4011.983750000001, + 3104.376250000002, + 3072.6762500000004, + 2871.4762500000006, + 2743.776250000001, + 3504.94325, + 3506.04325, + 3624.24325, + 3402.24325, + 2507.7062500000006, + 2554.0062500000013, + 2571.20625, + 2604.806250000001, + 2774.0155, + 2818.6155, + 2735.5154999999995, + 2560.7155000000007, + 2220.9132499999996, + 2077.31325, + 2178.2132500000002, + 2299.113250000001, + 2090.1495000000004, + 2192.6495000000004, + 2196.7495000000004, + 1882.6495, + 713.4910000000006, + 557.1910000000007, + 535.8910000000005, + 195.09100000000083, + 1005.1585000000001, + 1101.9585000000002, + 1256.2585, + 1372.7585000000001, + -32.014749999999395, + -512.6147499999994, + -532.6147500000001, + -521.1147500000002, + -1671.92975, + -1609.9297500000005, + -1603.22975, + -1572.4297499999998, + -1899.679749999999, + -1835.0797500000006, + -1782.6797499999998, + -1799.5797499999996, + -2716.45025, + -2757.65025, + -2768.2502500000005, + -2579.55025, + -3500.0247500000005, + -3382.2247499999994, + -3386.1247500000004, + -3343.924749999999, + -2869.5605000000005, + -2844.6605, + -2569.6605000000004, + -2515.7605, + -1932.9772500000001, + -1924.67725, + -1863.7772500000003, + -1593.67725, + -1419.5037499999994, + -1438.0037500000003, + -1353.5037499999999, + -952.3037499999994, + -217.79475000000033, + -510.59475000000043, + -905.3947500000004, + -484.6947500000003, + 1023.1305, + 810.2305000000001, + 522.5305000000006, + 329.1305000000003, + 1224.1724999999994, + 1346.9725000000003, + 1265.4725000000003, + 1359.8725000000002, + 530.8542499999998, + 777.6102499999998, + 854.5102500000002, + 1035.4102500000004, + -1176.6879999999999, + -954.4880000000002, + -568.5879999999997, + -585.8879999999998, + -953.5567500000002, + -491.25675, + -366.3567500000002, + -342.1567499999997, + -1035.1485, + -1297.5485000000006, + -1220.0484999999994, + -816.8484999999996, + 70.4872499999998, + -90.5127499999999, + -244.61275000000012, + 130.48724999999976, + 867.5649999999999, + 694.0649999999998, + 946.3649999999998, + 1321.4650000000004, + -14.161250000000097, + -300.6312499999997, + -46.30024999999924, + 285.4587499999998, + 537.6652499999999, + 436.42524999999966, + 824.0272500000003, + 619.6582500000003, + 56.742749999999425, + -93.57524999999976, + 449.7717499999998, + 669.0927499999987, + 1054.4477500000003, + 1048.99375, + 943.5927500000004, + 1157.4717499999992, + 1036.8227500000003, + 1100.78975, + 1283.7287500000004, + 1509.3157499999995, + 930.4007499999993, + 917.3317500000007, + 968.5317500000001, + 846.63175, + 2748.8585, + 2963.1585, + 2897.458500000001, + 2697.3584999999994, + 2457.262, + 2512.862, + 2503.792, + 2303.462, + 2600.2775, + 2640.4274999999984, + 2763.6804999999995, + 2823.5275000000006, + 3310.5367499999998, + 3312.22475, + 3271.4247500000006, + 3161.52475, + 2785.7647500000003, + 2737.3647499999993, + 2623.3647499999997, + 2900.76475, + 3454.43525, + 3751.03525, + 3779.83525, + 3975.63525, + 4604.987500000001, + 4809.040500000001, + 4688.0085, + 4815.5215, + 5744.60475, + 5767.206749999999, + 6174.074750000001, + 6451.90675, + 6670.091, + 6894.874999999999, + 7248.512000000001, + 6556.918000000001, + 6801.14575, + 6844.056749999999, + 6528.82575, + 6170.54375, + 7185.785, + 7218.248999999999, + 6893.645000000001, + 6876.368000000001, + 7956.0864999999985, + 7904.1815, + 7834.744499999999, + 7582.687500000001, + 7437.135250000001, + 6936.043249999999, + 6506.78925, + 6180.7212500000005, + 5517.64825, + 4726.046250000001, + 4828.046249999999, + 4664.246250000001, + 3637.9657499999994, + 3650.0657500000007, + 3778.4657500000003, + 3809.7657499999996, + 2845.2857499999996, + 2460.3277500000004, + 2366.52075, + 2249.2857500000005, + 2315.22825, + 2103.2142500000004, + 1884.0762499999998, + 2324.29625, + 2156.4457500000003, + 2034.7667500000007, + 2322.1157499999995, + 2444.6837499999992 + ] + }, + { + "name": { + "en": "Non-Renewable", + "de": "Nicht Erneuerbar", + "fr": "Non Renouvelable", + "it": "Non Rinnovabile", + "es": "No renovable" + }, + "color": "rgb(158,152,148)", + "type": "area", + "visible": true, + "showInLegend": true, + "showInNavigator": false, + "allowCsvDownloadForItem": true, + "yAxis": 0, + "data": [ + 27283.878163093825, + 26275.531376211195, + 25832.96953602026, + 25623.564038440996, + 25571.45259028932, + 25442.460084612037, + 25404.515438715487, + 25492.08790745222, + 25697.582344469447, + 25491.882969050588, + 25656.943091054956, + 25566.39334879754, + 25701.364354319503, + 25762.876339208135, + 25897.048997699494, + 25973.460955028007, + 26077.462759239945, + 26323.26399259612, + 26798.985970443387, + 27541.35727055037, + 28903.30073082256, + 30102.041995214673, + 30774.32498212302, + 31548.747182524035, + 32665.749577422637, + 33622.34743734367, + 34230.966478042195, + 34533.31983296303, + 34588.2408692936, + 34645.75025667464, + 34723.92207590941, + 34825.78107100811, + 34800.31856597159, + 34742.40110957662, + 34716.10624799321, + 34753.59712843221, + 34628.28663682839, + 34472.033012728025, + 34425.257584786894, + 34356.29539301872, + 34292.40717124209, + 34151.94430666594, + 33999.02540077239, + 33929.234655095104, + 33876.447710788925, + 34061.38106489776, + 34065.648260160175, + 33924.66308317326, + 33721.85333063637, + 33470.110615512334, + 33209.93629953642, + 33149.00535171677, + 33346.22914965836, + 33371.750608875474, + 33202.11168819419, + 33207.23929661267, + 33090.81458504501, + 33605.30880060456, + 33860.60441706458, + 34052.441706057056, + 34121.16176399136, + 34277.84208931747, + 34304.18704115545, + 34470.76959499168, + 34818.215093553226, + 34890.367715995075, + 34944.8519255841, + 35058.58454389141, + 35311.343959996935, + 35337.53847320974, + 35227.65326831423, + 35047.19501157648, + 34774.528312689086, + 34738.18927149482, + 34715.89659791282, + 34640.75621690908, + 34515.30022710381, + 34207.02947047305, + 33946.697718191106, + 33684.79872580756, + 33212.46571341864, + 32163.990427734854, + 31191.91199189465, + 30487.610932062948, + 30176.344498671482, + 29601.19353097527, + 28564.63780621467, + 27600.987232948537, + 26657.08553854109, + 25606.593368981987, + 24640.532659462362, + 23995.668901142388, + 23503.053176306305, + 22891.611095564804, + 22569.535189983813, + 22393.48444230255, + 21156.669869413552, + 20519.353298820814, + 20380.800417234714, + 20185.357428037143, + 19943.561846888133, + 19919.18630043216, + 19852.827681016115, + 19953.116891033365, + 19764.538675364543, + 19695.142781578157, + 19618.585299740585, + 19585.51591366268, + 19497.03040076149, + 19400.701003162285, + 19427.767893493587, + 19416.140687192536, + 19401.926581367283, + 19452.620317004294, + 19546.911135356873, + 19667.373937284166, + 20098.880749897733, + 20162.13839665955, + 20454.073216224817, + 20948.764194794756, + 21588.65653238865, + 22873.686922566136, + 24412.398174604215, + 25491.82065308909, + 26626.662693526974, + 27511.452167423748, + 27740.858141181012, + 27879.16892749852, + 28062.333949651253, + 28120.034429222622, + 28177.138678070136, + 28210.242701450843, + 28199.30039733283, + 28184.86491314812, + 28122.931342119544, + 28022.985095650187, + 27615.168188562064, + 27376.373156170055, + 27185.500889801908, + 26919.21388978577, + 26820.830284092815, + 26605.808850916415, + 26494.43399589786, + 26182.963400600933, + 25630.970475558956, + 25500.13458172232, + 25654.18937495356, + 25637.347647288832, + 25688.388188128447, + 25873.13110613864, + 26285.084863437245, + 26872.793429628622, + 27564.34409163378, + 27970.531979701496, + 28451.493354357015, + 29434.718994829323, + 30558.739796470658, + 30970.226769410074, + 31319.73142280613, + 31537.324200744635, + 32164.666271200163, + 32509.333141284114, + 32616.138548369552, + 32680.949291862296, + 32887.69430772514, + 33012.8796045633, + 32915.019337959355, + 32684.381426460688, + 32166.77693526183, + 31945.279642024034, + 31788.018461945463, + 31728.021243187675, + 31518.257773119396, + 31505.170385711514, + 31464.473841290604, + 31036.944021477346, + 29817.157925900152, + 28812.320905575412, + 27948.64914295686, + 27063.487951311337, + 26329.63613663548, + 25366.460317167617, + 24354.10872956342, + 23569.969605792387, + 22886.365015965275, + 22521.35837232654, + 21890.031981620326, + 20912.636123173437, + 20055.12750000052, + 19319.967301047924, + 19175.601120641524, + 18621.52948636748, + 18227.13660738419, + 17823.913921118034, + 17599.11342280383, + 17007.10482502944, + 16779.1028715344, + 16824.958000587805, + 16914.46439410698, + 16915.346527707814, + 16784.50559114913, + 16614.46752313625, + 16513.281915115294, + 16559.772021782857, + 17007.01705067255, + 17285.489185796185, + 17498.438241948865, + 17441.590362902745, + 17675.740178407854, + 17786.873330415307, + 17773.685978353413, + 18007.136111128402, + 18937.564403225508, + 19456.026698213045, + 19595.825030023792, + 20360.56532841775, + 21577.78519044586, + 22502.960934358376, + 22950.5818069869, + 23156.785310976484, + 23721.767489802947, + 23736.7164733681, + 23858.37867405096, + 24042.447224773627, + 24248.355788262503, + 24095.660502327184, + 23694.579781398552, + 23257.098710756356, + 22294.841424595677, + 22237.006035942897, + 22041.677664078503, + 21421.986817244855, + 20847.70708566991, + 20562.30613846871, + 20453.87381978609, + 20504.129815926513, + 20477.590391540107, + 20487.05959540008, + 20526.6770073527, + 20716.31972507606, + 20473.9130136111, + 20696.87334707623, + 20982.990581990965, + 21146.472284246014, + 21599.94327054398, + 21908.570252513528, + 22229.92073188937, + 22513.703210963504, + 22945.931985165073, + 23439.087516398824, + 23512.25777300079, + 23636.852203488845, + 23229.425982935198, + 23563.226177409037, + 23931.085167208206, + 24237.119743949836, + 24707.384436019005, + 24854.634091373842, + 24991.38248107823, + 24932.21743845422, + 24531.556270421723, + 24529.57861112216, + 24558.944922605868, + 24125.377024596786, + 23055.097093411387, + 22403.5121237316, + 22109.970633719015, + 21864.11413221038, + 21706.431667196735, + 21471.050652504025, + 21404.61610872341, + 21309.372006068064, + 20246.532921725404, + 19663.86313524379, + 18823.357648435147, + 18278.542033728987, + 17801.837492686172, + 17461.153267126225, + 16738.15772687128, + 16146.908368440525, + 15732.044707934825, + 15329.082381371156, + 15166.613116577244, + 14986.769655105398, + 14092.342757549157, + 13635.771461669068, + 13391.187578779822, + 12987.322252775506, + 12491.695069383715, + 12194.486821174522, + 11731.702482780169, + 11581.106141533364, + 11225.7110538851, + 11152.399754098265, + 11092.374791770668, + 11111.625717093308, + 11121.153947782266, + 11079.746127467106, + 11415.767196447632, + 11516.506164594317, + 11423.368968995288, + 11436.168206260969, + 11416.518560947508, + 11443.496520593186, + 11940.155636546864, + 11999.401856400475, + 12170.133425299022, + 12262.285330956367, + 12925.717207666428, + 13283.515618630277, + 13689.993850827872, + 14098.297684733825, + 15121.52261066606, + 15728.969247339, + 16216.859179286632, + 16812.022582815778, + 17668.155052624847, + 17935.809252764746, + 18068.506897275776, + 17848.809352389442, + 17611.34618710367, + 17346.565956666967, + 17212.888545998347, + 16983.657990168897, + 16101.778795806622, + 15537.021419737106, + 15377.911232252949, + 15324.089001034154, + 14559.699222552761, + 14039.667898633754, + 13943.184054681755, + 13938.483278231946, + 13976.304454950077, + 14131.20265517013, + 14130.728899494818, + 14067.203775013772, + 13856.800475851884, + 13929.688345958464, + 14081.815748794777, + 14617.137081017301, + 15543.83833305387, + 16071.450651091682, + 16805.126934480133, + 17453.432330151252, + 18661.20247984297, + 19761.805013717116, + 20702.055254962732, + 21683.23153471729, + 23274.340880607353, + 24234.619688092374, + 24761.24511727111, + 25521.8334869751, + 26255.778083007823, + 26612.26895191555, + 26767.09272243115, + 26978.723144719614, + 27096.927868159994, + 27198.554026879083, + 27592.011829401417, + 27885.214515588203, + 28182.30229806797, + 28069.95307232556, + 28133.798665046103, + 28062.244102813613, + 27837.271380507136, + 27802.213044528136, + 27703.194690853714, + 27573.20377719195, + 27302.890929729238, + 27138.804855490085, + 26890.581084504007, + 26530.445632278796, + 26178.456491701403, + 26063.118248955932, + 26043.618503809663, + 25959.105427759372, + 25644.20060435596, + 25606.686967329162, + 25612.873949553657, + 25723.79176672373, + 25413.77062369022, + 25288.574059714156, + 25061.27949429878, + 24854.661416740164, + 24966.4969717357, + 24949.935115394375, + 25017.14907669182, + 24983.385213237452, + 24607.36311013235, + 24623.845786168476, + 24631.90339911564, + 24558.197498455804, + 24099.299324652162, + 23931.50531478597, + 23798.36914556387, + 23461.3337356896, + 22895.45636369022, + 22849.7048939636, + 22948.303746471374, + 23089.98938625591, + 23617.383589364024, + 23813.85497839828, + 23949.939254894845, + 24175.95885399541, + 24756.304069877402, + 25539.371962369434, + 26089.6929136649, + 26972.9630452836, + 28124.27679086578, + 28847.842330097847, + 29238.234785073513, + 29529.64756702843, + 30030.567678260282, + 30125.046475278217, + 30178.72092569688, + 30226.862535604858, + 30232.106338361875, + 30324.76939720631, + 30365.135179739, + 30399.072781560117, + 30217.80925348931, + 30094.362226395817, + 29984.03227896412, + 29883.465037622995, + 29633.37038540973, + 29402.373446849757, + 29441.324781673982, + 29293.20561984828, + 28998.33113810693, + 29037.657937611595, + 29231.09059249524, + 29227.87818922721, + 29336.626036742462, + 29339.97595037281, + 29391.623365263342, + 29491.452536685327, + 29449.522866311137, + 29685.74115734675, + 29794.478026571145, + 30119.433222761734, + 30502.367754275354, + 30559.02616428841, + 30671.621000955976, + 30875.534953958686, + 30944.995990671512, + 31092.863090863553, + 31287.21480767869, + 31234.670425104934, + 31464.932108244928, + 31605.717114381943, + 31675.500922858744, + 31237.855906918838, + 31324.00224812149, + 31283.587011044838, + 31445.806893466684, + 31482.202470874166, + 31195.697217813537, + 30805.023214177807, + 30409.45018415984, + 30527.5992915041, + 30176.62189638505, + 29968.50273899672, + 29560.80773033357, + 29237.99083255419, + 28508.699782736665, + 28118.359409796063, + 27522.688164856474, + 27033.658234714596, + 26535.927325434976, + 26192.83770421887, + 25465.774021181256, + 24255.5343318775, + 23351.75633327536, + 22369.99867916692, + 21667.871593347045, + 21046.031543983467, + 20309.965708492917, + 20040.6839086666, + 19686.974214981812, + 18925.74285007902, + 18171.59080462458, + 18461.317507724692, + 18299.69858522689, + 18240.975571179464, + 18073.685335005335, + 18026.276642137927, + 18040.527253175762, + 18163.221409474827, + 17772.292679987022, + 17763.642707328196, + 17698.250212989024, + 17607.92958902705, + 17557.86420712658, + 17437.7205955332, + 17556.556485511355, + 17552.349011703915, + 17626.41020592733, + 17642.199863122038, + 17561.06428790677, + 17548.934227391892, + 17630.966299525076, + 17833.59003673973, + 17923.41831852365, + 17630.506260639584, + 17735.680596184968, + 17762.012323367802, + 17871.299511232326, + 18586.93454726038, + 19548.713579627074, + 19704.418126024335, + 19726.581145087708, + 19736.99113312781, + 20031.36762480246, + 20226.943152900083, + 20288.72482113112, + 20125.06863719866, + 20317.903854138818, + 19854.838727312523, + 19762.0288504314, + 19623.712514366147, + 19236.580318278273, + 19170.389914392978, + 19074.69321667302, + 19070.90709964622, + 18857.901922010617, + 18804.41744262211, + 18807.116624200138, + 18871.560641905937, + 18597.07465318225, + 18463.692321539413, + 18561.714304854602, + 18689.89046714383, + 18372.71367331645, + 18463.54285551982, + 18920.52857614667, + 19237.05722477642, + 19974.65042541284, + 20436.827279549216, + 20961.437039722892, + 21440.71239873721, + 22220.826158027914, + 22685.79409959139, + 22959.214877004644, + 23439.58587316461, + 24027.499793206858, + 24197.95965025589, + 24290.90922225947, + 24496.52903036896, + 24614.359667371948, + 24619.92468693683, + 24648.69371954889, + 24646.90628822771, + 24475.083429435024, + 24111.051609122558, + 24002.0657160673, + 23961.6389519811, + 24068.270010538774, + 23873.613535020613, + 23878.585229299642, + 23745.821528202316, + 22917.884211748707, + 22569.100703036973, + 22366.486028223117, + 22242.444949938035, + 21902.119802829184, + 21443.63550135117, + 21312.58202114528, + 21314.987714327854, + 20845.070996760063, + 20736.470265538897, + 20616.004699456073, + 20501.471410799495, + 19904.085147154587, + 20013.78557502549, + 20151.24538662268, + 20120.051612065996, + 20421.877883899935, + 20485.154059749002, + 20471.767386042375, + 20409.232601460844, + 20281.248188257618, + 20282.557355782345, + 20291.19529503356, + 20286.295408244463, + 20186.572491522853, + 19954.14335830349, + 19850.5980128601, + 19757.51873310698, + 19443.543043169095, + 19394.895778985167, + 19381.937107565474, + 19553.129602106732, + 19764.760819525367, + 19895.75713990605, + 19934.553866447484, + 19976.35956146185, + 20350.782833414072, + 20400.572925662702, + 20421.54942038342, + 20391.948715219907, + 20228.621240502664, + 20143.390247225707, + 20200.93424063711, + 20383.946479957336, + 20877.5251991629, + 21305.51586596158, + 21475.96233442382, + 21721.844209451632, + 21962.418709723832, + 21975.26906684725, + 21957.55312318279, + 21924.795274683755, + 22270.40665309884, + 22369.285866659793, + 22589.539757935323, + 22611.27643752584, + 22951.372067166994, + 23066.882214569177, + 23069.94047195941, + 23178.358465488476, + 23401.180296844952, + 23587.545567164707, + 23649.094458078704, + 23683.087580344578, + 23681.147878020067, + 23734.578919853044, + 23767.654396136844, + 23854.044087754755, + 23901.2654345289, + 23906.28728146368, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + { + "name": { + "en": "Renewable", + "de": "Erneuerbar", + "fr": "Renouvelable", + "it": "Rinnovabile", + "es": "Renovable" + }, + "color": "rgb(179, 222, 105)", + "type": "area", + "visible": true, + "showInLegend": true, + "showInNavigator": false, + "allowCsvDownloadForItem": true, + "yAxis": 0, + "data": [ + 9983.726212386859, + 9940.318308283498, + 9882.917752974394, + 9872.172113580258, + 9800.149509398478, + 9851.559333228128, + 9912.950515366854, + 9879.56740009103, + 9915.10453561268, + 9967.779184967765, + 10082.134364483805, + 10272.04055674844, + 10321.243405964642, + 10275.886616471988, + 10308.280699701316, + 10525.530443411797, + 10473.699222686799, + 10315.478265697511, + 10288.921514431793, + 10289.512779760764, + 10287.004982702118, + 10294.297595999513, + 10266.926921314654, + 10297.404561764673, + 10462.083747268856, + 10630.033826549903, + 10660.28102570645, + 10701.832897062279, + 10849.198037989223, + 10986.749155483025, + 11173.39846341044, + 11647.691149080045, + 12362.371594471872, + 13261.963016831352, + 14261.72795934499, + 15313.72161109223, + 16290.470203761939, + 17184.418778006642, + 18214.261201644502, + 19232.848223476067, + 19982.52678889018, + 20664.31484240919, + 21436.37791437453, + 22144.839841406534, + 22808.611715575018, + 23271.34868827481, + 23873.77399390432, + 24307.17016882181, + 24578.735754372392, + 24875.830461688358, + 25159.542010329307, + 25285.445755608936, + 25630.707285558485, + 25578.860018103907, + 25169.900492931512, + 24571.070292486387, + 23860.01241864116, + 22935.50485786847, + 21963.6748901046, + 20767.62555059229, + 19359.54639499162, + 17923.421912644128, + 16805.959683164434, + 16072.388887376703, + 15425.147226574054, + 15394.334017795181, + 15706.579868430115, + 16176.404675750056, + 16697.50667866552, + 17309.195382938444, + 18119.326290548437, + 18757.136077476585, + 19503.67548846561, + 20194.755157163345, + 20784.897806867986, + 21341.632337895266, + 21635.319042601295, + 21678.240398731483, + 21812.6710351906, + 22004.781687569623, + 22164.51648304054, + 22530.346416909662, + 22773.43388279144, + 22889.392358169036, + 23124.090269936885, + 23238.69201215647, + 23388.087877600417, + 23651.532106402236, + 23838.35720511908, + 24118.96172551748, + 24510.791774732774, + 24532.421452629413, + 24624.57093346661, + 24747.988763025896, + 24894.403950770306, + 25169.297071829737, + 25302.358384136787, + 25708.77008109814, + 25763.193714695815, + 25945.035427209536, + 26099.59130030463, + 26042.656781165795, + 26039.368617227497, + 25953.725405333775, + 25871.584438119724, + 25990.818056185803, + 26422.86493614316, + 26700.653436138316, + 26827.80846968439, + 26987.722449662968, + 27243.57740102891, + 27525.649254157237, + 27734.86211770161, + 27847.784800601963, + 27728.35171674661, + 27644.28814416414, + 27726.61381177244, + 27505.18834951963, + 27534.21410986976, + 27526.332159652637, + 28338.104282740373, + 28489.962309196788, + 28462.436499784635, + 28254.25373933794, + 28060.008167599983, + 27828.394415837673, + 27925.56046487791, + 28253.39264021839, + 28725.036598573424, + 29243.038119398883, + 29916.818001398206, + 30792.88002565228, + 31825.758695931538, + 33030.425357189764, + 34289.99580433643, + 35265.47001517588, + 35988.70963375381, + 36742.411445987804, + 37417.02181112117, + 38133.14760092051, + 38841.52151102161, + 39417.521129666704, + 39694.45257595857, + 39761.885131019415, + 39686.14363808668, + 39530.976072377016, + 38813.01212028996, + 38165.12754103582, + 37284.23844907273, + 36410.08169404919, + 35131.61283532508, + 33820.15097669835, + 32563.07429252848, + 31061.05564084097, + 29344.215223051957, + 27517.5230369684, + 25595.419668612318, + 23786.75666568975, + 22207.572035295125, + 20949.78548829359, + 20489.512254769445, + 20297.854075560128, + 20103.650073097735, + 20068.231010932595, + 20340.728010792693, + 20914.73613456125, + 21354.404943296486, + 21880.912368610134, + 22283.74924225459, + 22580.894696626856, + 23100.520906406757, + 23371.16949563643, + 23712.869008496495, + 24357.1710015965, + 25184.635651195997, + 25499.525350476222, + 25709.9453239031, + 25836.687763357426, + 26125.008186682782, + 26280.824266840493, + 26313.645719932676, + 26427.202143236304, + 26687.640136125843, + 26747.302117029627, + 26674.485295212573, + 26902.837892119656, + 27284.72861433132, + 27438.994028966303, + 27424.33483732342, + 27546.173814610876, + 27948.822853337027, + 28184.151596046246, + 27978.198781475763, + 28065.732634369764, + 28171.641177232428, + 28114.846631317334, + 27997.25506535931, + 27955.645355522214, + 27994.28966444931, + 28165.131196985538, + 27915.941131807, + 27645.050310764553, + 27264.507712099, + 27500.966158791445, + 27052.614787788105, + 27145.20516397268, + 27449.252089887217, + 27604.97544219966, + 28292.912339721857, + 28639.565168874433, + 28939.188018221634, + 29256.4595002644, + 29247.204968069946, + 29226.043533485506, + 29211.598179181638, + 29610.326911089076, + 30976.495444132943, + 31501.90785756524, + 31875.757562941064, + 32290.78125381968, + 32711.34574593009, + 33234.7710238699, + 33824.04795714907, + 34661.61282393231, + 35629.586671554214, + 36662.48928787178, + 38222.322428043786, + 39979.1087625313, + 41973.43673005665, + 43875.21029379789, + 45175.92584623233, + 46129.70911818312, + 47021.923000534094, + 47772.072732840235, + 48541.0449928793, + 49146.99478668504, + 49864.706137181856, + 50279.521866773364, + 50563.47268092513, + 50320.76882156993, + 50341.41018070739, + 50162.71645046049, + 49264.12155089494, + 48235.94631193871, + 47896.1425333294, + 46708.31920414071, + 44851.564555620695, + 42809.17637227141, + 40881.339050077, + 39367.149067959814, + 37797.89562997393, + 36410.599123559354, + 35492.56657629271, + 34371.13722742117, + 33639.16933428408, + 33359.80141389919, + 33145.793323494014, + 33715.23333861989, + 34674.79636786936, + 35904.58687975039, + 36528.138737559595, + 37522.36338383252, + 38265.35799733549, + 39069.572772661115, + 41086.910359731904, + 42253.81817472323, + 42329.388972956556, + 42135.78791942024, + 42319.79556024816, + 42305.547660101714, + 42422.6342813216, + 42581.203231641324, + 42761.35318181798, + 42847.59796780512, + 42691.310403460724, + 42604.614807313104, + 42441.024900027805, + 42582.781812110996, + 42372.75917411797, + 42094.343811137725, + 41207.8323997376, + 41270.18883057499, + 41281.83403957922, + 41124.430008309035, + 40724.963398640415, + 40998.469794801975, + 41200.97119660728, + 41561.445360343954, + 41531.12444261648, + 41586.6452243919, + 41753.98989204681, + 41539.03035076574, + 41051.29412538206, + 40897.54391063172, + 40935.54171015456, + 40912.876993505386, + 40937.4377557144, + 40821.68638778114, + 40642.09755343708, + 40263.00855390774, + 40183.16382812183, + 40057.6109760397, + 40135.292758245836, + 40350.94773566024, + 40283.88418017747, + 40510.77042938353, + 40857.48125218672, + 41161.45283187879, + 41231.763796241044, + 41223.24111176312, + 41334.090377485525, + 41673.706218213134, + 42891.181579656026, + 43199.26984055323, + 43639.90711286434, + 43706.72957387973, + 43606.461730331146, + 44003.85428307319, + 44497.17087691235, + 45685.30450808172, + 46921.83346728974, + 47816.40764263962, + 49121.805217788096, + 50580.7862468298, + 52115.17668555897, + 54461.365751736244, + 56473.34005168028, + 58120.72942220772, + 60063.266461558, + 61210.91838786191, + 62126.10047163299, + 62943.500253294456, + 63227.55729211576, + 63954.68088633327, + 63586.21882538029, + 63080.49133622633, + 62060.7302043901, + 61364.697797056, + 60145.212980074226, + 58638.266553386624, + 57230.52350761629, + 55401.81918605935, + 53624.39518336447, + 51246.70169763486, + 49641.70632168779, + 47567.70327495447, + 45050.36517607731, + 43256.97400208087, + 41339.49749549024, + 40117.34221863246, + 38803.973692507745, + 37829.68299123276, + 36550.26906436851, + 36306.43623829232, + 36189.5450599634, + 36041.053156002126, + 35989.8279959551, + 35719.337761571005, + 35388.230348918034, + 35155.231631904906, + 35060.37433578403, + 34375.31990978852, + 33521.63870386516, + 32721.9797798559, + 31695.431772288477, + 30334.11175605857, + 29472.32088147402, + 28886.215367172153, + 28491.367612933383, + 27805.373245786985, + 27223.468413745395, + 26390.10004934018, + 25339.428261623307, + 24592.759387059785, + 24016.953631496308, + 23389.0137473631, + 21981.15963066704, + 21009.648103914697, + 20331.020167722956, + 19765.631838818997, + 19254.442612100698, + 19230.48570145057, + 19189.246588246304, + 18661.968515485045, + 17707.04250806646, + 17308.578080804327, + 16899.961888500697, + 16491.38144298533, + 16216.255065854728, + 15874.022203250997, + 15674.701796090947, + 15461.422916475713, + 15470.715899111936, + 15316.153790094268, + 15142.969193909983, + 15141.883719707223, + 15502.197843488684, + 15652.042562029905, + 15679.34028814039, + 15831.864666351208, + 15834.7285209366, + 15824.752176006992, + 15761.222769865115, + 15649.28076139231, + 15760.957637158283, + 16155.72863324173, + 16359.800040084345, + 16381.27439487767, + 16768.240139018588, + 17023.376305718317, + 17320.683567906257, + 18105.00563446539, + 18845.3914988843, + 19645.349514376794, + 20465.172034341707, + 21991.635334367114, + 23529.995015560726, + 24896.352294605014, + 26436.224525256956, + 27847.47883079844, + 28930.496391606688, + 30098.839491100734, + 31277.810074111905, + 32183.83923942, + 32583.453130163063, + 32861.16615849424, + 33179.42768596232, + 33461.19355494561, + 33702.183924973884, + 33916.59801682518, + 33753.168684425735, + 33346.83133303352, + 32899.486515290424, + 32677.710658627366, + 32463.02703487598, + 32047.882332975383, + 31775.88972292789, + 31431.797711778017, + 30670.55429463921, + 29886.402634545033, + 29140.80475312032, + 28614.84581803442, + 28271.602599010814, + 27864.950270947695, + 27533.508513315035, + 26808.71751459693, + 26027.607937404497, + 25239.784798978617, + 24343.89114261488, + 23767.286032762328, + 23578.193761784278, + 23603.07160997059, + 23554.198175169695, + 23576.95269920092, + 23259.392586523703, + 23254.912678740955, + 23630.579623805548, + 23790.342793169875, + 23986.801838317748, + 24074.737629075506, + 24053.58989101329, + 24098.849170331767, + 23996.377735131675, + 23867.6884816846, + 23842.40391865795, + 23873.571895810062, + 24074.226463364714, + 24185.8923496541, + 24816.53234754716, + 25391.53149788356, + 26021.905718335343, + 26517.913187586433, + 26641.928224054936, + 27051.931849325345, + 27260.556281730875, + 27341.419832434487, + 27336.045466642754, + 27519.367063886155, + 27628.21571217518, + 27536.492740917005, + 26869.632703074647, + 26766.484884976722, + 26462.803457768154, + 26292.770987664066, + 26237.66499113905, + 26404.28985736177, + 26485.921588493766, + 26686.141241455683, + 26596.013018464073, + 27278.623464731834, + 27577.086872360116, + 27729.990989943886, + 27844.08897035663, + 28039.010685760484, + 28310.571204987376, + 28593.930122896483, + 29192.429331915628, + 29780.664780952848, + 30437.879499823066, + 30824.215315378326, + 30946.91913048601, + 31051.707635786122, + 31050.810319642693, + 31036.885457537774, + 31708.04857403225, + 31843.971106906272, + 32054.754246982167, + 32167.902523456956, + 32749.3975571906, + 33193.04621592197, + 33134.947807491, + 33166.8687220439, + 34418.84499687729, + 34980.73782081347, + 35733.84621581972, + 36641.86281477327, + 37041.14529147572, + 38111.44451303927, + 39015.62794313366, + 39691.69175538912, + 40499.20933550676, + 41265.81258312425, + 41937.82108016037, + 42693.97245710583, + 43343.4619417064, + 43678.783789559755, + 43787.37616767543, + 43481.39178241945, + 43824.36365225115, + 43481.66765982905, + 42958.38270532146, + 42156.58663660668, + 41766.610697275544, + 40755.79584691157, + 39260.04818591976, + 37735.632178028754, + 36430.76546669747, + 35153.20644426192, + 34090.50814264542, + 33335.401847717534, + 32100.38560084591, + 30627.86093557252, + 29611.888783506296, + 28647.867308303074, + 27736.007161753914, + 27345.22094779334, + 27639.65308330601, + 27814.653341101126, + 28128.007836002373, + 28376.07205224462, + 28576.821500058555, + 28901.742899229183, + 29193.967858032764, + 29815.13079772693, + 30165.862851837614, + 30094.37404182922, + 29905.378641911822, + 29840.953158635326, + 29634.300992115717, + 29452.80238976701, + 29255.381902237154, + 28909.166716765656, + 28317.141193025764, + 27745.21621549532, + 26724.737540910803, + 26233.758814877787, + 25545.06247893952, + 24892.577611463294, + 24429.907786301672, + 24114.06722071458, + 23402.062558928006, + 22745.132385128778, + 22024.22557979702, + 21389.077665834055, + 20582.496100559645, + 20153.915546152606, + 19881.34297253353, + 19712.133287136243, + 19402.596611682377, + 19201.29526884199, + 18808.432258453857, + 18679.34993590261, + 18541.8846566965, + 18368.98831043026, + 18170.732638553545, + 18100.07688107251, + 17897.72346279613, + 17600.584731592786, + 17306.244699856412, + 17096.31377706694, + 16853.91833946534, + 16549.334393815036, + 16397.62388268089, + 16576.89121949681, + 16714.955597693937, + 16841.568950205194, + 16934.85708360034, + 17156.65505437956, + 17067.26571761305, + 17065.700347179016, + 17474.847262171494, + 17662.344738845222, + 17919.508462149624, + 18063.894409896537, + 18145.412097724908, + 17948.94519561876, + 17949.85761232911, + 18194.78049909642, + 18642.094158628886, + 19382.21170281024, + 20282.03118409908, + 21162.66791571294, + 22054.494464887117, + 22912.96397373294, + 23430.657962370635, + 23647.53222135951, + 23799.440624826264, + 23863.25295085361, + 23854.993539952422, + 23993.02135394574, + 24114.922065868574, + 24156.006604021815, + 24111.41399900247, + 24001.657377273834, + 23503.833284544184, + 23090.443959705553, + 22724.159640772355, + 22236.787727915398, + 21702.370454701013, + 20835.219705164407, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + { + "name": { + "en": "Load", + "de": "Last", + "fr": "Charge", + "it": "Carico", + "es": "Carga" + }, + "id": "load", + "color": "rgb(50,50,50)", + "type": "line", + "visible": false, + "showInLegend": true, + "showInNavigator": true, + "allowCsvDownloadForItem": true, + "navigatorOptions": { + "visible": true, + "type": "line" + }, + "yAxis": 0, + "data": [ + 45375.44, + 44796.5, + 44206.35, + 43704.63, + 43019.68, + 42824.6, + 42501.42, + 42305.49, + 42247.23, + 42017.36, + 42011.82, + 42187.91, + 42398.12, + 42785.75, + 42907.59, + 43246.16, + 43967.77, + 44474.59, + 44968.19, + 45600.64, + 47467.44, + 49166.18, + 50697.4, + 52067.47, + 55326.83, + 58082.92, + 59601.48, + 61408.93, + 63073.5, + 63974.83, + 64543.2, + 65014.66, + 66174.67, + 67221.29, + 67438.83, + 67809.05, + 67833.05, + 68069.15, + 68774.0, + 68925.72, + 68878.84, + 68363.93847, + 68880.10337, + 68955.51629, + 68924.84771, + 68972.91977, + 69074.70964, + 69184.82753, + 68972.55996, + 68774.72827, + 68912.59904, + 68842.35273, + 68696.11296, + 68070.61607, + 67547.68953, + 67349.10413, + 67004.47071, + 66496.41858, + 65829.17524, + 65652.0785, + 65811.84276, + 65434.34133, + 64972.95283, + 65005.59875, + 65120.14699, + 65438.91104, + 65890.98761, + 66661.94152, + 67843.81724, + 68234.45855, + 68881.86162, + 69209.68584, + 68564.46353, + 68494.83176, + 68309.35128, + 67935.47551, + 67674.22036, + 66987.1003, + 66374.10123, + 65874.87793, + 64785.56195, + 63620.51185, + 62808.38847, + 62005.66095, + 61006.61411, + 59994.4684, + 58757.5529, + 57711.1914, + 57001.06433, + 56226.448, + 55253.03505, + 54314.86936, + 53325.63648, + 52276.89445, + 51443.29201, + 50517.01291, + 49742.93025, + 49150.51118, + 48599.71562, + 47883.56263, + 47242.61598, + 46614.70879, + 46462.15221, + 46175.49179, + 45663.52619, + 45741.67233, + 45718.05576, + 46123.92363, + 46694.52118, + 46644.64371, + 46505.93379, + 46623.7471, + 47851.71562, + 48084.11087, + 48499.00455, + 48707.46066, + 49908.62555, + 50540.13155, + 51745.70145, + 52946.14273, + 56360.21405, + 58843.68624, + 60634.05142, + 62029.95935, + 63666.19784, + 64562.1946, + 64895.98577, + 65755.04591, + 66565.17359, + 67347.65553, + 68032.8122, + 68050.9071, + 67322.63304, + 67662.57102, + 68026.13297, + 68814.71346, + 68245.12372, + 68499.3651, + 68534.86756, + 68384.9695, + 68426.73686, + 68444.74625, + 68876.78732, + 68339.57022, + 67629.46477, + 67313.14166, + 66814.7916, + 67083.24657, + 67148.27247, + 66764.57955, + 65814.95783, + 65517.27855, + 65644.90186, + 65314.88642, + 64977.56553, + 64783.85213, + 64659.51332, + 64313.34145, + 64118.26387, + 64313.51745, + 64113.38956, + 64545.22259, + 65188.84081, + 65925.83023, + 67001.45509, + 67543.47107, + 67513.57502, + 67629.72723, + 67745.80468, + 67750.11821, + 67605.99836, + 67152.44564, + 66876.12677, + 66236.62815, + 65839.31521, + 65346.27308, + 64101.63736, + 63063.19455, + 62243.92232, + 61574.47734, + 59881.53788, + 58989.59372, + 58381.27821, + 57291.64816, + 56156.23163, + 55276.69844, + 54433.20152, + 53620.73814, + 52408.5188, + 51343.87142, + 50820.76721, + 50292.78422, + 49746.55439, + 49158.48994, + 48326.72395, + 48089.73074, + 47946.32997, + 47298.79445, + 47284.67846, + 47100.51256, + 46578.05268, + 46414.21487, + 46078.90844, + 46521.26564, + 46681.30991, + 46881.37615, + 47325.47015, + 47481.08983, + 48283.71483, + 48348.76566, + 48874.22191, + 49778.13999, + 51145.91492, + 52170.23816, + 53184.89268, + 54850.37984, + 57513.52953, + 59860.25054, + 61541.55207, + 62938.95429, + 64846.79666, + 65629.45641, + 66666.54641, + 67093.02719, + 67625.71214, + 68248.81553, + 69194.83445, + 69618.33824, + 68856.32075, + 68806.91622, + 69338.77511, + 69960.05536, + 68756.33083, + 69298.53686, + 69449.30861, + 69617.52788, + 69230.56578, + 69300.19358, + 69301.03117, + 68729.46326, + 68671.66845, + 68092.40328, + 68321.54661, + 67520.83543, + 67350.64503, + 66598.69484, + 65875.45258, + 65105.74761, + 64205.45825, + 64454.91083, + 63972.00608, + 63562.57684, + 63256.79188, + 63094.0686, + 62873.53711, + 62658.45905, + 63749.48612, + 63724.33725, + 64498.71954, + 65370.14481, + 66657.867, + 67337.1748, + 67761.43412, + 67763.97495, + 68109.93942, + 67690.97708, + 67518.37291, + 67241.13032, + 66906.75987, + 66201.22782, + 65827.29459, + 65286.94567, + 63866.97717, + 63522.33041, + 62566.26461, + 61687.54027, + 60752.73668, + 59784.48606, + 58814.12886, + 57860.09244, + 57176.77771, + 56310.47095, + 55345.74791, + 54513.17134, + 53395.10335, + 52254.24061, + 51590.88771, + 50743.33166, + 49436.72857, + 49052.22927, + 48600.11917, + 47849.84258, + 47725.44529, + 47682.66162, + 47358.89887, + 47084.15448, + 46576.00501, + 46296.26809, + 46355.72017, + 46410.56567, + 46606.41881, + 46762.14208, + 46746.45376, + 47049.37554, + 47444.78196, + 47658.25723, + 48024.6699, + 48751.13422, + 50448.79943, + 51094.6935, + 52345.43918, + 53726.5321, + 57060.62291, + 58747.54143, + 60596.77437, + 62136.34418, + 63640.93717, + 64432.70014, + 65460.60923, + 66004.57801, + 66919.10734, + 67849.22028, + 69051.48698, + 68822.47842, + 69069.55166, + 68956.79094, + 68934.16486, + 70169.06158, + 70294.21783, + 70867.42641, + 70858.65996, + 70789.55181, + 70162.4907, + 70446.58814, + 70293.8455, + 70475.89129, + 69868.62513, + 69460.40757, + 69262.24702, + 69388.62535, + 69261.90715, + 68249.06015, + 67374.23046, + 66659.09861, + 67205.36015, + 66922.02412, + 65813.49433, + 66500.02563, + 66193.69377, + 65693.93782, + 65021.39073, + 64696.94031, + 65695.41236, + 66225.83182, + 66751.86851, + 67115.22971, + 67760.60351, + 68370.16591, + 69000.77883, + 68925.63415, + 68484.72886, + 68455.98268, + 67957.21372, + 67298.41333, + 66771.71709, + 65534.54497, + 64699.7803, + 64268.04271, + 63228.69341, + 62547.99738, + 61339.67028, + 60397.53658, + 59421.8715, + 58964.81981, + 57998.67175, + 56949.32572, + 56153.55949, + 55382.21134, + 54396.65326, + 53190.48664, + 51986.5209, + 51085.25408, + 50647.65564, + 49749.72517, + 48560.26007, + 47804.85755, + 47159.57675, + 46356.80057, + 45939.64009, + 45784.23412, + 45219.5854, + 44763.43668, + 44738.68116, + 45052.95526, + 45224.68029, + 45237.91491, + 45339.64698, + 45335.89307, + 45552.40858, + 45436.77387, + 46082.93705, + 46208.29824, + 47033.20826, + 47658.01431, + 48768.4575, + 49654.4064, + 51083.8152, + 52962.57165, + 55620.39585, + 57735.66143, + 59380.10972, + 60958.54263, + 62464.29269, + 63509.05635, + 63901.683, + 64499.26868, + 65445.81028, + 66529.84869, + 67086.46496, + 67598.1167, + 67423.99213, + 67442.304, + 67507.37727, + 68156.87073, + 68193.45893, + 68369.88316, + 68342.64308, + 68362.75494, + 68402.57562, + 68205.93973, + 68159.34331, + 68417.89647, + 67925.81802, + 67364.39426, + 67373.73409, + 66758.74568, + 66116.48827, + 65917.25013, + 65560.54888, + 64795.83686, + 64332.30602, + 64249.28047, + 63944.70556, + 63967.86831, + 64356.16517, + 64180.37709, + 64096.9704, + 63855.8049, + 64343.5961, + 64868.74574, + 65187.18341, + 65703.65634, + 66453.68278, + 66807.04179, + 67101.35563, + 67124.88405, + 66784.32934, + 66162.98447, + 65879.13583, + 66047.007, + 65038.03872, + 64326.90229, + 63990.4354, + 63140.85737, + 61338.34686, + 60317.10022, + 59646.2477, + 58616.2277, + 57882.30917, + 56923.32398, + 56425.80401, + 55684.69154, + 54322.46951, + 53655.31633, + 52952.56289, + 51711.41747, + 50470.46154, + 49793.52303, + 49324.13763, + 48628.01045, + 47356.33627, + 46954.70751, + 46446.80833, + 46118.17815, + 45411.71154, + 44924.98187, + 44538.26054, + 44306.82225, + 43582.89809, + 43429.50087, + 43327.59126, + 43117.74063, + 42382.11056, + 42542.00404, + 42835.89668, + 42850.32356, + 42803.93593, + 43076.72251, + 43542.95165, + 43530.94621, + 43649.78382, + 43363.9666, + 43638.43767, + 44068.59575, + 45795.01229, + 46097.16842, + 46794.33877, + 47384.17351, + 48096.78684, + 48650.29063, + 49051.6161, + 49469.44156, + 50971.87539, + 51550.48114, + 52394.31832, + 52999.04129, + 54188.722, + 54700.91326, + 55130.21116, + 55493.39318, + 56252.82354, + 56710.43764, + 57250.69542, + 57695.99476, + 58132.94737, + 58351.7271, + 58375.50637, + 58040.06041, + 57751.1378, + 57634.96693, + 56711.75148, + 56361.21335, + 56562.5904, + 55936.54291, + 55936.80191, + 55443.91194, + 55628.5753, + 55131.9637, + 54863.308, + 54977.35632, + 54963.35666, + 54474.42298, + 54605.26556, + 54752.86387, + 54901.71845, + 55356.70414, + 55686.0096, + 55945.81499, + 57271.86401, + 57604.12912, + 57740.00156, + 57678.39687, + 57878.61484, + 57694.97509, + 57277.53002, + 56735.31464, + 55930.75776, + 55377.59652, + 54644.43282, + 53870.52724, + 53885.04749, + 52855.68865, + 52177.68211, + 51412.88757, + 50460.74973, + 49619.37656, + 49131.31137, + 48790.71801, + 48438.97264, + 47858.08117, + 47000.13963, + 46406.40535, + 45625.41988, + 45089.88811, + 44157.237, + 43487.78784, + 42945.80872, + 42200.80914, + 41919.82696, + 41279.93288, + 40654.20345, + 40388.8085, + 39932.65319, + 39439.14734, + 39540.29557, + 39401.9003, + 39459.45855, + 39060.5108, + 39112.19842, + 39041.98604, + 39109.30786, + 39039.91078, + 39066.85051, + 38989.58122, + 39100.74794, + 39424.39692, + 39591.4502, + 39956.26674, + 39802.56272, + 39665.93912, + 40450.6599, + 40521.53868, + 41224.85871, + 41512.30364, + 42233.07041, + 43174.18897, + 43737.09765, + 44315.86275, + 45668.41729, + 46313.99274, + 47291.74881, + 47946.50624, + 48691.42526, + 49411.30337, + 50532.90616, + 50767.62495, + 51163.45579, + 51559.35094, + 52206.74121, + 53031.55111, + 53413.99109, + 54346.11427, + 54750.46531, + 54490.80945, + 54303.16323, + 53843.17998, + 53741.23547, + 53548.89843, + 53341.53414, + 52748.43147, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + { + "name": { + "en": "Load forecast", + "de": "Last Prognose", + "fr": "Charge pr\u00e9visions", + "it": "Carico prognosi", + "es": "Carga predicho" + }, + "linkedTo": "load", + "color": "rgb(75,75,75)", + "type": "line", + "visible": false, + "showInLegend": false, + "showInNavigator": true, + "allowCsvDownloadForItem": true, + "navigatorOptions": { + "visible": true, + "type": "line" + }, + "hideInPercentView": true, + "yAxis": 0, + "data": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 54811.417242, + 54363.342869, + 54206.371344, + 53600.89087, + 53554.907204, + 53496.759778, + 53682.946527, + 53752.348682, + 54036.329751, + 53860.37653, + 54460.011414, + 55058.504025, + 55727.340109, + 56410.507661, + 57257.056802, + 57910.348213, + 58728.863305, + 58687.291181, + 58612.773621, + 58979.868305, + 58703.898346, + 58642.030733, + 57917.8591, + 57344.999307, + 57015.372758, + 56288.17605, + 55668.305739, + 54900.496776, + 54110.189189, + 53683.315484, + 53089.305557, + 52477.020392, + 51886.138147, + 51508.418918, + 51161.426379, + 50577.390058, + 49822.491443, + 49287.64919, + 48324.89678, + 47634.275289, + 47061.394455, + 46445.760931 + ] + }, + { + "name": [ + { + "en": "Day Ahead Auction (DE-LU)", + "de": "Day Ahead Auktion (DE-LU)", + "fr": "Day Ahead Auction (DE-LU)", + "it": "Giorno prezzo spot avanti (DE-LU)", + "es": "Subasta del d\u00eda siguiente (DE-LU)" + } + ], + "currency": "EUR", + "unit": "EUR/MWh", + "color": "rgb(228, 26, 28)", + "type": "line", + "visible": true, + "showInLegend": true, + "allowCsvDownloadForItem": true, + "yAxis": 1, + "data": [ + 92.38, + 89.98, + 87.4, + 86.16, + 88.71, + 88.46, + 86.43, + 86.94, + 90.65, + 89.6, + 88.25, + 87.11, + 88.17, + 87.27, + 87.63, + 87.02, + 85.32, + 85.05, + 87.02, + 91.32, + 86.07, + 86.54, + 94.57, + 102.07, + 88.27, + 103.37, + 122.25, + 139.92, + 125.13, + 140.93, + 149.45, + 156.47, + 176.4, + 159.14, + 156.15, + 139.81, + 175.95, + 146.07, + 132.6, + 122.57, + 145.7, + 127.54, + 123.34, + 108.51, + 122.57, + 117.76, + 111.81, + 108.84, + 109.43, + 107.39, + 102.83, + 104.04, + 107.91, + 107.08, + 105.47, + 113.18, + 102.38, + 104.13, + 118.62, + 137.13, + 102.85, + 110.52, + 128.94, + 162.7, + 125.11, + 129.52, + 131.96, + 146.83, + 146.44, + 146.3, + 136.77, + 128.1, + 154.37, + 130.56, + 103.39, + 94.58, + 133.08, + 102.11, + 90.58, + 88.88, + 89.7, + 96.4, + 88.44, + 85.9, + 94.0, + 87.63, + 88.63, + 85.69, + 92.18, + 89.59, + 85.9, + 85.9, + 92.91, + 85.62, + 81.24, + 75.71, + 89.65, + 86.47, + 78.38, + 78.26, + 88.86, + 80.93, + 76.54, + 73.6, + 77.65, + 76.9, + 74.5, + 74.16, + 73.04, + 75.86, + 76.4, + 74.18, + 73.51, + 75.37, + 75.94, + 77.16, + 70.72, + 76.63, + 76.92, + 84.02, + 74.61, + 77.76, + 88.88, + 105.9, + 97.03, + 103.94, + 109.38, + 110.49, + 119.64, + 115.42, + 113.56, + 106.57, + 115.76, + 98.64, + 103.62, + 94.44, + 106.39, + 90.83, + 85.99, + 82.68, + 93.08, + 88.87, + 85.91, + 83.83, + 85.41, + 83.58, + 85.29, + 92.36, + 84.06, + 86.7, + 90.43, + 98.53, + 87.43, + 94.24, + 104.77, + 118.5, + 97.75, + 117.05, + 136.58, + 145.72, + 128.59, + 141.12, + 148.68, + 153.9, + 147.63, + 149.49, + 143.6, + 132.31, + 137.35, + 122.31, + 107.44, + 92.89, + 112.69, + 96.44, + 105.18, + 105.82, + 106.87, + 103.73, + 96.9, + 85.2, + 106.94, + 96.91, + 94.43, + 87.25, + 116.38, + 94.56, + 85.91, + 71.39, + 89.78, + 79.84, + 74.29, + 58.09, + 88.54, + 76.66, + 68.23, + 66.48, + 76.38, + 69.06, + 62.55, + 65.27, + 65.4, + 64.06, + 67.0, + 61.18, + 65.51, + 62.12, + 60.97, + 61.0, + 60.89, + 63.22, + 63.78, + 61.31, + 63.99, + 66.28, + 74.12, + 72.81, + 59.39, + 77.21, + 76.4, + 94.99, + 105.94, + 107.55, + 110.46, + 105.09, + 134.39, + 109.73, + 103.55, + 55.22, + 107.02, + 83.19, + 71.36, + 50.88, + 88.59, + 72.2, + 70.99, + 54.33, + 72.22, + 58.88, + 55.06, + 53.86, + 52.5, + 54.12, + 58.91, + 71.01, + 53.0, + 64.83, + 83.87, + 93.37, + 61.48, + 57.24, + 80.0, + 122.98, + 65.85, + 92.64, + 127.0, + 134.39, + 100.0, + 114.0, + 116.0, + 124.5, + 117.21, + 115.99, + 119.0, + 109.38, + 115.99, + 109.9, + 99.05, + 94.53, + 99.35, + 87.42, + 87.21, + 81.73, + 85.5, + 84.81, + 86.03, + 67.44, + 106.53, + 86.02, + 63.0, + 52.07, + 102.13, + 79.43, + 64.63, + 55.99, + 73.57, + 64.53, + 51.41, + 38.23, + 44.57, + 36.24, + 30.88, + 19.98, + 25.37, + 19.03, + 17.26, + 14.23, + 14.2, + 13.36, + 9.25, + 7.94, + 8.57, + 8.09, + 7.91, + 6.55, + 5.64, + 5.63, + 5.63, + 7.1, + 9.18, + 15.63, + 32.1, + 46.98, + 33.77, + 54.99, + 57.17, + 57.61, + 56.18, + 85.02, + 86.11, + 80.07, + 85.29, + 67.91, + 51.65, + 59.48, + 58.79, + 56.76, + 50.6, + 48.29, + 58.51, + 50.4, + 47.59, + 44.79, + 47.25, + 36.95, + 40.07, + 47.57, + 42.05, + 46.86, + 51.23, + 52.22, + 50.41, + 52.21, + 68.01, + 71.66, + 48.49, + 64.37, + 80.27, + 116.21, + 66.94, + 85.44, + 118.21, + 136.07, + 75.35, + 118.17, + 129.6, + 122.93, + 114.95, + 128.49, + 134.41, + 142.55, + 125.67, + 128.47, + 134.35, + 138.72, + 132.32, + 130.57, + 130.15, + 126.51, + 122.99, + 107.94, + 102.16, + 114.29, + 131.32, + 123.82, + 111.33, + 99.96, + 127.32, + 114.18, + 103.83, + 97.01, + 117.09, + 109.52, + 100.01, + 84.47, + 97.96, + 94.94, + 101.3, + 99.57, + 96.96, + 91.72, + 98.89, + 95.64, + 98.65, + 97.3, + 97.09, + 86.69, + 90.79, + 89.83, + 90.62, + 94.88, + 88.83, + 90.07, + 93.05, + 96.57, + 95.69, + 92.08, + 86.08, + 92.56, + 97.64, + 113.1, + 112.87, + 127.5, + 131.0, + 140.0, + 139.99, + 142.4, + 153.68, + 140.31, + 132.54, + 119.01, + 134.74, + 121.93, + 119.9, + 107.04, + 122.8, + 115.64, + 106.17, + 103.5, + 110.37, + 106.29, + 105.23, + 102.25, + 102.1, + 100.0, + 99.55, + 103.25, + 102.19, + 101.52, + 105.28, + 112.7, + 103.69, + 110.7, + 101.27, + 112.59, + 112.71, + 107.77, + 117.26, + 123.83, + 117.04, + 124.91, + 106.9, + 115.01, + 135.55, + 134.26, + 130.94, + 125.53, + 121.17, + 106.01, + 117.06, + 135.0, + 124.96, + 102.29, + 100.0, + 98.71, + 101.25, + 94.69, + 98.42, + 101.3, + 102.7, + 89.06, + 92.94, + 87.95, + 103.2, + 101.22, + 99.72, + 83.92, + 98.68, + 85.12, + 72.97, + 57.83, + 85.67, + 78.99, + 78.58, + 66.27, + 83.9, + 81.59, + 75.97, + 70.34, + 81.24, + 75.07, + 73.9, + 68.66, + 76.0, + 69.39, + 74.16, + 72.57, + 77.16, + 73.94, + 68.64, + 69.06, + 66.21, + 66.91, + 69.23, + 80.11, + 55.15, + 66.7, + 70.26, + 80.52, + 77.02, + 77.91, + 77.87, + 75.42, + 74.37, + 75.05, + 86.93, + 83.91, + 85.34, + 80.35, + 89.91, + 85.34, + 86.85, + 85.94, + 83.26, + 86.4, + 86.32, + 85.68, + 86.13, + 83.56, + 80.74, + 81.24, + 87.28, + 87.51, + 85.99, + 93.42, + 89.31, + 93.95, + 89.91, + 89.92, + 89.91, + 108.87, + 95.82, + 106.91, + 91.28, + 110.76, + 106.37, + 101.5, + 110.3, + 121.0, + 114.26, + 125.02, + 125.01, + 122.91, + 125.04, + 126.39, + 120.84, + 113.82, + 117.9, + 110.6, + 117.56, + 110.2, + 110.98, + 118.68, + 108.16, + 98.68, + 105.62, + 103.29, + 102.64, + 99.99, + 108.66, + 103.21, + 100.03, + 94.97, + 105.39, + 100.0, + 98.79, + 87.29, + 96.71, + 93.15, + 92.08, + 93.05, + 97.78, + 91.71, + 96.65, + 93.86, + 93.27, + 88.56, + 95.81, + 95.76, + 96.81, + 94.75, + 87.09, + 87.15, + 90.81, + 88.21, + 87.1, + 86.74, + 97.99, + 90.41, + 89.05, + 87.07, + 81.65, + 86.92, + 87.03, + 87.09, + 87.08, + 98.16, + 88.34, + 96.37, + 99.1, + 99.1, + 99.83, + 95.93, + 102.31, + 101.76, + 100.73, + 99.94, + 100.3, + 99.55, + 97.84, + 99.42, + 102.12, + 103.86, + 103.62, + 100.67, + 105.58, + 101.2, + 101.82, + 101.27, + 103.21, + 106.63, + 102.18, + 103.97, + 100.78, + 109.6, + 112.81, + 113.87, + 107.38, + 113.55, + 118.89, + 127.06, + 116.32, + 121.55, + 124.93, + 135.18, + 123.83, + 128.9, + 132.94, + 131.82, + 129.5, + 125.13, + 124.71, + 121.5, + 120.2, + 116.98, + 116.05, + 108.09, + 111.96, + 103.27, + 100.0, + 95.64, + 94.38, + 91.04, + 93.03, + 87.19, + 99.99, + 96.31, + 90.74, + 90.5, + 100.02, + 86.78, + 84.78, + 74.53 + ] + }, + { + "name": [ + { + "en": "Intraday Continuous 15 minutes Average Price (DE-LU)", + "de": "Intraday kontinuierlich, 15 Minuten Durchschnittspreis (DE-LU)", + "fr": "Intraday continuous 15 minutes, moyenne de prix (DE-LU)", + "it": "Prezzo medio intraday 15 minuti (DE-LU)" + } + ], + "currency": "EUR", + "unit": "EUR/MWh", + "color": "rgb(255, 154, 0)", + "type": "line", + "visible": true, + "showInLegend": true, + "allowCsvDownloadForItem": false, + "yAxis": 1, + "data": [ + 91.69, + 89.1, + 87.67, + 87.04, + 90.09, + 88.53, + 88.83, + 89.14, + 88.99, + 88.58, + 87.56, + 87.34, + 87.38, + 86.29, + 85.94, + 86.4, + 86.29, + 86.61, + 86.94, + 87.54, + 88.1, + 90.07, + 92.44, + 95.16, + 91.68, + 104.04, + 114.13, + 121.26, + 126.98, + 135.05, + 139.99, + 143.18, + 150.44, + 146.63, + 145.38, + 137.14, + 142.38, + 126.04, + 121.5, + 111.16, + 122.93, + 114.78, + 106.93, + 101.6, + 116.24, + 109.52, + 107.09, + 101.28, + 103.39, + 99.26, + 96.76, + 95.38, + 100.75, + 95.92, + 94.94, + 92.11, + 88.84, + 95.63, + 107.71, + 119.06, + 105.38, + 114.5, + 126.51, + 132.31, + 125.36, + 129.02, + 129.9, + 131.91, + 132.52, + 133.64, + 131.05, + 126.71, + 130.52, + 123.8, + 120.25, + 111.15, + 120.16, + 105.46, + 100.4, + 93.39, + 98.93, + 94.03, + 91.59, + 87.02, + 108.34, + 98.85, + 94.96, + 88.29, + 100.65, + 90.97, + 88.22, + 83.88, + 91.83, + 86.62, + 87.15, + 77.31, + 91.34, + 89.2, + 84.71, + 82.53, + 87.73, + 83.8, + 85.54, + 80.29, + 86.29, + 86.64, + 89.34, + 87.46, + 75.96, + 78.44, + 79.01, + 79.63, + 71.83, + 69.11, + 71.16, + 76.64, + 75.54, + 80.11, + 83.27, + 88.49, + 82.14, + 85.31, + 95.37, + 99.43, + 99.15, + 105.04, + 109.99, + 111.1, + 118.76, + 116.1, + 114.27, + 111.62, + 136.99, + 120.67, + 117.29, + 105.78, + 116.18, + 106.2, + 102.28, + 99.51, + 106.41, + 104.01, + 98.78, + 93.22, + 95.63, + 92.88, + 92.34, + 93.65, + 89.08, + 93.37, + 97.33, + 96.33, + 90.42, + 98.27, + 106.41, + 112.39, + 102.71, + 111.27, + 119.08, + 126.67, + 115.52, + 123.09, + 124.99, + 136.4, + 126.5, + 128.95, + 131.35, + 126.24, + 122.67, + 116.69, + 113.53, + 109.29, + 115.73, + 111.2, + 102.39, + 98.55, + 115.94, + 97.5, + 92.13, + 83.91, + 97.01, + 94.23, + 82.63, + 74.74, + 95.78, + 84.26, + 77.39, + 73.3, + 80.29, + 72.76, + 74.23, + 63.09, + 74.08, + 69.16, + 70.09, + 67.08, + 68.23, + 69.22, + 71.82, + 70.9, + 76.21, + 77.95, + 77.65, + 76.09, + 79.77, + 79.63, + 77.01, + 77.4, + 74.48, + 76.03, + 77.29, + 76.62, + 74.58, + 75.82, + 82.36, + 86.49, + 86.18, + 97.75, + 108.21, + 113.58, + 114.75, + 113.96, + 113.56, + 112.24, + 117.34, + 102.91, + 102.51, + 91.43, + 112.9, + 100.47, + 83.87, + 70.45, + 105.73, + 90.62, + 85.42, + 85.05, + 104.5, + 93.99, + 93.64, + 91.11, + 92.42, + 91.82, + 91.64, + 96.29, + 93.47, + 100.74, + 106.48, + 112.94, + 102.16, + 122.37, + 141.32, + 152.48, + 150.8, + 165.39, + 178.81, + 193.0, + 146.07, + 150.87, + 154.68, + 160.41, + 143.03, + 135.38, + 132.19, + 121.18, + 116.08, + 107.44, + 98.4, + 94.04, + 101.8, + 92.43, + 86.65, + 85.26, + 87.58, + 72.41, + 69.32, + 65.42, + 74.98, + 71.53, + 64.41, + 57.05, + 80.91, + 73.41, + 67.21, + 60.37, + 65.7, + 58.99, + 52.97, + 43.33, + 63.25, + 62.34, + 62.35, + 61.49, + 61.44, + 56.15, + 53.7, + 49.97, + 47.95, + 51.17, + 54.28, + 53.96, + 52.3, + 53.64, + 57.12, + 62.29, + 57.49, + 61.58, + 70.81, + 71.51, + 60.99, + 69.52, + 74.13, + 87.04, + 67.19, + 78.61, + 82.85, + 80.17, + 86.46, + 96.62, + 94.1, + 92.1, + 91.72, + 84.18, + 78.56, + 75.04, + 93.57, + 83.1, + 73.62, + 68.95, + 75.65, + 64.84, + 58.52, + 56.0, + 66.5, + 64.14, + 65.75, + 62.46, + 64.42, + 66.47, + 67.96, + 71.78, + 69.79, + 75.07, + 82.02, + 79.75, + 70.52, + 78.64, + 94.22, + 102.31, + 84.1, + 94.57, + 107.89, + 112.75, + 89.33, + 86.26, + 97.4, + 99.7, + 108.51, + 111.4, + 114.33, + 115.1, + 114.27, + 114.36, + 115.52, + 117.34, + 113.04, + 111.7, + 113.28, + 112.33, + 105.22, + 102.29, + 104.43, + 106.64, + 115.87, + 104.72, + 95.92, + 91.83, + 113.15, + 102.65, + 94.52, + 87.06, + 103.43, + 92.54, + 87.72, + 84.17, + 93.79, + 96.26, + 97.13, + 92.55, + 90.57, + 88.96, + 89.72, + 89.54, + 89.04, + 88.43, + 87.41, + 84.04, + 86.98, + 86.23, + 86.72, + 86.42, + 89.76, + 87.66, + 88.63, + 89.22, + 88.56, + 88.38, + 92.02, + 93.8, + 96.19, + 111.19, + 115.9, + 136.1, + 144.21, + 150.89, + 150.28, + 141.62, + 139.76, + 128.17, + 118.26, + 108.9, + 119.99, + 110.15, + 103.44, + 100.73, + 116.73, + 112.73, + 106.23, + 106.02, + 110.82, + 106.65, + 105.4, + 108.93, + 113.0, + 111.31, + 112.94, + 112.03, + 114.49, + 113.29, + 112.15, + 114.96, + 112.49, + 117.49, + 115.89, + 119.77, + 112.44, + 109.91, + 114.33, + 116.61, + 107.43, + 111.83, + 108.65, + 115.26, + 138.48, + 164.98, + 162.33, + 198.86, + 191.08, + 171.72, + 125.92, + 112.84, + 122.74, + 104.53, + 99.0, + 96.73, + 113.84, + 110.91, + 123.72, + 121.59, + 130.08, + 110.84, + 97.23, + 88.94, + 108.67, + 99.25, + 91.83, + 83.43, + 92.89, + 89.44, + 89.78, + 79.0, + 91.25, + 88.21, + 90.64, + 89.51, + 94.26, + 96.82, + 96.29, + 88.2, + 93.54, + 90.31, + 87.42, + 84.25, + 92.56, + 86.82, + 87.22, + 86.12, + 88.56, + 83.75, + 80.04, + 77.16, + 84.36, + 83.03, + 83.33, + 87.43, + 73.8, + 81.72, + 84.64, + 84.53, + 85.35, + 85.86, + 87.83, + 87.28, + 87.15, + 88.35, + 89.16, + 87.71, + 86.1, + 84.42, + 78.81, + 73.92, + 86.48, + 82.77, + 80.79, + 78.38, + 86.05, + 84.85, + 85.43, + 83.87, + 84.7, + 85.9, + 86.18, + 87.18, + 85.11, + 87.68, + 88.75, + 92.11, + 90.91, + 99.25, + 106.1, + 111.54, + 106.0, + 113.58, + 116.25, + 119.84, + 107.92, + 112.51, + 119.75, + 120.96, + 114.16, + 117.68, + 119.3, + 116.98, + 117.65, + 109.73, + 103.44, + 98.93, + 109.99, + 101.37, + 98.57, + 92.66, + 94.69, + 93.58, + 86.36, + 83.76, + 96.34, + 100.68, + 97.69, + 89.07, + 95.42, + 99.02, + 98.89, + 91.33, + 90.96, + 91.85, + 94.51, + 88.94, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + { + "name": [ + { + "en": "Pan-European Intraday auction, 15 minutes IDA1 price (DE-LU)", + "de": "Gekoppelte Intraday Auktion, 15 Minuten IDA1-Preis (DE-LU)", + "fr": "Intraday auction 15 minutes coupl\u00e9es, prix IDA1 (DE-LU)", + "it": "Contratti prezzo intraday 15 minuti accoppiata, prezzi IDA1 (DE-LU)", + "es": "Subasta intrad\u00eda 15 minutos acoplada, precios IDA1 (DE-LU)" + } + ], + "currency": "EUR", + "unit": "EUR/MWh", + "color": "rgb(33,113,181)", + "type": "line", + "visible": false, + "showInLegend": true, + "allowCsvDownloadForItem": false, + "yAxis": 1, + "data": [ + 91.0, + 86.0, + 90.09, + 88.61, + 91.82, + 88.6, + 91.18, + 86.64, + 91.32, + 89.55, + 88.04, + 86.69, + 89.18, + 87.93, + 87.93, + 86.69, + 83.96, + 86.61, + 86.45, + 91.7, + 86.6, + 82.3, + 96.0, + 92.34, + 99.88, + 104.06, + 114.75, + 119.11, + 127.02, + 135.95, + 144.99, + 148.03, + 160.03, + 147.8, + 145.84, + 137.93, + 149.51, + 135.0, + 131.9, + 123.0, + 135.78, + 134.99, + 128.88, + 116.34, + 125.24, + 120.81, + 122.58, + 112.8, + 110.99, + 113.0, + 113.0, + 109.95, + 109.0, + 113.0, + 113.0, + 110.72, + 105.5, + 113.0, + 124.99, + 131.52, + 114.93, + 126.09, + 134.99, + 144.99, + 134.99, + 141.09, + 131.89, + 136.03, + 135.09, + 154.99, + 145.2, + 141.95, + 140.63, + 135.0, + 128.69, + 110.7, + 128.94, + 115.62, + 106.81, + 103.27, + 105.46, + 107.82, + 101.13, + 90.79, + 103.4, + 100.65, + 98.66, + 87.0, + 100.33, + 98.3, + 95.81, + 87.0, + 95.85, + 94.82, + 91.81, + 77.0, + 73.9, + 85.89, + 78.98, + 75.63, + 82.8, + 78.95, + 74.7, + 71.51, + 83.61, + 76.63, + 76.01, + 73.9, + 76.36, + 72.01, + 73.9, + 69.28, + 78.81, + 75.8, + 69.18, + 78.23, + 78.28, + 76.6, + 86.4, + 91.0, + 81.02, + 80.72, + 95.0, + 102.75, + 99.83, + 113.52, + 115.69, + 115.8, + 124.96, + 121.04, + 110.89, + 107.12, + 115.9, + 112.27, + 101.32, + 93.88, + 102.01, + 95.42, + 88.66, + 86.97, + 93.42, + 92.28, + 87.03, + 85.72, + 84.29, + 83.68, + 82.69, + 85.8, + 79.9, + 80.1, + 85.9, + 91.82, + 82.4, + 90.28, + 107.99, + 108.29, + 100.01, + 108.09, + 117.0, + 132.22, + 119.0, + 129.73, + 134.68, + 152.99, + 133.1, + 133.2, + 135.74, + 129.95, + 117.99, + 114.65, + 110.61, + 107.45, + 114.55, + 112.97, + 109.99, + 106.97, + 111.25, + 111.23, + 93.6, + 91.8, + 113.74, + 102.07, + 90.07, + 85.72, + 97.47, + 91.9, + 85.96, + 84.18, + 89.47, + 85.5, + 81.1, + 68.8, + 83.62, + 82.92, + 78.65, + 72.01, + 79.79, + 74.8, + 74.0, + 71.35, + 71.62, + 67.49, + 72.3, + 71.76, + 71.33, + 69.89, + 67.48, + 67.62, + 70.58, + 67.47, + 68.75, + 68.01, + 62.74, + 66.25, + 86.41, + 85.8, + 86.0, + 88.56, + 87.0, + 84.93, + 97.2, + 102.63, + 110.11, + 110.58, + 110.95, + 102.45, + 91.35, + 83.0, + 99.22, + 88.1, + 80.0, + 67.01, + 87.42, + 79.38, + 77.0, + 67.89, + 74.2, + 69.89, + 67.48, + 64.71, + 67.44, + 67.45, + 68.77, + 68.89, + 67.48, + 67.61, + 74.9, + 74.41, + 70.5, + 67.66, + 84.97, + 96.36, + 87.66, + 97.69, + 107.0, + 113.99, + 102.34, + 109.03, + 106.0, + 120.23, + 106.0, + 108.5, + 106.72, + 104.37, + 103.22, + 95.11, + 92.47, + 90.8, + 82.09, + 80.52, + 78.27, + 78.84, + 75.67, + 72.8, + 72.8, + 70.49, + 77.29, + 70.42, + 63.0, + 56.92, + 75.7, + 66.95, + 63.52, + 56.03, + 55.8, + 51.47, + 50.44, + 42.06, + 54.34, + 44.95, + 41.58, + 25.14, + 43.46, + 35.0, + 24.9, + 17.97, + 27.14, + 23.11, + 15.72, + 13.0, + 17.7, + 15.78, + 15.31, + 12.75, + 12.03, + 11.99, + 16.0, + 23.98, + 23.12, + 42.99, + 48.89, + 64.98, + 59.24, + 70.88, + 72.14, + 74.98, + 86.8, + 93.9, + 93.9, + 98.06, + 100.25, + 75.03, + 68.27, + 64.38, + 68.51, + 65.25, + 58.67, + 58.12, + 62.0, + 53.38, + 54.06, + 49.72, + 50.3, + 47.0, + 46.42, + 47.8, + 48.73, + 52.5, + 52.97, + 56.02, + 54.97, + 57.13, + 57.99, + 62.5, + 63.52, + 67.3, + 76.7, + 98.49, + 76.22, + 97.06, + 127.37, + 130.03, + 102.66, + 114.4, + 121.99, + 119.18, + 120.0, + 120.56, + 126.5, + 130.03, + 129.23, + 120.36, + 121.8, + 119.81, + 124.17, + 120.01, + 117.7, + 114.06, + 113.37, + 107.3, + 103.52, + 105.84, + 116.38, + 113.19, + 100.67, + 98.05, + 110.0, + 97.0, + 93.33, + 89.08, + 94.5, + 89.58, + 87.47, + 83.54, + 97.96, + 94.94, + 101.3, + 99.57, + 96.96, + 91.72, + 98.89, + 95.64, + 98.65, + 97.3, + 97.09, + 86.69, + 90.79, + 89.83, + 90.62, + 94.88, + 88.83, + 90.07, + 93.05, + 96.57, + 95.69, + 92.08, + 86.08, + 92.56, + 97.64, + 113.1, + 112.87, + 127.5, + 131.0, + 140.0, + 139.99, + 142.4, + 153.68, + 140.31, + 132.54, + 119.01, + 134.74, + 121.93, + 119.9, + 107.04, + 122.8, + 115.64, + 106.17, + 103.5, + 110.37, + 106.29, + 105.23, + 102.25, + 102.1, + 100.0, + 99.55, + 103.25, + 102.19, + 101.52, + 105.28, + 112.7, + 103.69, + 110.7, + 101.27, + 112.59, + 112.71, + 107.77, + 117.26, + 123.83, + 117.04, + 124.91, + 106.9, + 115.01, + 135.55, + 134.26, + 130.94, + 125.53, + 121.17, + 106.01, + 117.06, + 135.0, + 124.96, + 102.29, + 100.0, + 98.71, + 101.25, + 94.69, + 98.42, + 101.3, + 102.7, + 89.06, + 92.94, + 87.95, + 103.2, + 101.22, + 99.72, + 83.92, + 98.68, + 85.12, + 72.97, + 57.83, + 83.53, + 77.96, + 79.52, + 74.79, + 85.82, + 83.9, + 73.9, + 76.38, + 83.39, + 78.67, + 79.54, + 77.2, + 82.34, + 79.52, + 76.24, + 73.9, + 79.55, + 79.6, + 75.75, + 74.87, + 77.89, + 76.2, + 72.14, + 76.17, + 62.59, + 75.2, + 74.0, + 76.85, + 76.38, + 78.56, + 79.58, + 75.08, + 81.09, + 83.51, + 83.9, + 83.9, + 87.8, + 86.99, + 86.11, + 81.21, + 89.2, + 83.9, + 84.69, + 84.09, + 88.04, + 84.26, + 86.35, + 84.33, + 79.55, + 79.66, + 89.48, + 89.03, + 85.46, + 85.45, + 87.63, + 91.9, + 87.02, + 88.38, + 104.08, + 105.07, + 99.96, + 96.66, + 111.73, + 120.41, + 103.0, + 109.52, + 121.0, + 117.64, + 113.21, + 119.0, + 119.99, + 119.0, + 124.93, + 119.0, + 114.05, + 110.01, + 114.84, + 104.0, + 106.76, + 98.8, + 108.99, + 102.17, + 97.82, + 95.5, + 103.53, + 97.42, + 92.8, + 92.8, + 97.54, + 96.0, + 98.99, + 94.08, + 95.62, + 95.07, + 92.92, + 89.14, + 93.01, + 92.67, + 91.79, + 90.2, + 92.78, + 89.77, + 92.46, + 91.96, + 91.5, + 87.12, + 92.68, + 92.34, + 93.18, + 90.36, + 85.34, + 86.61, + 86.74, + 86.45, + 85.38, + 80.07, + 91.97, + 85.49, + 85.38, + 86.42, + 71.0, + 81.96, + 83.9, + 87.13, + 83.9, + 93.47, + 85.34, + 88.37, + 91.01, + 95.1, + 91.97, + 88.4, + 103.08, + 94.19, + 89.28, + 91.02, + 92.6, + 95.34, + 92.91, + 96.32, + 94.32, + 99.28, + 101.4, + 99.28, + 102.99, + 97.88, + 102.53, + 99.28, + 100.15, + 102.09, + 102.67, + 104.1, + 95.84, + 98.0, + 110.92, + 116.29, + 97.94, + 107.6, + 112.42, + 119.22, + 111.35, + 116.2, + 119.52, + 125.21, + 118.88, + 123.31, + 131.04, + 126.0, + 124.56, + 124.95, + 114.88, + 113.25, + 123.47, + 111.9, + 105.96, + 95.39, + 113.82, + 107.41, + 95.0, + 87.24, + 106.25, + 95.15, + 90.47, + 86.12, + 104.28, + 101.12, + 88.8, + 85.58, + 96.64, + 91.9, + 82.86, + 77.38 + ] + }, + { + "name": [ + { + "en": "Pan-European Intraday auction, 15 minutes IDA2 price (DE-LU)", + "de": "Gekoppelte Intraday Auktion, 15 Minuten IDA2-Preis (DE-LU)", + "fr": "Intraday auction 15 minutes coupl\u00e9es, prix IDA2 (DE-LU)", + "it": "Contratti prezzo intraday 15 minuti accoppiata, prezzi IDA2 (DE-LU)", + "es": "Subasta intrad\u00eda 15 minutos acoplada, precios IDA2 (DE-LU)" + } + ], + "currency": "EUR", + "unit": "EUR/MWh", + "color": "rgb(107,174,214)", + "type": "line", + "visible": false, + "showInLegend": true, + "allowCsvDownloadForItem": false, + "yAxis": 1, + "data": [ + 92.11, + 85.35, + 82.81, + 81.14, + 86.17, + 83.7, + 80.0, + 84.63, + 84.03, + 84.5, + 84.0, + 82.96, + 82.09, + 80.28, + 80.22, + 79.91, + 80.3, + 79.65, + 83.69, + 84.03, + 90.8, + 79.84, + 84.32, + 91.91, + 92.99, + 102.92, + 120.07, + 125.54, + 131.75, + 139.99, + 143.99, + 147.43, + 155.97, + 151.94, + 147.31, + 140.0, + 155.97, + 140.83, + 132.4, + 123.0, + 131.0, + 136.15, + 126.64, + 115.91, + 130.93, + 121.91, + 117.69, + 112.98, + 115.3, + 112.89, + 109.92, + 109.05, + 112.0, + 114.09, + 112.89, + 116.73, + 111.5, + 116.0, + 123.99, + 136.0, + 117.88, + 126.78, + 139.21, + 148.66, + 135.83, + 141.67, + 141.95, + 146.0, + 140.01, + 143.03, + 137.26, + 133.65, + 143.0, + 129.01, + 117.01, + 110.27, + 140.0, + 131.18, + 121.1, + 106.35, + 112.73, + 115.9, + 104.11, + 99.66, + 108.71, + 102.08, + 89.95, + 85.04, + 91.47, + 88.15, + 87.02, + 79.89, + 88.06, + 88.39, + 83.75, + 73.02, + 90.38, + 86.17, + 80.05, + 75.23, + 89.07, + 77.71, + 79.05, + 70.17, + 78.0, + 72.73, + 72.02, + 70.73, + 66.04, + 65.9, + 70.88, + 72.79, + 70.05, + 69.76, + 69.38, + 71.09, + 72.37, + 73.29, + 80.39, + 81.07, + 83.99, + 88.8, + 98.29, + 104.99, + 102.99, + 103.58, + 109.89, + 108.92, + 125.2, + 113.02, + 113.94, + 101.49, + 115.0, + 104.96, + 101.1, + 91.42, + 104.32, + 94.0, + 89.13, + 84.01, + 97.81, + 95.54, + 88.7, + 87.95, + 87.23, + 85.04, + 82.64, + 84.52, + 85.38, + 85.0, + 88.0, + 90.0, + 82.07, + 89.26, + 97.33, + 101.94, + 83.27, + 99.07, + 117.44, + 125.41, + 117.72, + 123.57, + 129.95, + 139.02, + 129.02, + 127.11, + 126.59, + 119.81, + 135.0, + 100.91, + 82.59, + 81.5, + 103.2, + 98.01, + 99.63, + 99.16, + 102.62, + 102.01, + 90.9, + 88.48, + 103.85, + 92.33, + 83.69, + 82.61, + 91.45, + 79.02, + 77.44, + 72.33, + 79.99, + 73.18, + 68.82, + 64.01, + 73.4, + 71.99, + 66.91, + 62.41, + 69.8, + 63.37, + 58.63, + 60.01, + 61.65, + 61.8, + 62.25, + 60.01, + 64.67, + 62.8, + 60.07, + 57.46, + 58.06, + 57.61, + 60.0, + 58.55, + 60.0, + 61.15, + 69.45, + 67.24, + 64.85, + 73.04, + 73.1, + 79.07, + 96.76, + 92.09, + 96.59, + 95.6, + 82.01, + 71.14, + 75.48, + 86.81, + 76.32, + 63.56, + 65.07, + 49.8, + 66.19, + 59.11, + 57.34, + 49.8, + 64.55, + 59.96, + 56.12, + 53.01, + 64.4, + 65.99, + 67.28, + 67.99, + 65.99, + 69.95, + 76.69, + 83.76, + 68.17, + 71.84, + 88.34, + 104.0, + 92.13, + 102.0, + 112.45, + 122.91, + 104.6, + 110.47, + 113.51, + 122.0, + 107.59, + 107.56, + 105.79, + 100.66, + 102.06, + 92.91, + 89.5, + 85.14, + 81.15, + 75.72, + 73.04, + 70.1, + 70.18, + 66.16, + 63.91, + 57.8, + 73.69, + 65.59, + 58.75, + 38.0, + 71.29, + 58.63, + 57.08, + 51.21, + 49.94, + 42.27, + 40.22, + 29.05, + 59.65, + 55.38, + 49.28, + 39.73, + 44.1, + 31.7, + 30.2, + 30.0, + 30.0, + 25.31, + 23.09, + 21.6, + 22.2, + 19.0, + 21.23, + 24.78, + 17.48, + 19.95, + 28.01, + 34.21, + 25.08, + 39.27, + 55.65, + 72.0, + 58.18, + 78.16, + 82.72, + 84.99, + 88.26, + 101.27, + 96.78, + 92.0, + 93.98, + 84.0, + 61.09, + 56.01, + 64.08, + 55.78, + 49.89, + 50.25, + 56.0, + 52.83, + 50.59, + 40.8, + 49.6, + 38.4, + 36.0, + 40.0, + 50.56, + 53.79, + 51.75, + 51.91, + 53.14, + 58.72, + 61.13, + 71.99, + 53.01, + 61.08, + 68.44, + 86.93, + 59.03, + 80.02, + 100.75, + 106.0, + 80.54, + 94.86, + 99.01, + 117.58, + 116.79, + 118.36, + 120.91, + 119.93, + 120.0, + 117.1, + 117.91, + 116.99, + 115.27, + 112.43, + 110.25, + 107.28, + 103.0, + 95.18, + 93.65, + 97.99, + 106.98, + 97.41, + 92.04, + 91.57, + 103.22, + 98.4, + 88.38, + 82.0, + 88.57, + 84.01, + 77.05, + 70.0, + 87.41, + 91.5, + 91.99, + 86.97, + 89.01, + 87.26, + 88.51, + 87.1, + 86.66, + 86.62, + 87.92, + 82.0, + 83.0, + 81.75, + 82.45, + 83.55, + 82.22, + 82.42, + 84.27, + 86.01, + 84.01, + 82.23, + 81.94, + 84.9, + 89.02, + 98.8, + 99.49, + 123.77, + 117.83, + 122.03, + 124.43, + 128.0, + 134.06, + 129.89, + 117.32, + 108.26, + 124.8, + 116.44, + 102.06, + 99.61, + 117.09, + 108.08, + 98.83, + 95.59, + 99.62, + 98.05, + 95.05, + 92.88, + 94.01, + 87.83, + 91.03, + 95.13, + 95.0, + 94.29, + 97.51, + 98.28, + 83.67, + 109.8, + 95.08, + 120.79, + 117.69, + 110.81, + 118.58, + 120.92, + 112.41, + 113.04, + 103.15, + 112.03, + 119.19, + 118.33, + 121.06, + 118.02, + 109.01, + 98.2, + 101.57, + 106.83, + 109.03, + 97.0, + 90.31, + 89.62, + 103.4, + 99.51, + 92.05, + 94.0, + 102.36, + 91.45, + 87.96, + 87.03, + 109.69, + 104.63, + 96.01, + 89.68, + 96.0, + 88.0, + 79.81, + 67.34, + 79.6, + 76.86, + 76.85, + 70.03, + 79.55, + 76.37, + 73.64, + 70.01, + 76.45, + 73.02, + 70.81, + 69.72, + 73.28, + 71.03, + 71.07, + 70.02, + 73.29, + 71.34, + 70.11, + 69.34, + 77.9, + 77.99, + 76.15, + 79.15, + 58.97, + 68.02, + 68.72, + 71.08, + 77.35, + 77.41, + 78.04, + 76.78, + 80.11, + 81.15, + 82.91, + 81.99, + 89.41, + 90.55, + 89.62, + 83.9, + 90.91, + 88.99, + 89.07, + 87.99, + 91.92, + 89.87, + 88.99, + 87.19, + 87.96, + 85.97, + 89.91, + 90.98, + 89.93, + 91.31, + 89.95, + 95.97, + 87.99, + 91.77, + 97.97, + 109.81, + 96.92, + 104.68, + 101.0, + 107.52, + 97.05, + 95.09, + 104.84, + 102.31, + 104.52, + 112.42, + 112.39, + 112.06, + 113.28, + 112.73, + 108.0, + 102.0, + 110.5, + 100.06, + 107.27, + 95.57, + 99.09, + 101.61, + 89.1, + 90.0, + 98.97, + 99.58, + 92.27, + 93.31, + 97.51, + 94.96, + 95.9, + 89.41, + 91.92, + 93.3, + 92.95, + 86.0, + 98.1, + 98.31, + 98.0, + 93.99, + 96.46, + 95.0, + 95.18, + 93.36, + 97.74, + 92.11, + 96.99, + 92.57, + 99.91, + 95.69, + 91.49, + 93.95, + 92.99, + 93.4, + 93.1, + 91.99, + 96.96, + 94.0, + 92.82, + 88.22, + 84.71, + 89.39, + 90.71, + 89.39, + 90.54, + 95.25, + 91.2, + 93.01, + 97.35, + 97.91, + 96.34, + 93.9, + 106.19, + 101.17, + 93.41, + 92.08, + 99.63, + 93.01, + 91.44, + 95.92, + 98.0, + 98.2, + 97.74, + 94.05, + 106.0, + 100.0, + 101.41, + 98.0, + 104.24, + 102.5, + 100.01, + 100.02, + 96.35, + 97.55, + 112.69, + 108.01, + 100.01, + 108.83, + 117.99, + 123.85, + 117.9, + 123.07, + 124.0, + 135.91, + 126.52, + 125.13, + 126.55, + 126.21, + 126.8, + 123.21, + 118.92, + 115.91, + 126.93, + 122.07, + 114.98, + 97.07, + 110.03, + 100.06, + 99.9, + 88.01, + 92.39, + 97.45, + 93.5, + 82.17, + 92.1, + 86.94, + 83.95, + 83.08, + 86.37, + 78.58, + 76.72, + 71.85 + ] + }, + { + "name": [ + { + "en": "Pan-European Intraday auction, 15 minutes IDA3 price (DE-LU)", + "de": "Gekoppelte Intraday Auktion, 15 Minuten IDA3-Preis (DE-LU)", + "fr": "Intraday auction 15 minutes coupl\u00e9es, prix IDA3 (DE-LU)", + "it": "Contratti prezzo intraday 15 minuti accoppiata, prezzi IDA3 (DE-LU)", + "es": "Subasta intrad\u00eda 15 minutos acoplada, precios IDA3 (DE-LU)" + } + ], + "currency": "EUR", + "unit": "EUR/MWh", + "color": "rgb(189,215,231)", + "type": "line", + "visible": false, + "showInLegend": true, + "allowCsvDownloadForItem": false, + "yAxis": 1, + "data": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 102.01, + 101.0, + 98.01, + 98.01, + 101.7, + 101.81, + 101.14, + 103.38, + 98.0, + 106.0, + 114.99, + 127.99, + 109.99, + 117.99, + 129.97, + 140.47, + 124.0, + 128.05, + 132.01, + 136.77, + 137.44, + 140.31, + 133.84, + 130.01, + 137.91, + 124.16, + 116.01, + 104.93, + 130.19, + 118.01, + 106.01, + 92.07, + 101.58, + 104.0, + 98.0, + 90.18, + 101.87, + 95.99, + 91.11, + 87.04, + 86.0, + 86.0, + 83.92, + 81.28, + 85.1, + 84.67, + 76.12, + 65.31, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 89.0, + 87.01, + 87.94, + 88.99, + 86.01, + 86.0, + 90.22, + 92.37, + 86.14, + 90.15, + 98.99, + 104.72, + 93.99, + 106.0, + 120.41, + 126.6, + 119.54, + 127.01, + 132.46, + 141.0, + 131.0, + 128.1, + 128.05, + 119.23, + 119.82, + 106.01, + 95.94, + 93.0, + 101.7, + 94.22, + 94.48, + 93.0, + 100.76, + 88.31, + 87.03, + 82.88, + 93.01, + 87.42, + 79.68, + 74.08, + 92.0, + 84.81, + 76.29, + 72.01, + 77.99, + 71.88, + 71.99, + 66.95, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 88.01, + 88.01, + 91.1, + 92.01, + 86.63, + 87.17, + 93.03, + 97.7, + 86.27, + 88.82, + 100.8, + 116.43, + 100.0, + 110.99, + 122.01, + 131.01, + 114.0, + 118.0, + 118.02, + 127.99, + 120.91, + 116.67, + 118.01, + 108.01, + 113.93, + 97.49, + 89.02, + 84.01, + 85.02, + 76.0, + 71.54, + 68.0, + 71.18, + 67.12, + 64.0, + 58.89, + 67.01, + 58.85, + 52.01, + 49.73, + 64.44, + 53.06, + 48.33, + 42.17, + 52.56, + 41.21, + 28.0, + 26.67, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 57.82, + 60.01, + 55.08, + 57.01, + 53.64, + 59.04, + 62.98, + 64.55, + 60.44, + 65.99, + 70.98, + 82.71, + 65.21, + 80.0, + 93.84, + 104.37, + 79.96, + 92.91, + 99.0, + 103.24, + 102.0, + 111.98, + 114.12, + 117.9, + 111.01, + 110.01, + 113.03, + 113.77, + 114.0, + 112.01, + 113.17, + 112.61, + 108.64, + 96.01, + 96.39, + 99.33, + 113.99, + 104.87, + 94.0, + 90.65, + 112.26, + 96.59, + 95.49, + 86.94, + 105.99, + 92.85, + 87.01, + 81.47, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 106.76, + 103.88, + 103.44, + 100.01, + 104.34, + 104.0, + 104.9, + 112.24, + 101.97, + 110.4, + 104.12, + 114.89, + 109.95, + 108.0, + 114.99, + 118.0, + 108.95, + 112.01, + 101.37, + 108.07, + 118.2, + 119.0, + 117.85, + 113.01, + 109.0, + 99.65, + 99.89, + 103.04, + 103.44, + 91.98, + 91.37, + 89.46, + 94.06, + 89.88, + 88.21, + 87.92, + 92.99, + 86.3, + 87.71, + 86.99, + 90.2, + 89.95, + 89.99, + 80.99, + 86.67, + 82.99, + 77.01, + 63.14, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 81.55, + 82.0, + 85.69, + 86.91, + 86.95, + 86.94, + 87.92, + 90.35, + 87.92, + 89.98, + 95.06, + 100.99, + 92.11, + 95.94, + 97.86, + 104.11, + 94.0, + 96.0, + 103.18, + 107.0, + 101.99, + 109.65, + 108.64, + 105.0, + 111.81, + 107.99, + 101.66, + 94.06, + 102.35, + 95.16, + 97.73, + 89.01, + 96.0, + 99.61, + 91.0, + 85.9, + 91.99, + 90.25, + 89.99, + 88.49, + 95.36, + 91.39, + 90.0, + 87.04, + 90.0, + 89.03, + 88.52, + 81.5, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 102.08, + 100.32, + 102.28, + 99.38, + 100.0, + 98.92, + 98.92, + 102.66, + 96.63, + 100.2, + 109.09, + 114.08, + 99.6, + 108.01, + 116.97, + 126.0, + 112.24, + 121.13, + 126.0, + 131.07, + 126.0, + 127.71, + 125.08, + 121.21, + 128.11, + 127.08, + 124.8, + 119.93, + 117.97, + 120.26, + 113.0, + 102.95, + 115.94, + 104.0, + 99.05, + 92.0, + 98.99, + 93.42, + 92.0, + 86.99, + 95.0, + 90.78, + 85.99, + 87.0, + 91.01, + 81.47, + 80.0, + 65.9 + ] + }, + { + "name": [ + { + "en": "Intraday Continuous 15 minutes Low Price (DE-LU)", + "de": "Intraday kontinuierlich, 15 Minuten Niedrigstpreis (DE-LU)", + "fr": "Intraday continuous 15 minutes, prix le plus bas (DE-LU)", + "it": "Prezzo basso intraday 15 minuti (DE-LU)" + } + ], + "currency": "EUR", + "unit": "EUR/MWh", + "color": "rgb(0, 255, 0)", + "type": "line", + "visible": false, + "showInLegend": true, + "allowCsvDownloadForItem": false, + "yAxis": 1, + "data": [ + 76.7, + 77.5, + 13.9, + 77.43, + 63.24, + 66.32, + 80.18, + 75.21, + 69.63, + 75.67, + 81.71, + 53.48, + 43.58, + 74.49, + 75.1, + 79.98, + 77.84, + 77.71, + 48.92, + 53.32, + 81.21, + -13.48, + 63.51, + 81.08, + 73.0, + 80.81, + 82.59, + 84.96, + 95.92, + 111.0, + 107.48, + 107.27, + 117.2, + 122.41, + 111.42, + 99.05, + 95.74, + 76.49, + 78.71, + 59.46, + 73.97, + 62.22, + 45.54, + 72.05, + 74.95, + 53.81, + 78.19, + 76.72, + 59.51, + 71.15, + 38.41, + 38.72, + 26.36, + -2.44, + 55.04, + 47.43, + 55.2, + 60.83, + 86.25, + 78.01, + 90.97, + 79.35, + 101.9, + 95.43, + 106.6, + 108.09, + 96.51, + 79.87, + 99.42, + 109.15, + 113.41, + 112.94, + 95.78, + 102.91, + 102.03, + 93.81, + 79.83, + 65.3, + 78.0, + 69.91, + 79.8, + 27.7, + 80.32, + 73.54, + 89.33, + 79.84, + 79.76, + 75.37, + 86.27, + 74.08, + 77.22, + 70.0, + 69.51, + 70.61, + 71.3, + 63.5, + 77.63, + 80.0, + 68.53, + 72.43, + 77.47, + 73.91, + 69.19, + 70.4, + 74.2, + 71.75, + 71.55, + 68.98, + 48.25, + 67.01, + 68.42, + 65.78, + 18.05, + -49.91, + 19.0, + 35.42, + 39.34, + 59.12, + 30.0, + 74.27, + 52.34, + 55.02, + 62.24, + 66.55, + 76.51, + 79.7, + 80.8, + 93.1, + 88.56, + 98.31, + 70.95, + 59.76, + 106.23, + 99.09, + 95.48, + 55.41, + 69.73, + 74.51, + 46.23, + 81.32, + 85.31, + 77.52, + 75.0, + 67.27, + 82.91, + 75.77, + 65.03, + 58.01, + 61.57, + 74.86, + 73.8, + 78.26, + 76.6, + 82.61, + 85.37, + 92.72, + 83.73, + 89.06, + 79.86, + 105.0, + 44.15, + 96.08, + 20.94, + 86.88, + 98.03, + 94.47, + 90.38, + 97.95, + 94.6, + 82.01, + 84.5, + 84.5, + 89.2, + 81.88, + 68.63, + 66.33, + 82.11, + 73.0, + 71.0, + 56.01, + 76.81, + 68.9, + 69.9, + 61.75, + 74.69, + 68.71, + 64.0, + 55.44, + 65.77, + 50.59, + 59.15, + 42.74, + 50.42, + 27.7, + 57.11, + 52.02, + 54.63, + 58.26, + 54.97, + 55.1, + 61.43, + 61.11, + 59.39, + 34.09, + 61.96, + 60.8, + 51.39, + 57.48, + 52.8, + 58.01, + 60.19, + 51.05, + 56.99, + 54.82, + 68.3, + 57.26, + 45.53, + 72.5, + 75.43, + 75.0, + 92.11, + 90.81, + 75.21, + 76.21, + 89.6, + 63.52, + 58.99, + 43.03, + 76.88, + 64.5, + 34.8, + 22.77, + 68.3, + 57.68, + 58.46, + 27.32, + 36.51, + 61.7, + 57.54, + 55.0, + 24.81, + 53.62, + 61.62, + 64.01, + 63.62, + 50.45, + 70.24, + 76.17, + 64.5, + 67.72, + 81.7, + 97.68, + 79.5, + 79.79, + 104.8, + 104.76, + 44.19, + 70.13, + 63.71, + 60.41, + 52.21, + 75.02, + 76.03, + 65.16, + 64.67, + 75.4, + 70.8, + 59.03, + 76.1, + 62.06, + 59.31, + 61.72, + 54.57, + 37.42, + 37.14, + 37.75, + 50.54, + 36.34, + 35.03, + 13.76, + 61.5, + 52.79, + 47.61, + 18.7, + 38.26, + 34.63, + 27.7, + 20.81, + 41.03, + 38.26, + 6.0, + 25.9, + 30.45, + -2.39, + 15.83, + 13.15, + -31.9, + 11.14, + 8.5, + 6.09, + 12.49, + -15.56, + -54.69, + -6.5, + 1.69, + 8.79, + 15.72, + 17.1, + 11.62, + 28.24, + 32.9, + 55.6, + 9.12, + 23.2, + 24.03, + 29.5, + 15.18, + 58.73, + 22.19, + 58.34, + 68.22, + 63.41, + 50.12, + 38.04, + 51.0, + 46.27, + 39.63, + 36.71, + 45.53, + 32.4, + 23.76, + 16.98, + 29.03, + 24.24, + 21.4, + 21.82, + 15.0, + 28.9, + 34.31, + 38.0, + 31.37, + 40.0, + 46.2, + 44.54, + -6.75, + 53.43, + 59.0, + 69.65, + 53.13, + 59.0, + 83.85, + 73.11, + 51.76, + 19.0, + 50.96, + 72.34, + 72.67, + 39.52, + 36.13, + 92.43, + 100.18, + 98.75, + 94.22, + 102.73, + 90.31, + 87.69, + 96.95, + 94.82, + 77.43, + 69.59, + 70.86, + 69.05, + 71.99, + 75.8, + 75.61, + 58.74, + 93.4, + 86.41, + 63.49, + 57.95, + 62.96, + 55.11, + 73.46, + 63.3, + 82.6, + 81.21, + 85.3, + 81.48, + 69.2, + -12.72, + 48.87, + 80.64, + 54.97, + 56.06, + 61.9, + 70.42, + 62.89, + 65.88, + 40.9, + 16.33, + 29.42, + 15.39, + 61.54, + 71.8, + 46.24, + 60.76, + 54.44, + 67.83, + 45.79, + 87.96, + 88.48, + 61.34, + 98.51, + 102.84, + 100.67, + 72.73, + 88.7, + 59.1, + 67.01, + 63.97, + 74.54, + 72.19, + 61.34, + 72.78, + 65.83, + 77.07, + 74.96, + 47.72, + 60.72, + 41.3, + 56.14, + 83.5, + 31.0, + 57.74, + 57.48, + 82.9, + 86.6, + 84.39, + 78.01, + 61.27, + 84.79, + 65.86, + 91.39, + 74.04, + 28.75, + 64.01, + 85.51, + 75.3, + 86.97, + 92.02, + 90.45, + 95.22, + 74.06, + 81.93, + 83.46, + 98.93, + 82.62, + 77.0, + 83.18, + 77.8, + 76.4, + 78.61, + 79.7, + 72.4, + 78.04, + 79.23, + 69.92, + 81.56, + 71.08, + 82.52, + 22.61, + 59.79, + 84.0, + -50.0, + 64.58, + 59.77, + 0.0, + 62.36, + 53.98, + 47.9, + 75.37, + 77.3, + 53.65, + 70.59, + 76.66, + 65.31, + 63.73, + 69.83, + 68.32, + 63.83, + 62.47, + 67.0, + 61.83, + 71.22, + 11.01, + 72.58, + 73.21, + 66.02, + 50.68, + -8.9, + 57.5, + 59.4, + -48.86, + 65.97, + 4.62, + 63.81, + 65.91, + 51.53, + 70.26, + 30.13, + 64.89, + 65.0, + 72.92, + 57.05, + 54.86, + 64.42, + -3.5, + 29.23, + 19.1, + 6.12, + 19.1, + 19.1, + 39.73, + 23.91, + 56.22, + 51.28, + 54.8, + 53.07, + 73.42, + 68.38, + 71.8, + 63.13, + 58.0, + 9.17, + 62.44, + 73.72, + 68.12, + 78.77, + 79.46, + 87.01, + 81.04, + 90.7, + 85.58, + 98.93, + 60.63, + 91.65, + 69.67, + 98.66, + 94.01, + 96.64, + 100.0, + 83.22, + 84.68, + 77.91, + 81.05, + 82.24, + 88.09, + 77.23, + 70.4, + 76.68, + 55.8, + 54.45, + 59.22, + 61.0, + 84.0, + 69.78, + 80.3, + 65.73, + 68.94, + 63.32, + 64.88, + 79.25, + 77.1, + 65.4, + 81.7, + 61.33, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + { + "name": [ + { + "en": "Intraday Continuous 15 minutes High Price (DE-LU)", + "de": "Intraday kontinuierlich, 15 Minuten H\u00f6chstpreis (DE-LU)", + "fr": "Intraday continuous 15 minutes, prix le plus haut (DE-LU)", + "it": "Prezzo elevato intraday 15 minuti (DE-LU)" + } + ], + "currency": "EUR", + "unit": "EUR/MWh", + "color": "rgb(160, 32, 240)", + "type": "line", + "visible": false, + "showInLegend": true, + "allowCsvDownloadForItem": false, + "yAxis": 1, + "data": [ + 117.96, + 188.0, + 187.69, + 183.54, + 108.77, + 105.19, + 133.08, + 133.77, + 121.19, + 138.48, + 134.27, + 133.02, + 118.0, + 132.56, + 135.52, + 135.08, + 117.92, + 130.93, + 131.77, + 139.28, + 117.85, + 200.0, + 114.37, + 147.39, + 125.32, + 142.0, + 146.75, + 148.99, + 136.8, + 159.05, + 194.44, + 182.97, + 174.07, + 170.66, + 169.29, + 160.88, + 223.88, + 192.57, + 180.91, + 170.09, + 186.84, + 174.18, + 170.96, + 154.08, + 156.11, + 161.56, + 159.26, + 157.06, + 143.43, + 156.46, + 149.38, + 150.15, + 142.46, + 154.76, + 152.53, + 160.88, + 137.95, + 149.78, + 184.21, + 183.29, + 141.86, + 160.48, + 178.58, + 209.4, + 156.95, + 184.1, + 204.3, + 199.58, + 185.47, + 197.19, + 191.88, + 197.5, + 216.2, + 193.88, + 166.6, + 163.2, + 197.1, + 158.38, + 149.5, + 177.8, + 171.28, + 148.07, + 138.03, + 133.94, + 172.02, + 149.29, + 136.17, + 131.49, + 152.9, + 135.99, + 189.98, + 132.57, + 129.5, + 134.69, + 138.36, + 123.27, + 145.1, + 134.4, + 125.29, + 970.02, + 122.52, + 126.04, + 122.72, + 129.99, + 139.03, + 123.09, + 135.16, + 122.81, + 104.98, + 124.72, + 123.3, + 123.03, + 104.58, + 122.16, + 124.87, + 125.05, + 104.28, + 121.89, + 124.21, + 122.1, + 108.6, + 107.38, + 130.95, + 125.27, + 124.38, + 142.47, + 138.61, + 147.51, + 241.65, + 159.26, + 216.56, + 152.82, + 262.7, + 160.0, + 155.0, + 142.3, + 139.8, + 169.83, + 145.51, + 146.49, + 800.0, + 227.12, + 133.81, + 130.56, + 250.0, + 128.38, + 131.1, + 136.77, + 111.16, + 140.0, + 136.28, + 140.98, + 106.69, + 124.18, + 152.11, + 164.69, + 129.69, + 164.95, + 178.09, + 185.88, + 153.48, + 178.34, + 194.68, + 196.39, + 177.48, + 800.0, + 229.0, + 178.28, + 180.98, + 190.08, + 153.86, + 137.18, + 144.51, + 144.26, + 151.82, + 152.25, + 147.21, + 150.06, + 143.59, + 134.05, + 141.84, + 145.8, + 141.81, + 135.18, + 162.69, + 141.12, + 133.88, + 119.28, + 125.27, + 126.39, + 122.28, + 104.51, + 118.63, + 121.5, + 116.5, + 112.7, + 112.39, + 118.9, + 140.0, + 122.87, + 128.15, + 120.32, + 121.7, + 113.07, + 123.7, + 119.99, + 115.13, + 109.16, + 93.6, + 110.31, + 110.42, + 108.96, + 102.6, + 114.49, + 120.98, + 127.48, + 124.29, + 124.96, + 231.1, + 249.34, + 136.59, + 170.7, + 149.9, + 133.63, + 157.72, + 179.49, + 267.42, + 134.95, + 278.99, + 138.0, + 118.96, + 101.2, + 171.0, + 142.4, + 141.9, + 136.57, + 180.0, + 147.39, + 124.66, + 141.0, + 122.87, + 133.97, + 122.86, + 125.79, + 146.67, + 167.55, + 170.0, + 169.0, + 198.22, + 350.0, + 314.08, + 250.57, + 356.35, + 354.74, + 399.1, + 370.83, + 232.33, + 225.58, + 261.0, + 278.94, + 238.88, + 217.9, + 215.64, + 187.31, + 194.54, + 161.89, + 145.19, + 192.88, + 200.28, + 171.47, + 153.86, + 128.46, + 130.45, + 131.66, + 131.3, + 114.4, + 151.56, + 131.96, + 110.96, + 101.56, + 149.06, + 127.16, + 113.0, + 104.54, + 102.23, + 112.54, + 96.5, + 84.6, + 89.12, + 87.54, + 101.14, + 110.23, + 102.58, + 103.95, + 122.87, + 174.78, + 87.26, + 90.35, + 106.47, + 179.19, + 116.24, + 105.7, + 110.1, + 101.0, + 106.39, + 104.29, + 97.27, + 90.9, + 97.79, + 89.79, + 90.23, + 120.96, + 86.99, + 100.57, + 177.66, + 132.28, + 106.7, + 145.0, + 121.91, + 122.62, + 120.17, + 105.89, + 258.49, + 248.06, + 179.7, + 185.54, + 146.32, + 125.45, + 123.38, + 129.84, + 95.09, + 107.78, + 131.89, + 128.46, + 120.48, + 127.5, + 125.27, + 109.97, + 120.0, + 120.05, + 152.5, + 161.35, + 125.37, + 119.99, + 96.06, + 114.03, + 168.75, + 197.16, + 129.45, + 149.01, + 165.69, + 177.48, + 113.0, + 164.34, + 220.54, + 170.21, + 146.08, + 177.0, + 180.96, + 182.88, + 151.31, + 174.76, + 181.29, + 225.77, + 157.78, + 178.96, + 173.18, + 172.79, + 156.9, + 155.84, + 151.14, + 162.67, + 158.93, + 165.09, + 156.41, + 147.46, + 159.47, + 161.61, + 149.63, + 144.16, + 149.9, + 154.47, + 145.22, + 133.54, + 138.86, + 205.82, + 150.28, + 145.6, + 130.8, + 140.76, + 208.88, + 144.66, + 133.36, + 146.45, + 144.46, + 134.66, + 125.39, + 137.86, + 200.84, + 144.37, + 124.71, + 138.57, + 142.07, + 145.56, + 135.5, + 140.06, + 135.17, + 127.95, + 138.12, + 147.99, + 160.0, + 330.6, + 286.99, + 346.75, + 242.0, + 194.69, + 800.0, + 250.0, + 180.81, + 233.35, + 210.77, + 169.76, + 170.87, + 220.51, + 179.08, + 169.88, + 155.48, + 175.0, + 154.3, + 153.77, + 157.68, + 194.87, + 206.13, + 285.45, + 199.49, + 159.5, + 165.52, + 156.04, + 215.32, + 160.97, + 143.08, + 158.96, + 150.56, + 160.66, + 152.19, + 157.06, + 166.36, + 172.86, + 160.58, + 171.97, + 155.76, + 182.16, + 443.19, + 608.29, + 416.98, + 999.97, + 699.99, + 549.03, + 249.76, + 182.8, + 179.38, + 151.57, + 149.17, + 146.76, + 196.13, + 327.1, + 313.39, + 317.4, + 263.39, + 204.74, + 140.96, + 135.96, + 169.0, + 149.26, + 148.76, + 197.18, + 163.66, + 133.16, + 143.89, + 110.93, + 151.71, + 148.4, + 135.29, + 137.23, + 164.33, + 222.17, + 165.53, + 195.09, + 125.5, + 122.43, + 119.01, + 117.14, + 146.59, + 116.99, + 120.57, + 119.89, + 117.18, + 120.26, + 116.12, + 116.48, + 99.76, + 114.86, + 111.17, + 126.43, + 98.87, + 136.88, + 109.89, + 133.99, + 142.5, + 100.09, + 118.68, + 130.56, + 107.73, + 132.03, + 133.28, + 131.38, + 153.68, + 163.89, + 137.1, + 132.23, + 120.03, + 133.88, + 162.49, + 133.41, + 114.08, + 132.6, + 129.43, + 129.59, + 118.45, + 128.38, + 134.78, + 132.88, + 119.6, + 141.4, + 154.07, + 141.14, + 140.07, + 157.99, + 154.7, + 156.34, + 143.78, + 154.29, + 138.8, + 171.59, + 141.18, + 149.91, + 157.7, + 179.48, + 143.82, + 185.7, + 219.89, + 170.34, + 153.74, + 169.46, + 140.0, + 162.79, + 150.76, + 158.85, + 164.61, + 157.39, + 139.19, + 148.79, + 144.45, + 140.54, + 136.2, + 156.0, + 161.0, + 141.15, + 139.49, + 150.12, + 155.92, + 139.45, + 134.65, + 142.08, + 369.98, + 135.65, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + { + "name": [ + { + "en": "Intraday Continuous 15 minutes ID3-Price (DE-LU)", + "de": "Intraday kontinuierlich, 15 Minuten ID3-Preis (DE-LU)", + "fr": "Intraday continuous 15 minutes ID3-prix (DE-LU)", + "it": "ID3 prezzo intraday 15 minuti (DE-LU)" + } + ], + "currency": "EUR", + "unit": "EUR/MWh", + "color": "rgb(255, 204, 0)", + "type": "line", + "visible": false, + "showInLegend": true, + "allowCsvDownloadForItem": false, + "yAxis": 1, + "data": [ + 92.05, + 88.78, + 87.62, + 86.37, + 89.26, + 88.01, + 88.0, + 89.39, + 89.22, + 88.91, + 88.01, + 87.94, + 87.16, + 86.49, + 85.96, + 86.32, + 85.88, + 86.36, + 86.57, + 86.3, + 87.75, + 89.05, + 90.99, + 91.04, + 88.38, + 97.23, + 108.83, + 117.73, + 126.93, + 132.17, + 137.41, + 142.79, + 149.85, + 148.76, + 149.1, + 141.93, + 145.24, + 130.67, + 126.58, + 112.75, + 123.42, + 111.18, + 104.99, + 99.66, + 110.39, + 105.38, + 102.09, + 98.96, + 99.7, + 97.1, + 95.91, + 94.31, + 96.61, + 95.63, + 93.57, + 89.92, + 85.48, + 90.76, + 100.61, + 114.25, + 101.33, + 111.47, + 124.96, + 129.78, + 124.2, + 127.55, + 129.81, + 133.15, + 131.21, + 131.84, + 128.59, + 124.57, + 128.6, + 123.1, + 120.49, + 110.83, + 117.54, + 105.21, + 98.54, + 91.58, + 96.66, + 89.57, + 87.98, + 83.13, + 102.63, + 94.84, + 93.39, + 87.63, + 104.2, + 91.94, + 89.28, + 85.23, + 96.15, + 87.31, + 85.29, + 77.52, + 90.99, + 89.02, + 85.59, + 80.81, + 87.9, + 84.03, + 83.62, + 79.57, + 81.52, + 83.46, + 87.4, + 88.51, + 78.19, + 80.99, + 81.35, + 81.94, + 73.67, + 72.41, + 77.78, + 78.76, + 75.55, + 79.52, + 80.92, + 84.56, + 78.41, + 82.77, + 89.86, + 94.39, + 95.24, + 98.75, + 102.16, + 105.07, + 114.64, + 113.87, + 111.65, + 104.59, + 133.5, + 120.83, + 116.25, + 109.76, + 122.21, + 111.03, + 104.55, + 99.16, + 103.98, + 103.17, + 101.47, + 95.06, + 94.06, + 92.26, + 93.13, + 94.91, + 90.64, + 93.18, + 95.83, + 96.92, + 92.42, + 100.36, + 109.05, + 116.38, + 105.87, + 114.38, + 122.73, + 127.6, + 118.82, + 123.51, + 127.65, + 142.5, + 125.67, + 126.46, + 130.38, + 125.02, + 123.03, + 115.38, + 117.19, + 112.58, + 120.0, + 114.42, + 104.92, + 100.97, + 120.2, + 102.88, + 98.03, + 87.4, + 99.51, + 94.2, + 81.9, + 72.5, + 98.45, + 82.85, + 77.51, + 73.21, + 80.86, + 71.86, + 72.28, + 64.37, + 74.52, + 70.02, + 69.13, + 66.81, + 65.48, + 62.73, + 62.68, + 64.97, + 74.56, + 73.64, + 75.21, + 74.18, + 77.29, + 77.97, + 79.8, + 80.34, + 77.78, + 80.07, + 80.86, + 79.31, + 76.36, + 78.17, + 82.95, + 86.37, + 86.54, + 99.92, + 110.18, + 119.59, + 119.87, + 121.6, + 123.51, + 121.02, + 118.19, + 100.11, + 100.59, + 95.18, + 119.16, + 107.42, + 98.42, + 78.58, + 107.57, + 90.36, + 85.81, + 83.64, + 102.37, + 94.23, + 95.92, + 94.94, + 98.46, + 96.41, + 98.06, + 101.96, + 96.77, + 101.62, + 107.94, + 116.22, + 103.35, + 123.91, + 136.18, + 152.74, + 147.28, + 168.74, + 192.96, + 220.77, + 163.72, + 172.36, + 177.1, + 183.95, + 159.84, + 146.16, + 134.08, + 120.62, + 111.02, + 97.85, + 86.33, + 81.54, + 91.25, + 86.91, + 79.1, + 81.8, + 89.94, + 75.89, + 69.07, + 64.39, + 77.23, + 73.4, + 63.25, + 57.39, + 84.16, + 75.68, + 67.58, + 62.63, + 71.18, + 64.75, + 58.15, + 47.82, + 65.58, + 64.74, + 62.33, + 61.11, + 58.96, + 56.67, + 57.31, + 55.0, + 55.98, + 57.72, + 60.84, + 61.01, + 61.04, + 61.12, + 68.02, + 71.82, + 71.32, + 75.45, + 82.82, + 81.89, + 70.79, + 77.38, + 79.95, + 88.11, + 65.33, + 75.94, + 82.82, + 84.35, + 92.67, + 95.55, + 93.08, + 88.22, + 86.18, + 83.12, + 72.08, + 54.9, + 83.59, + 75.44, + 65.33, + 66.16, + 78.38, + 67.18, + 60.37, + 56.72, + 68.46, + 66.17, + 67.26, + 67.51, + 71.05, + 71.94, + 71.16, + 75.32, + 75.27, + 79.47, + 85.46, + 87.41, + 80.74, + 84.98, + 90.67, + 101.26, + 88.28, + 95.1, + 104.63, + 113.98, + 95.99, + 96.85, + 101.65, + 102.05, + 110.78, + 111.79, + 114.62, + 113.85, + 111.94, + 112.62, + 115.04, + 114.16, + 109.66, + 110.05, + 110.35, + 112.16, + 104.23, + 101.02, + 103.64, + 108.68, + 119.36, + 107.35, + 95.26, + 91.1, + 114.84, + 101.21, + 91.01, + 84.92, + 102.02, + 91.22, + 86.49, + 83.95, + 92.74, + 93.21, + 92.51, + 90.68, + 89.29, + 88.03, + 89.74, + 89.19, + 86.57, + 87.13, + 86.53, + 82.35, + 86.01, + 83.65, + 86.04, + 86.04, + 89.07, + 88.09, + 88.92, + 89.09, + 87.28, + 87.27, + 93.36, + 93.44, + 93.91, + 109.13, + 112.38, + 125.33, + 137.5, + 153.8, + 152.71, + 149.51, + 141.72, + 138.06, + 127.59, + 114.96, + 120.85, + 108.32, + 97.99, + 92.68, + 113.28, + 108.45, + 100.67, + 104.32, + 109.7, + 104.18, + 102.78, + 104.56, + 114.09, + 115.06, + 116.65, + 117.23, + 120.64, + 121.68, + 120.68, + 122.42, + 120.68, + 124.09, + 122.01, + 124.65, + 117.55, + 115.49, + 114.98, + 114.06, + 103.52, + 108.67, + 104.8, + 110.73, + 115.02, + 127.49, + 141.28, + 170.12, + 170.46, + 177.26, + 138.87, + 120.11, + 132.59, + 111.42, + 102.72, + 99.46, + 114.59, + 104.98, + 102.06, + 105.09, + 135.5, + 118.9, + 103.35, + 92.72, + 114.69, + 107.46, + 97.09, + 84.37, + 94.61, + 86.2, + 85.89, + 79.7, + 88.62, + 84.45, + 82.6, + 83.07, + 91.91, + 90.05, + 91.26, + 89.42, + 96.09, + 93.96, + 91.34, + 88.94, + 96.21, + 89.34, + 88.29, + 87.16, + 89.98, + 86.48, + 84.93, + 83.75, + 86.92, + 84.91, + 83.15, + 85.52, + 73.78, + 80.1, + 81.75, + 85.01, + 85.75, + 86.03, + 87.72, + 88.16, + 88.38, + 89.72, + 91.53, + 91.43, + 91.76, + 89.43, + 88.67, + 80.83, + 91.23, + 86.16, + 84.33, + 82.33, + 86.94, + 82.46, + 82.02, + 82.04, + 81.35, + 83.13, + 85.17, + 87.83, + 85.52, + 86.11, + 86.07, + 92.05, + 88.49, + 91.72, + 102.2, + 112.98, + 110.38, + 119.31, + 120.89, + 124.52, + 110.22, + 116.06, + 121.12, + 123.86, + 116.92, + 120.07, + 120.78, + 119.32, + 118.94, + 113.97, + 103.87, + 98.11, + 111.46, + 103.11, + 101.19, + 91.77, + 96.87, + 93.87, + 85.78, + 81.1, + 92.98, + 92.68, + 93.35, + 88.43, + 94.05, + 94.62, + 97.04, + 91.53, + 92.62, + 90.86, + 91.3, + 87.88, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + { + "name": [ + { + "en": "Intraday Continuous 15 minutes ID1-Price (DE-LU)", + "de": "Intraday kontinuierlich, 15 Minuten ID1-Preis (DE-LU)", + "fr": "Intraday continuous 15 minutes ID1-prix (DE-LU)", + "it": "ID1 prezzo intraday 15 minuti (DE-LU)" + } + ], + "currency": "EUR", + "unit": "EUR/MWh", + "color": "rgb(255, 255, 0)", + "type": "line", + "visible": false, + "showInLegend": true, + "allowCsvDownloadForItem": false, + "yAxis": 1, + "data": [ + 89.54, + 88.32, + 87.89, + 86.64, + 89.19, + 87.06, + 87.41, + 90.53, + 87.62, + 86.64, + 87.16, + 87.89, + 86.98, + 85.94, + 85.39, + 85.71, + 85.07, + 86.67, + 86.95, + 85.97, + 86.3, + 87.69, + 91.79, + 91.5, + 91.91, + 104.35, + 124.68, + 130.49, + 128.92, + 135.11, + 151.21, + 165.41, + 166.72, + 156.09, + 147.42, + 134.37, + 135.09, + 116.32, + 107.12, + 92.06, + 101.66, + 97.65, + 95.72, + 94.55, + 109.18, + 104.68, + 106.4, + 100.19, + 96.34, + 91.9, + 91.24, + 89.33, + 90.82, + 89.03, + 87.15, + 83.37, + 80.11, + 86.66, + 94.57, + 117.19, + 112.07, + 118.11, + 125.64, + 127.55, + 125.12, + 129.27, + 128.89, + 131.3, + 123.94, + 121.86, + 121.92, + 124.46, + 131.99, + 125.57, + 120.24, + 108.97, + 110.98, + 99.65, + 100.29, + 91.44, + 95.4, + 88.02, + 92.69, + 86.26, + 110.09, + 99.1, + 98.54, + 92.2, + 110.39, + 93.07, + 92.52, + 88.65, + 97.99, + 85.3, + 81.79, + 78.31, + 92.19, + 93.7, + 89.69, + 83.25, + 91.04, + 86.27, + 88.13, + 83.38, + 83.21, + 88.33, + 93.32, + 95.18, + 79.89, + 84.74, + 82.01, + 83.69, + 65.37, + 63.35, + 70.45, + 67.92, + 68.5, + 76.34, + 77.66, + 84.39, + 80.16, + 89.23, + 97.73, + 98.19, + 96.7, + 102.06, + 104.76, + 110.49, + 124.72, + 124.2, + 121.8, + 114.35, + 155.94, + 130.25, + 123.69, + 118.2, + 124.55, + 106.18, + 98.53, + 100.71, + 99.36, + 108.87, + 113.04, + 97.11, + 91.04, + 94.16, + 97.29, + 99.37, + 92.3, + 94.37, + 96.95, + 95.67, + 96.44, + 106.23, + 108.5, + 113.78, + 108.28, + 117.3, + 119.58, + 121.41, + 113.1, + 116.96, + 121.42, + 135.84, + 126.22, + 130.02, + 132.07, + 128.53, + 131.78, + 123.15, + 127.43, + 121.6, + 118.92, + 105.66, + 95.23, + 95.33, + 126.39, + 97.08, + 95.06, + 83.63, + 92.71, + 94.48, + 81.33, + 71.29, + 101.34, + 79.52, + 76.85, + 76.82, + 82.27, + 71.55, + 73.27, + 65.03, + 70.43, + 69.85, + 70.46, + 67.22, + 63.04, + 61.99, + 66.75, + 73.18, + 82.16, + 83.37, + 84.35, + 84.09, + 82.78, + 83.67, + 86.33, + 85.58, + 75.7, + 78.74, + 79.72, + 77.61, + 75.27, + 79.03, + 84.07, + 86.87, + 93.75, + 107.06, + 115.68, + 131.95, + 123.34, + 124.29, + 124.09, + 117.88, + 107.72, + 93.0, + 108.99, + 107.95, + 124.92, + 110.27, + 101.02, + 75.95, + 105.15, + 85.09, + 86.26, + 83.96, + 109.08, + 105.72, + 108.26, + 102.92, + 99.92, + 99.12, + 95.26, + 98.94, + 102.28, + 107.88, + 110.69, + 120.6, + 113.58, + 137.48, + 148.47, + 174.43, + 184.16, + 204.13, + 262.7, + 258.56, + 162.74, + 134.81, + 124.24, + 120.57, + 111.55, + 106.16, + 102.95, + 97.83, + 94.46, + 84.1, + 81.4, + 79.49, + 91.42, + 97.22, + 87.97, + 98.37, + 108.91, + 78.8, + 65.95, + 61.49, + 71.86, + 74.81, + 65.06, + 59.7, + 88.5, + 79.75, + 69.9, + 69.36, + 74.52, + 67.0, + 57.01, + 43.67, + 69.32, + 75.4, + 63.31, + 65.04, + 67.57, + 78.48, + 74.08, + 71.34, + 59.12, + 63.38, + 69.6, + 73.18, + 75.68, + 73.87, + 81.09, + 80.24, + 79.52, + 80.36, + 83.13, + 77.46, + 70.85, + 76.28, + 76.57, + 79.26, + 59.12, + 73.0, + 88.86, + 81.73, + 88.8, + 84.32, + 75.37, + 81.38, + 84.74, + 86.71, + 80.5, + 55.14, + 96.36, + 93.22, + 86.47, + 90.42, + 97.8, + 84.28, + 68.14, + 60.3, + 81.54, + 77.76, + 82.54, + 82.22, + 84.51, + 76.92, + 75.14, + 83.4, + 86.69, + 87.32, + 92.77, + 94.54, + 87.24, + 83.75, + 86.54, + 104.74, + 91.86, + 99.01, + 112.78, + 122.97, + 103.08, + 94.87, + 93.31, + 91.27, + 98.47, + 101.75, + 113.75, + 112.57, + 113.0, + 116.51, + 121.16, + 120.25, + 104.76, + 108.72, + 115.53, + 121.94, + 106.84, + 102.77, + 111.05, + 120.42, + 122.01, + 107.47, + 95.52, + 91.95, + 115.92, + 103.77, + 89.66, + 86.74, + 102.31, + 89.32, + 87.79, + 88.21, + 95.71, + 94.65, + 93.43, + 94.59, + 88.83, + 89.45, + 90.12, + 88.2, + 85.86, + 87.04, + 85.54, + 78.21, + 80.87, + 80.28, + 87.22, + 87.4, + 92.74, + 89.84, + 89.43, + 89.33, + 84.78, + 87.49, + 97.2, + 94.65, + 98.01, + 113.54, + 117.44, + 134.38, + 155.42, + 194.58, + 178.45, + 159.85, + 138.39, + 127.75, + 119.73, + 107.98, + 114.98, + 98.83, + 90.98, + 95.22, + 119.28, + 118.63, + 113.93, + 120.24, + 119.87, + 110.33, + 114.02, + 116.85, + 121.44, + 126.44, + 130.94, + 126.21, + 128.26, + 123.26, + 117.1, + 117.02, + 120.23, + 120.67, + 117.8, + 121.45, + 109.35, + 104.93, + 102.79, + 103.5, + 101.14, + 115.04, + 111.53, + 118.67, + 121.1, + 148.33, + 177.38, + 232.69, + 230.97, + 266.48, + 169.59, + 117.15, + 110.25, + 96.94, + 92.24, + 94.41, + 126.15, + 113.07, + 115.45, + 122.08, + 168.01, + 135.52, + 105.68, + 85.18, + 103.86, + 103.04, + 91.71, + 78.68, + 87.98, + 82.64, + 89.82, + 85.71, + 93.64, + 88.04, + 84.02, + 88.88, + 97.87, + 94.97, + 99.1, + 95.38, + 102.77, + 97.56, + 93.09, + 87.94, + 98.84, + 85.57, + 87.86, + 88.96, + 92.38, + 89.05, + 86.65, + 82.27, + 84.79, + 80.74, + 78.17, + 82.41, + 70.03, + 84.22, + 86.74, + 87.75, + 86.75, + 86.24, + 88.02, + 89.51, + 89.32, + 90.69, + 93.0, + 94.78, + 93.21, + 89.52, + 84.19, + 71.65, + 86.54, + 79.07, + 78.99, + 76.11, + 83.79, + 78.0, + 79.18, + 84.33, + 84.31, + 88.0, + 89.14, + 92.31, + 87.27, + 83.93, + 85.6, + 96.87, + 90.07, + 92.22, + 110.14, + 122.22, + 121.33, + 125.25, + 124.65, + 128.44, + 106.49, + 109.77, + 121.09, + 124.74, + 118.22, + 121.5, + 121.87, + 119.14, + 117.59, + 109.11, + 96.65, + 93.63, + 108.2, + 102.19, + 101.69, + 92.32, + 93.77, + 89.72, + 83.84, + 78.75, + 93.42, + 98.12, + 101.65, + 94.27, + 98.42, + 100.12, + 105.92, + 96.86, + 94.01, + 91.87, + 92.38, + 90.0, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + { + "name": [ + { + "en": "Intraday Balancing Energy Price (ID AEP) 15 minutes (DE-LU)", + "de": "Intraday Index Ausgleichsenergiepreis (ID AEP) 15 Minuten (DE-LU)", + "fr": "Prix d'\u00e9quilibre intrajournalier (ID AEP) 15 minutes (DE-LU)", + "it": "Prezzo di bilanciamento intraday (ID AEP) 15 minuti (DE-LU)", + "es": "Precio de equilibrio intrad\u00eda (ID AEP) 15 minutos (DE-LU)" + } + ], + "currency": "EUR", + "unit": "EUR/MWh", + "color": "rgb(255, 145, 0)", + "type": "line", + "visible": false, + "showInLegend": true, + "allowCsvDownloadForItem": true, + "yAxis": 1, + "data": [ + 87.33, + 87.8, + 86.88, + 91.42, + 94.24, + 91.11, + 94.9, + 90.62, + 85.97, + 86.32, + 86.0, + 87.85, + 88.47, + 83.76, + 82.87, + 89.22, + 87.59, + 88.95, + 88.66, + 92.73, + 89.66, + 101.26, + 100.64, + 114.75, + 109.9, + 122.82, + 135.55, + 136.19, + 127.1, + 149.11, + 148.55, + 160.52, + 152.31, + 139.16, + 136.31, + 108.68, + 111.01, + 85.26, + 90.31, + 85.12, + 88.82, + 96.02, + 84.52, + 93.4, + 103.91, + 115.88, + 104.67, + 88.85, + 86.66, + 82.03, + 82.35, + 77.7, + 85.45, + 87.82, + 77.9, + 66.16, + 74.06, + 92.42, + 127.23, + 107.47, + 115.25, + 123.51, + 128.02, + 128.75, + 125.28, + 128.06, + 121.19, + 111.76, + 124.47, + 127.77, + 125.95, + 119.75, + 122.56, + 111.25, + 126.04, + 115.18, + 99.68, + 88.42, + 96.29, + 90.84, + 108.01, + 87.85, + 96.18, + 113.04, + 140.92, + 116.13, + 111.46, + 101.45, + 111.24, + 91.23, + 89.8, + 77.2, + 82.15, + 101.51, + 113.31, + 92.47, + 102.75, + 95.23, + 82.11, + 94.2, + 90.65, + 86.1, + 99.19, + 91.79, + 99.22, + 105.24, + 109.46, + 96.22, + 72.47, + 80.38, + 78.86, + 78.16, + 53.77, + 44.05, + 38.7, + 67.57, + 71.66, + 80.61, + 87.08, + 101.95, + 79.66, + 74.57, + 104.02, + 105.9, + 96.7, + 116.82, + 125.55, + 133.63, + 128.72, + 127.32, + 137.01, + 135.19, + 200.48, + 134.64, + 133.05, + 87.88, + 111.17, + 89.4, + 110.85, + 115.4, + 145.41, + 95.06, + 89.54, + 100.17, + 110.76, + 98.09, + 91.85, + 87.01, + 86.89, + 115.33, + 122.42, + 97.38, + 96.74, + 103.74, + 107.31, + 105.23, + 107.49, + 106.87, + 97.59, + 116.84, + 98.02, + 108.51, + 96.63, + 104.4, + 111.67, + 140.88, + 143.27, + 143.89, + 130.19, + 122.51, + 111.39, + 109.53, + 101.44, + 110.55, + 96.27, + 83.6, + 109.37, + 86.71, + 80.29, + 73.29, + 102.54, + 98.17, + 92.73, + 79.15, + 100.49, + 90.06, + 77.02, + 78.34, + 88.05, + 96.19, + 81.22, + 62.29, + 67.53, + 63.87, + 70.47, + 66.11, + 74.65, + 91.57, + 111.77, + 104.26, + 89.63, + 99.77, + 97.28, + 97.66, + 104.4, + 97.95, + 71.51, + 76.64, + 75.44, + 78.3, + 80.56, + 81.89, + 85.88, + 80.94, + 91.42, + 105.3, + 111.56, + 115.62, + 132.61, + 121.43, + 121.08, + 113.46, + 86.9, + 100.86, + 129.28, + 119.88, + 120.54, + 115.11, + 148.85, + 100.01, + 55.5, + 78.68, + 142.12, + 112.06, + 122.71, + 121.72, + 133.15, + 122.51, + 111.66, + 91.54, + 89.65, + 93.2, + 83.39, + 95.78, + 123.96, + 141.67, + 137.63, + 139.8, + 169.94, + 193.82, + 216.47, + 234.12, + 235.49, + 144.1, + 157.55, + 128.5, + 71.73, + 83.51, + 74.15, + 84.01, + 87.5, + 87.08, + 98.93, + 86.32, + 75.73, + 84.75, + 93.87, + 131.44, + 111.43, + 107.93, + 108.75, + 93.31, + 90.52, + 55.08, + 57.84, + 74.27, + 82.34, + 82.7, + 86.49, + 67.8, + 76.67, + 92.47, + 76.21, + 69.92, + 60.56, + 56.69, + 53.37, + 40.22, + 67.35, + 58.09, + 77.61, + 87.1, + 84.57, + 68.48, + 71.79, + 70.76, + 63.22, + 80.16, + 93.6, + 85.84, + 92.09, + 82.81, + 92.6, + 93.3, + 78.69, + 78.7, + 82.03, + 76.58, + 76.61, + 73.0, + 69.94, + 92.69, + 65.25, + 62.6, + 63.15, + 34.83, + 41.54, + 122.26, + 72.38, + 94.03, + 79.17, + 78.86, + 140.32, + 154.56, + 134.42, + 122.78, + 121.23, + 93.49, + 99.4, + 77.57, + 77.64, + 93.3, + 112.42, + 105.26, + 96.79, + 73.71, + 91.04, + 94.59, + 103.07, + 98.18, + 105.67, + 105.08, + 99.82, + 70.66, + 57.56, + 95.74, + 130.89, + 143.11, + 110.25, + 127.12, + 134.44, + 95.82, + 70.88, + 37.89, + 77.45, + 83.58, + 94.62, + 113.58, + 108.63, + 113.24, + 122.78, + 128.51, + 114.92, + 146.67, + 126.62, + 110.33, + 126.81, + 109.32, + 107.7, + 118.04, + 119.43, + 111.1, + 121.28, + 87.83, + 91.21, + 93.64, + 125.48, + 123.49, + 112.69, + 105.42, + 122.31, + 96.58, + 95.73, + 88.86, + 93.61, + 106.32, + 115.11, + 101.14, + 87.18, + 85.17, + 84.82, + 87.43, + 87.41, + 83.64, + 86.44, + 84.17, + 81.54, + 87.92, + 84.98, + 80.43, + 96.29, + 82.21, + 84.42, + 84.1, + 87.59, + 96.04, + 99.76, + 98.49, + 116.23, + 133.52, + 143.7, + 235.38, + 208.3, + 204.2, + 177.54, + 131.29, + 162.96, + 89.61, + 77.46, + 91.66, + 141.17, + 138.85, + 132.35, + 135.76, + 144.93, + 140.98, + 124.22, + 141.79, + 125.68, + 117.63, + 146.85, + 160.98, + 135.29, + 141.07, + 133.28, + 123.62, + 135.51, + 123.83, + 98.8, + 104.36, + 120.8, + 121.95, + 109.68, + 105.95, + 84.3, + 78.88, + 119.46, + 117.59, + 109.91, + 118.33, + 125.63, + 147.88, + 302.0, + 358.1, + 337.23, + 735.59, + 227.65, + 105.64, + 108.44, + 85.75, + 87.93, + 88.14, + 89.27, + 101.65, + 142.19, + 163.24, + 230.29, + 176.84, + 178.24, + 114.47, + 93.86, + 77.51, + 106.22, + 75.03, + 84.55, + 78.8, + 98.88, + 114.03, + 122.57, + 92.25, + 101.86, + 106.14, + 118.97, + 120.8, + 114.39, + 131.68, + 120.84, + 102.05, + 99.86, + 97.41, + 90.77, + 79.13, + 94.58, + 91.99, + 97.24, + 95.62, + 98.9, + 77.6, + 66.63, + 45.43, + 76.02, + 72.59, + 85.96, + 103.0, + 77.49, + 95.08, + 101.75, + 86.69, + 86.67, + 86.31, + 87.05, + 87.86, + 84.6, + 87.85, + 81.9, + 79.04, + 73.35, + 61.35, + 40.23, + 34.44, + 66.38, + 68.05, + 62.95, + 51.09, + 79.14, + 102.46, + 95.68, + 81.06, + 94.02, + 86.16, + 82.05, + 74.08, + 75.79, + 99.54, + 97.54, + 88.51, + 113.89, + 136.0, + 127.05, + 116.37, + 115.9, + 118.7, + 114.49, + 119.77, + 112.43, + 113.36, + 126.38, + 122.45, + 121.8, + 119.14, + 122.23, + 114.5, + 113.09, + 90.81, + 91.03, + 93.52, + 103.87, + 92.03, + 81.48, + 88.83, + 79.84, + 78.1, + 82.07, + 87.42, + 112.19, + 130.18, + 130.04, + 101.45, + 108.72, + 128.47, + 123.37, + 104.64, + 87.4, + 106.86, + 119.14, + 106.52, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + { + "name": { + "en": "CO2 Emission Allowances, Auction DE", + "de": "CO2 Emissionszertifikate, Auktion DE", + "fr": "Quotas d'\u00e9mission europ\u00e9ens de CO2, Auction DE", + "it": "Quote di emissioni europee di CO2, Auction DE", + "es": "Derechos de emisi\u00f3n de CO2, Subasta DE" + }, + "unit": "EUR/tCO2", + "color": "rgb(228, 26, 28)", + "type": "line", + "visible": false, + "showInLegend": true, + "allowCsvDownloadForItem": true, + "yAxis": 1, + "data": [ + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 79.76, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85, + 80.85 + ] + }, + { + "name": { + "en": "CO2 Emission Allowances, Auction EU", + "de": "CO2 Emissionszertifikate, Auktion EU", + "fr": "Quotas d'\u00e9mission europ\u00e9ens de CO2, Auction EU", + "it": "Quote di emissioni europee di CO2, Auction EU", + "es": "Derechos de emisi\u00f3n de CO2, Subasta EU" + }, + "unit": "EUR/tCO2", + "color": "rgb(0,0,139)", + "type": "line", + "visible": false, + "showInLegend": true, + "allowCsvDownloadForItem": true, + "yAxis": 1, + "data": [ + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 80.07, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.97, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 79.91, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31, + 81.31 + ] + } +] \ No newline at end of file diff --git a/simulation/src/test/resources/stockprices_DE_LU_week_32-33.csv b/forecast/src/test/resources/stockprices_DE_LU_week_32-33.csv similarity index 100% rename from simulation/src/test/resources/stockprices_DE_LU_week_32-33.csv rename to forecast/src/test/resources/stockprices_DE_LU_week_32-33.csv diff --git a/simulation/src/test/resources/stockprices_DE_LU_week_32_2025.svg b/forecast/src/test/resources/stockprices_DE_LU_week_32_2025.svg similarity index 100% rename from simulation/src/test/resources/stockprices_DE_LU_week_32_2025.svg rename to forecast/src/test/resources/stockprices_DE_LU_week_32_2025.svg diff --git a/simulation/src/test/resources/stockprices_DE_LU_week_33_2025.svg b/forecast/src/test/resources/stockprices_DE_LU_week_33_2025.svg similarity index 100% rename from simulation/src/test/resources/stockprices_DE_LU_week_33_2025.svg rename to forecast/src/test/resources/stockprices_DE_LU_week_33_2025.svg diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForcastDAO.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForcastDAO.java deleted file mode 100644 index 202a993..0000000 --- a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForcastDAO.java +++ /dev/null @@ -1,5 +0,0 @@ -package de.hatoka.eos.persistence.capi; - -public interface WeatherForcastDAO extends Dao -{ -} diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherDataSource.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherDataSource.java similarity index 90% rename from persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherDataSource.java rename to persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherDataSource.java index 3d0bde8..83edcf9 100644 --- a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherDataSource.java +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherDataSource.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.persistence.capi; +package de.hatoka.eos.persistence.capi.weather; /** * Enumeration of weather data sources. diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForcastDAO.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForcastDAO.java new file mode 100644 index 0000000..566353d --- /dev/null +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForcastDAO.java @@ -0,0 +1,7 @@ +package de.hatoka.eos.persistence.capi.weather; + +import de.hatoka.eos.persistence.capi.Dao; + +public interface WeatherForcastDAO extends Dao +{ +} diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastKey.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKey.java similarity index 87% rename from persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastKey.java rename to persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKey.java index efc8fcf..f996080 100644 --- a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastKey.java +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKey.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.persistence.capi; +package de.hatoka.eos.persistence.capi.weather; import java.time.ZonedDateTime; diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastPO.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastPO.java similarity index 89% rename from persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastPO.java rename to persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastPO.java index 1027644..216af3a 100644 --- a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherForecastPO.java +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastPO.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.persistence.capi; +package de.hatoka.eos.persistence.capi.weather; import de.hatoka.eos.units.capi.Percentage; diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherStation.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherStation.java similarity index 94% rename from persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherStation.java rename to persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherStation.java index cd79478..6f1fd9a 100644 --- a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/WeatherStation.java +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherStation.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.persistence.capi; +package de.hatoka.eos.persistence.capi.weather; /** * Weather stations with their MeteoMedia station numbers and geographic coordinates for OpenMeteo API. diff --git a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java index 2050600..fa442bc 100644 --- a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java +++ b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java @@ -8,9 +8,9 @@ import com.influxdb.client.write.Point; import com.influxdb.query.FluxRecord; import com.influxdb.query.FluxTable; -import de.hatoka.eos.persistence.capi.WeatherForcastDAO; -import de.hatoka.eos.persistence.capi.WeatherForecastKey; -import de.hatoka.eos.persistence.capi.WeatherForecastPO; +import de.hatoka.eos.persistence.capi.weather.WeatherForcastDAO; +import de.hatoka.eos.persistence.capi.weather.WeatherForecastKey; +import de.hatoka.eos.persistence.capi.weather.WeatherForecastPO; import de.hatoka.eos.units.capi.Percentage; import jakarta.inject.Inject; import jakarta.inject.Singleton; diff --git a/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java b/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java index 224408b..939ef4d 100644 --- a/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java +++ b/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java @@ -1,9 +1,9 @@ package de.hatoka.eos.persistence.influx; -import de.hatoka.eos.persistence.capi.WeatherStation; -import de.hatoka.eos.persistence.capi.WeatherDataSource; -import de.hatoka.eos.persistence.capi.WeatherForecastKey; -import de.hatoka.eos.persistence.capi.WeatherForecastPO; +import de.hatoka.eos.persistence.capi.weather.WeatherStation; +import de.hatoka.eos.persistence.capi.weather.WeatherDataSource; +import de.hatoka.eos.persistence.capi.weather.WeatherForecastKey; +import de.hatoka.eos.persistence.capi.weather.WeatherForecastPO; import de.hatoka.eos.units.capi.Percentage; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/metrics/ForecastMetricsExporter.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/metrics/ForecastMetricsExporter.java deleted file mode 100644 index c32d56e..0000000 --- a/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/metrics/ForecastMetricsExporter.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.hatoka.eos.simulation.capi.business.metrics; - -import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; -import de.hatoka.eos.simulation.capi.business.forecast.WeatherForecast; - -import java.time.ZonedDateTime; - -/** - * Interface for exporting simulation metrics to time-series databases like InfluxDB. - */ -public interface ForecastMetricsExporter -{ - /** - * Exports forecasts of given time - * - * @param time time of forecast event - * @param energyPriceProvider energy price provider provides price for given time - */ - void export(ZonedDateTime time, EnergyPriceForecast energyPriceProvider); - - /** - * Exports forecasts of given time - * - * @param time time of forecast event - * @param weatherService weather service provides sun probability for given time - */ - void export(ZonedDateTime time, WeatherForecast weatherService); -} \ No newline at end of file diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/CsvStockEnergyPriceProvider.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/CsvStockEnergyPriceProvider.java deleted file mode 100644 index 191292c..0000000 --- a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/CsvStockEnergyPriceProvider.java +++ /dev/null @@ -1,132 +0,0 @@ -package de.hatoka.eos.simulation.internal.business.forecast; - -import de.hatoka.eos.simulation.capi.business.config.CsvPriceConfig; -import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; -import de.hatoka.eos.units.capi.Money; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -public class CsvStockEnergyPriceProvider implements EnergyPriceForecast -{ - private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd"); - - private final Map> pricesByDateAndHour = new ConcurrentHashMap<>(); - private final CsvPriceConfig config; - - public CsvStockEnergyPriceProvider(CsvPriceConfig config) - { - this.config = config; - loadResources(); - } - - private void loadResources() - { - for (String resource : config.resource()) - { - loadCsvResource(resource); - } - } - - private void loadCsvResource(String resourcePath) - { - try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourcePath)) - { - if (inputStream == null) - { - throw new IllegalArgumentException("Resource not found: " + resourcePath); - } - - try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) - { - reader.lines() - .filter(line -> !line.trim().isEmpty() && !line.startsWith("#")) - .map(String::trim) - .forEach(this::parseCsvLine); - } - } - catch (IOException e) - { - throw new RuntimeException("Failed to load CSV resource: " + resourcePath, e); - } - } - - private void parseCsvLine(String line) - { - String[] parts = line.split(","); - if (parts.length < 2) - { - return; // Skip invalid lines - } - - String dateStr = parts[0].trim(); - Map hourlyPrices = new HashMap<>(); - - // Parse hourly prices (parts[1] = hour 0, parts[2] = hour 1, etc.) - for (int hour = 0; hour < Math.min(24, parts.length - 1); hour++) - { - try - { - double price = Double.parseDouble(parts[hour + 1].trim()); - hourlyPrices.put(hour, price); - } - catch (NumberFormatException e) - { - // Skip invalid price values - } - } - - pricesByDateAndHour.put(dateStr, hourlyPrices); - } - - @Override - public Money getImportPrice(ZonedDateTime time) - { - Money marketPrice = getMarketPrice(time); - if (marketPrice == null) - { - throw new IllegalStateException("No price available for time: " + time); - } - return marketPrice.add(config.importCharge().price()); - } - - @Override - public Money getExportPrice(ZonedDateTime time) - { - Money marketPrice = getMarketPrice(time); - if (marketPrice == null) - { - throw new IllegalStateException("No price available for time: " + time); - } - return marketPrice.subtract(config.exportCharge().price()); - } - - /** - * @param time to lookup - * @return price for kWh - */ - private Money getMarketPrice(ZonedDateTime time) - { - String dateKey = time.format(DATE_FORMATTER); - int hour = time.getHour(); - - Map dayPrices = pricesByDateAndHour.get(dateKey); - if (dayPrices == null) - { - return null; - } - Double dayPrice = dayPrices.get(hour); - if (dayPrice == null) - { - return null; - } - return new Money(java.math.BigDecimal.valueOf(dayPrice), config.currency()).divide(1000); - } -} diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/InfluxWeatherForecast.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoWeatherForecast.java similarity index 58% rename from simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/InfluxWeatherForecast.java rename to simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoWeatherForecast.java index 7caa66c..9431ec9 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/InfluxWeatherForecast.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoWeatherForecast.java @@ -1,25 +1,26 @@ package de.hatoka.eos.simulation.internal.business.forecast; +import de.hatoka.eos.persistence.capi.weather.WeatherDataSource; +import de.hatoka.eos.persistence.capi.weather.WeatherForcastDAO; +import de.hatoka.eos.persistence.capi.weather.WeatherForecastKey; import de.hatoka.eos.simulation.capi.business.forecast.WeatherForecast; -import de.hatoka.eos.persistence.capi.WeatherForecastKey; -import de.hatoka.eos.persistence.capi.WeatherDataSource; -import de.hatoka.eos.persistence.influx.InfluxWeatherForecastDao; import de.hatoka.eos.units.capi.Percentage; +import jakarta.inject.Inject; import java.time.ZonedDateTime; /** * Weather forecast implementation that reads from InfluxDB via ForecastDAO. */ -public class InfluxWeatherForecast implements WeatherForecast +public class DaoWeatherForecast implements WeatherForecast { private final String station; - private final InfluxWeatherForecastDao forecastDAO; + @Inject + private WeatherForcastDAO forecastDAO; - public InfluxWeatherForecast(String station, InfluxWeatherForecastDao forecastDAO) + public DaoWeatherForecast(String station) { this.station = station; - this.forecastDAO = forecastDAO; } @Override public Percentage getSunProbability(ZonedDateTime time) diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/simulation/Simulation.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/simulation/Simulation.java index ccd6939..6fbee2b 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/simulation/Simulation.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/simulation/Simulation.java @@ -3,7 +3,6 @@ import de.hatoka.eos.simulation.capi.business.device.Device; import de.hatoka.eos.simulation.capi.business.device.DeviceRef; import de.hatoka.eos.simulation.capi.business.device.DeviceState; -import de.hatoka.eos.simulation.capi.business.metrics.ForecastMetricsExporter; import de.hatoka.eos.simulation.capi.business.metrics.SimulationMetricsExporter; import de.hatoka.eos.simulation.capi.business.simulation.*; import org.slf4j.Logger; @@ -20,15 +19,13 @@ public class Simulation private static final Logger LOGGER = LoggerFactory.getLogger(Simulation.class); private final SimulationRequest request; private final SimulationMetricsExporter simulationMetricsExporter; - private final ForecastMetricsExporter forecastMetricsExporter; private Map currentState; - public Simulation(SimulationRequest request, SimulationMetricsExporter simulationMetricsExporter, ForecastMetricsExporter forecastMetricsExporter) + public Simulation(SimulationRequest request, SimulationMetricsExporter simulationMetricsExporter) { this.request = request; this.simulationMetricsExporter = simulationMetricsExporter; - this.forecastMetricsExporter = forecastMetricsExporter; } public SimulationResult run() @@ -71,7 +68,6 @@ private EnergySystem executeStep(SimulationStep step, EnergySystem system) currentState.put(deviceRef, stepResult.deviceState()); } simulationMetricsExporter.exportMetrics(new SimulationResult(request, step, currentState, updatedSystem.subtract(system))); - forecastMetricsExporter.export(step.startDate(), request.services().priceForecast()); return updatedSystem; } diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/simulation/SimulatorImpl.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/simulation/SimulatorImpl.java index a4cd437..2e9d0b0 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/simulation/SimulatorImpl.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/simulation/SimulatorImpl.java @@ -1,6 +1,5 @@ package de.hatoka.eos.simulation.internal.business.simulation; -import de.hatoka.eos.simulation.capi.business.metrics.ForecastMetricsExporter; import de.hatoka.eos.simulation.capi.business.metrics.SimulationMetricsExporter; import de.hatoka.eos.simulation.capi.business.simulation.SimulationRequest; import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; @@ -13,12 +12,10 @@ public class SimulatorImpl implements Simulator { @Inject private SimulationMetricsExporter metricsExporter; - @Inject - private ForecastMetricsExporter forecastMetricsExporter; @Override public SimulationResult simulate(SimulationRequest request) { - return new Simulation(request, metricsExporter, forecastMetricsExporter).run(); + return new Simulation(request, metricsExporter).run(); } } From 95b0b93b6fa6fdcbdbf53bb5153e0a033d2fef16 Mon Sep 17 00:00:00 2001 From: Thomas Bergmann Date: Sun, 16 Nov 2025 17:23:29 +0100 Subject: [PATCH 09/16] feat: add EnergyCharts download --- forecast/ENERGYCHARTS_IMPLEMENTATION.md | 65 +++++ .../AbstractWeatherForecastImporter.java | 3 +- .../forecast/ForecastImportApplication.java | 19 +- .../energycharts/EnergyChartsImporter.java | 116 +++++++- .../energycharts/EnergyChartsResponse.java | 45 ++++ .../EnergyChartsImporterTest.java | 34 +++ .../EnergyChartsResponseTest.java | 51 ++++ .../PriceForecastExporterTest.java | 54 ---- ...MeteoMediaWeatherForecastImporterTest.java | 5 +- .../OpenMeteoWeatherForecastImporterTest.java | 8 +- .../capi/energystock/EnergyStockDao.java | 7 + .../capi/energystock/EnergyStockKey.java | 11 + .../capi/energystock/EnergyStockPO.java | 19 ++ .../influx/InfluxEnergyStockDao.java | 130 +++++++++ .../influx/InfluxEnergyStockDaoTest.java | 255 ++++++++++++++++++ .../forecast/DaoEnergyPriceForecast.java | 29 ++ .../business/forecast/DaoWeatherForecast.java | 2 + .../metrics/InfluxDBMetricsExporter.java | 20 +- 18 files changed, 773 insertions(+), 100 deletions(-) create mode 100644 forecast/ENERGYCHARTS_IMPLEMENTATION.md create mode 100644 forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsResponse.java create mode 100644 forecast/src/test/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporterTest.java create mode 100644 forecast/src/test/java/de/hatoka/eos/forecast/energycharts/EnergyChartsResponseTest.java delete mode 100644 forecast/src/test/java/de/hatoka/eos/forecast/energycharts/PriceForecastExporterTest.java create mode 100644 persistence-api/src/main/java/de/hatoka/eos/persistence/capi/energystock/EnergyStockDao.java create mode 100644 persistence-api/src/main/java/de/hatoka/eos/persistence/capi/energystock/EnergyStockKey.java create mode 100644 persistence-api/src/main/java/de/hatoka/eos/persistence/capi/energystock/EnergyStockPO.java create mode 100644 persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxEnergyStockDao.java create mode 100644 persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxEnergyStockDaoTest.java create mode 100644 simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoEnergyPriceForecast.java diff --git a/forecast/ENERGYCHARTS_IMPLEMENTATION.md b/forecast/ENERGYCHARTS_IMPLEMENTATION.md new file mode 100644 index 0000000..cc1661e --- /dev/null +++ b/forecast/ENERGYCHARTS_IMPLEMENTATION.md @@ -0,0 +1,65 @@ +# EnergyCharts Importer Implementation + +## Overview +The EnergyChartsImporter has been implemented to parse JSON data from the Energy Charts API and extract the "Day Ahead Auction (DE-LU)" electricity price data. + +## Implementation Details + +### Files Created + +1. **EnergyChartsImporter.java** (Updated) + - Parses JSON response from Energy Charts API + - Extracts "Day Ahead Auction (DE-LU)" data section + - Maps 15-minute interval prices to timestamps + - Calculates the correct week of year and start of week + +2. **EnergyChartsResponse.java** (New) + - DTO for Energy Charts API response items + - Contains fields: name, currency, unit, color, type, visible, showInLegend, allowCsvDownloadForItem, yAxis, data + - Uses Jackson annotations for JSON deserialization + +3. **NameTranslation.java** (New) + - DTO for multi-language name translations + - Contains fields: en, de, fr, it, es (English, German, French, Italian, Spanish) + +4. **EnergyChartsImporterTest.java** (New) + - Tests JSON parsing functionality + - Verifies "Day Ahead Auction (DE-LU)" section extraction + - Validates data integrity and translations + +## Key Features + +- **JSON Parsing**: Uses Jackson ObjectMapper to parse the complex JSON structure +- **Data Extraction**: Finds the specific "Day Ahead Auction (DE-LU)" section by matching the English name +- **Time Mapping**: Maps each price data point to a 15-minute interval timestamp starting from Monday 00:00 +- **Week Calculation**: Properly calculates the ISO week number and start of week + +## JSON Structure + +The API returns an array of data series, each with: +```json +{ + "name": [{"en": "...", "de": "...", ...}], + "currency": "EUR", + "unit": "EUR/MWh", + "data": [price1, price2, ...] +} +``` + +The implementation searches for the entry where `name[0].en == "Day Ahead Auction (DE-LU)"` and extracts its data array. + +## Usage + +```java +EnergyChartsImporter importer = new EnergyChartsImporter(); +ZonedDateTime date = ZonedDateTime.of(2025, 11, 16, 0, 0, 0, 0, ZoneId.of("Europe/Berlin")); +Map prices = importer.downloadAndProcessWeatherData(date); +``` + +This returns a map of timestamps (15-minute intervals) to prices in EUR/MWh. + +## Test Data + +The test uses `energycharts_2025_11_16.json` which contains real data from Energy Charts for week 46 of 2025. +The "Day Ahead Auction (DE-LU)" section starts at line 4882 and contains price data in EUR/MWh. + diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java index d791a93..9506a6c 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java @@ -41,9 +41,8 @@ public abstract class AbstractWeatherForecastImporter * @throws IOException if there's an error downloading or processing the data * @throws InterruptedException if the operation is interrupted */ - public ZonedDateTime importWeatherForecast(WeatherStation station) throws IOException, InterruptedException + public ZonedDateTime importWeatherForecast(WeatherStation station, ZonedDateTime startDate) throws IOException, InterruptedException { - ZonedDateTime startDate = ZonedDateTime.now().toLocalDate().atStartOfDay(ZoneId.of("UTC")); logger.info("Starting weather forecast import for station: {}", station.name()); try diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java b/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java index 55538c2..76a5ec1 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java @@ -1,5 +1,6 @@ package de.hatoka.eos.forecast; +import de.hatoka.eos.forecast.energycharts.EnergyChartsImporter; import de.hatoka.eos.forecast.meteomedia.MeteoMediaWeatherForecastImporter; import de.hatoka.eos.forecast.openmeteo.OpenMeteoWeatherForecastImporter; import de.hatoka.eos.persistence.capi.weather.WeatherStation; @@ -10,10 +11,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.time.ZoneId; +import java.time.ZonedDateTime; + @QuarkusMain public class ForecastImportApplication implements QuarkusApplication { - private static final Logger logger = LoggerFactory.getLogger(ForecastImportApplication.class); + private final Logger logger = LoggerFactory.getLogger(ForecastImportApplication.class); @Inject MeteoMediaWeatherForecastImporter meteoMediaImporter; @@ -21,6 +25,9 @@ public class ForecastImportApplication implements QuarkusApplication @Inject OpenMeteoWeatherForecastImporter openMeteoImporter; + @Inject + EnergyChartsImporter energyChartsImporter; + public static void main(String[] args) { Quarkus.run(ForecastImportApplication.class, args); @@ -32,16 +39,18 @@ public int run(String... args) throws Exception logger.info("Starting weather forecast import..."); try { + ZonedDateTime startDate = ZonedDateTime.now().toLocalDate().atStartOfDay(ZoneId.of("UTC")); + energyChartsImporter.importStockData(startDate); // Import from MeteoMedia (can only import data of today and tomorrow) logger.debug("Importing from MeteoMedia..."); - meteoMediaImporter.importWeatherForecast(WeatherStation.APOLDA); - meteoMediaImporter.importWeatherForecast(WeatherStation.LEIPZIG_STADTWERKE); + meteoMediaImporter.importWeatherForecast(WeatherStation.APOLDA, startDate); + meteoMediaImporter.importWeatherForecast(WeatherStation.LEIPZIG_STADTWERKE, startDate); logger.info("Import from MeteoMedia finished."); // Import from OpenMeteo (can import up to 3 days) logger.debug("Importing from OpenMeteo..."); - openMeteoImporter.importWeatherForecast(WeatherStation.APOLDA); - openMeteoImporter.importWeatherForecast(WeatherStation.LEIPZIG_STADTWERKE); + openMeteoImporter.importWeatherForecast(WeatherStation.APOLDA, startDate); + openMeteoImporter.importWeatherForecast(WeatherStation.LEIPZIG_STADTWERKE, startDate); logger.info("Import from OpenMeteo finished."); return 0; diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java index 6e4d42f..0b254d0 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java @@ -1,5 +1,13 @@ package de.hatoka.eos.forecast.energycharts; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.hatoka.eos.persistence.capi.energystock.EnergyStockDao; +import de.hatoka.eos.persistence.capi.energystock.EnergyStockKey; +import de.hatoka.eos.persistence.capi.energystock.EnergyStockPO; +import de.hatoka.eos.units.capi.Money; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -8,24 +16,63 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.DayOfWeek; import java.time.Duration; import java.time.ZonedDateTime; +import java.time.temporal.WeekFields; import java.util.HashMap; +import java.util.List; import java.util.Map; +@Singleton public class EnergyChartsImporter { - private static final String URI_FORMAT = "https://energy-charts.info/charts/price_spot_market/data/de/week_15min_%s_%s.json"; // day format parameter week, year + private static final String URI_FORMAT = "https://energy-charts.info/charts/price_spot_market/data/de/week_15min_%s_%s.json"; // format parameters: year, week + private static final String DAY_AHEAD_AUCTION_NAME_EN = "Day Ahead Auction (DE-LU)"; private static final Logger logger = LoggerFactory.getLogger(EnergyChartsImporter.class); + private final ObjectMapper objectMapper = new ObjectMapper(); - protected Map downloadAndProcessWeatherData(ZonedDateTime startDate) + @Inject + private EnergyStockDao stockDao; + + public void importStockData(ZonedDateTime startDate) throws IOException, InterruptedException { - Double[] imageData = downloadData(URI.create(URI_FORMAT.formatted(startDate.getYear(), getWeekOfYear(startDate)))); - // for each 15min while imageData is available + List responses = downloadData(URI.create(URI_FORMAT.formatted(startDate.getYear(), getWeekOfYear(startDate)))); + EnergyChartsResponse dayAheadAuction = findDayAheadAuction(responses); + Map dayAheadPrices = mapDataToTime(startDate, dayAheadAuction.getData()); + String currency = dayAheadAuction.getCurrency(); + Map dayAheadPricesAsMoney = dayAheadPrices.entrySet().stream() + .collect(java.util.stream.Collectors.toMap( + Map.Entry::getKey, + entry -> new Money(java.math.BigDecimal.valueOf(entry.getValue()), currency) + )); + storePrices(dayAheadPricesAsMoney); + } + + private void storePrices(Map dayAheadPrices) + { + for (Map.Entry dayAheadPrice : dayAheadPrices.entrySet()) + { + EnergyStockKey key = new EnergyStockKey(dayAheadPrice.getKey()); + EnergyStockPO existingData = stockDao.get(key); + if (existingData == null) + { + existingData = new EnergyStockPO(); + } + existingData.setDayAheadPrice(dayAheadPrice.getValue()); + stockDao.update(key, existingData); + } + } + + private Map mapDataToTime(ZonedDateTime startDate, List priceData) + { + // Calculate the start of the week (Monday 00:00) ZonedDateTime dateOfPrice = getStartOfWeek(startDate); Map result = new HashMap<>(); - for(Double price : imageData) + + // Map each 15-minute interval to its price + for (Double price : priceData) { result.put(dateOfPrice, price); dateOfPrice = dateOfPrice.plusMinutes(15); @@ -33,18 +80,24 @@ protected Map downloadAndProcessWeatherData(ZonedDateTime return result; } - private ZonedDateTime getStartOfWeek(ZonedDateTime startDate) + private ZonedDateTime getStartOfWeek(ZonedDateTime date) { - // 2025/11/10 for 2025/11/16 - return startDate; + // Get Monday of the week at 00:00 + return date.with(DayOfWeek.MONDAY) + .withHour(0) + .withMinute(0) + .withSecond(0) + .withNano(0); } - private Integer getWeekOfYear(ZonedDateTime startDate) + private Integer getWeekOfYear(ZonedDateTime date) { - return 46; + // Use ISO week fields where Monday is the first day of the week (German standard) + WeekFields weekFields = WeekFields.ISO; + return date.get(weekFields.weekOfWeekBasedYear()); } - private Double[] downloadData(URI dataUrl) throws IOException, InterruptedException + private List downloadData(URI dataUrl) throws IOException, InterruptedException { try (HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(30)) @@ -61,13 +114,50 @@ private Double[] downloadData(URI dataUrl) throws IOException, InterruptedExcept logger.debug("Sending HTTP request to: {}", dataUrl); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { throw new IOException("HTTP request failed with status code: " + response.statusCode()); } - return response.body(); + return objectMapper.readValue( + response.body(), + new TypeReference<>() {} + ); + } + } + + /** + * Find the "Day Ahead Auction (DE-LU)" section + * @param responses response from Url or resource + * @return day ahead auction + */ + static EnergyChartsResponse findDayAheadAuction(List responses) + { + for (EnergyChartsResponse response : responses) + { + if (response.getName() != null) + { + Object nameAttribute = response.getName(); + if (nameAttribute instanceof List listAttribute) + { + Object firstNameAttribute = listAttribute.getFirst(); + if (firstNameAttribute instanceof Map mapName) + { + for(Map.Entry entry : mapName.entrySet()) + { + if (entry.getKey() instanceof String language && entry.getValue() instanceof String name) + { + if ("en".equals(language) && DAY_AHEAD_AUCTION_NAME_EN.equals(name)) + { + return response; + } + } + } + } + } + } } + throw new RuntimeException("Can't find " + DAY_AHEAD_AUCTION_NAME_EN); } } diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsResponse.java b/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsResponse.java new file mode 100644 index 0000000..4821874 --- /dev/null +++ b/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsResponse.java @@ -0,0 +1,45 @@ +package de.hatoka.eos.forecast.energycharts; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.List; + +/** + * Data Transfer Object for Energy Charts API response. + * Represents the array of data series from the Energy Charts API. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class EnergyChartsResponse +{ + @JsonProperty("name") + private Object name; + + @JsonProperty("currency") + private String currency; + + @JsonProperty("data") + private List data; + + public List getData() + { + return data; + } + + public void setData(List data) + { + this.data = data; + } + + public String getCurrency() + { + return currency; + } + + public Object getName() + { + return name; + } +} + diff --git a/forecast/src/test/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporterTest.java new file mode 100644 index 0000000..60b4c08 --- /dev/null +++ b/forecast/src/test/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporterTest.java @@ -0,0 +1,34 @@ +package de.hatoka.eos.forecast.energycharts; + +import de.hatoka.eos.persistence.capi.energystock.EnergyStockDao; +import de.hatoka.eos.persistence.capi.energystock.EnergyStockKey; +import de.hatoka.eos.persistence.capi.energystock.EnergyStockPO; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@QuarkusTest +public class EnergyChartsImporterTest +{ + @Inject + private EnergyChartsImporter importer; + @Inject + private EnergyStockDao dao; + + @Test + void testImportFromValidUrl() throws IOException, InterruptedException + { + // this importer can only import data of today and tomorrow + ZonedDateTime startDate = ZonedDateTime.now().toLocalDate().atStartOfDay(ZoneId.of("Europe/Berlin")); + importer.importStockData(startDate); + EnergyStockPO retrieved = dao.get(EnergyStockKey.valueOf(startDate.plusHours(1))); + assertNotNull(retrieved); + assertNotNull(retrieved.getDayAheadPrice()); + } +} diff --git a/forecast/src/test/java/de/hatoka/eos/forecast/energycharts/EnergyChartsResponseTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/energycharts/EnergyChartsResponseTest.java new file mode 100644 index 0000000..8aecb0a --- /dev/null +++ b/forecast/src/test/java/de/hatoka/eos/forecast/energycharts/EnergyChartsResponseTest.java @@ -0,0 +1,51 @@ +package de.hatoka.eos.forecast.energycharts; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class EnergyChartsResponseTest +{ + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void testParseEnergyChartsResponse() throws Exception + { + // Load the test JSON file + InputStream inputStream = getClass().getResourceAsStream("/energycharts_2025_11_16.json"); + assertNotNull(inputStream, "Test resource file not found"); + + String jsonContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + + // Parse JSON array + List responses = objectMapper.readValue( + jsonContent, + new TypeReference<>() {} + ); + + assertNotNull(responses); + assertFalse(responses.isEmpty()); + + EnergyChartsResponse dayAheadAuction = EnergyChartsImporter.findDayAheadAuction(responses); + assertNotNull(dayAheadAuction, "Day Ahead Auction (DE-LU) section not found"); + + // Verify the data + assertEquals("EUR", dayAheadAuction.getCurrency()); + assertNotNull(dayAheadAuction.getData()); + assertFalse(dayAheadAuction.getData().isEmpty()); + + // Check first few data points match expected values + List data = dayAheadAuction.getData(); + assertFalse(data.isEmpty()); + assertEquals(92.38, data.get(0), 0.001); + assertEquals(89.98, data.get(1), 0.001); + assertEquals(87.4, data.get(2), 0.001); + } +} + diff --git a/forecast/src/test/java/de/hatoka/eos/forecast/energycharts/PriceForecastExporterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/energycharts/PriceForecastExporterTest.java deleted file mode 100644 index 10ef4cf..0000000 --- a/forecast/src/test/java/de/hatoka/eos/forecast/energycharts/PriceForecastExporterTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package de.hatoka.eos.forecast.energycharts; - -import de.hatoka.eos.simulation.capi.business.config.InstallationConfig; -import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; -import de.hatoka.eos.simulation.internal.business.DateTooling; -import de.hatoka.eos.simulation.internal.business.config.ConfigurationLoader; -import io.quarkus.test.junit.QuarkusTest; -import jakarta.inject.Inject; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.time.Duration; -import java.time.ZonedDateTime; - -@QuarkusTest -public class PriceForecastExporterTest -{ - private static final Logger LOGGER = LoggerFactory.getLogger(PriceForecastExporterTest.class); - - @Inject - private ConfigurationLoader configurationLoader; - - @Test - public void testExportEnergyPrice() throws IOException - { - // Arrange - Load test configuration without electric car for cleaner curves - InstallationConfig installationConfig = configurationLoader.load("test-installation-for-grafana.yaml"); - EnergyPriceForecast priceProvider = installationConfig.getGrid().getEnergyPriceProvider(); - // Simulate from August, because of retrieved daily prices (15min interval) - ZonedDateTime startDate = DateTooling.createBerlinDate("2025/08/09"); - ZonedDateTime endDate = startDate.plusDays(3); - Duration stepDuration = Duration.ofMinutes(15); - - ZonedDateTime currentDate = startDate; - while(currentDate.isBefore(endDate)) - { - metricsExporter.export(currentDate, priceProvider); - currentDate = currentDate.plus(stepDuration); - } - - // Export to InfluxDB/Grafana - if (metricsExporter instanceof InfluxDBMetricsExporter influxDBMetricsExporter && !influxDBMetricsExporter.isAvailable()) - { - LOGGER.warn("⚠️ InfluxDB exporter not available. Start with: docker-compose up -d"); - LOGGER.info("💡 To enable metrics export:"); - LOGGER.info(" 1. Run: docker-compose up -d"); - LOGGER.info(" 2. Wait for services to start"); - LOGGER.info(" 3. Re-run this test"); - LOGGER.info(" 4. Open Grafana: http://localhost:3000 (admin/admin123)"); - } - } -} diff --git a/forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherForecastImporterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherForecastImporterTest.java index 0985488..d052883 100644 --- a/forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherForecastImporterTest.java +++ b/forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherForecastImporterTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.Test; import java.io.IOException; +import java.time.ZoneId; import java.time.ZonedDateTime; import static org.junit.jupiter.api.Assertions.*; @@ -32,10 +33,10 @@ class MeteoMediaWeatherForecastImporterTest void testImportFromValidUrl() throws IOException, InterruptedException { // this importer can only import data of today and tomorrow - ZonedDateTime testDate = importer.importWeatherForecast(WeatherStation.APOLDA); + ZonedDateTime startDate = ZonedDateTime.now().toLocalDate().atStartOfDay(ZoneId.of("UTC")); + ZonedDateTime testDate = importer.importWeatherForecast(WeatherStation.APOLDA, startDate); WeatherForecastPO retrieved = weatherDao.get(WeatherForecastKey.valueOf(WeatherStation.APOLDA, testDate.plusHours(1), WeatherDataSource.METEOMEDIA)); assertNotNull(retrieved); assertNotNull(retrieved.getSunProbability()); - importer.importWeatherForecast(WeatherStation.LEIPZIG_STADTWERKE); } } diff --git a/forecast/src/test/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoWeatherForecastImporterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoWeatherForecastImporterTest.java index 5956f58..2004740 100644 --- a/forecast/src/test/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoWeatherForecastImporterTest.java +++ b/forecast/src/test/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoWeatherForecastImporterTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.Test; import java.io.IOException; +import java.time.ZoneId; import java.time.ZonedDateTime; import static org.junit.jupiter.api.Assertions.*; @@ -32,7 +33,8 @@ class OpenMeteoWeatherForecastImporterTest void testImportFromOpenMeteoApi() throws IOException, InterruptedException { // Test importing data from OpenMeteo API - ZonedDateTime testDate = importer.importWeatherForecast(WeatherStation.APOLDA); + ZonedDateTime startDate = ZonedDateTime.now().toLocalDate().atStartOfDay(ZoneId.of("UTC")); + ZonedDateTime testDate = importer.importWeatherForecast(WeatherStation.APOLDA, startDate); // Verify that data was imported successfully assertNotNull(testDate, "Import should return a valid start date"); @@ -46,10 +48,6 @@ void testImportFromOpenMeteoApi() throws IOException, InterruptedException double sunProb = retrieved.getSunProbability().value(); assertTrue(sunProb >= 0.0 && sunProb <= 1.0, "Sun probability should be between 0.0 and 1.0, but was: " + sunProb); - - // Test importing for a different station - ZonedDateTime testDate2 = importer.importWeatherForecast(WeatherStation.LEIPZIG_STADTWERKE); - assertNotNull(testDate2, "Import for Leipzig should also return a valid start date"); } @Test diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/energystock/EnergyStockDao.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/energystock/EnergyStockDao.java new file mode 100644 index 0000000..c9d4388 --- /dev/null +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/energystock/EnergyStockDao.java @@ -0,0 +1,7 @@ +package de.hatoka.eos.persistence.capi.energystock; + +import de.hatoka.eos.persistence.capi.Dao; + +public interface EnergyStockDao extends Dao +{ +} diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/energystock/EnergyStockKey.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/energystock/EnergyStockKey.java new file mode 100644 index 0000000..0748151 --- /dev/null +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/energystock/EnergyStockKey.java @@ -0,0 +1,11 @@ +package de.hatoka.eos.persistence.capi.energystock; + +import java.time.ZonedDateTime; + +public record EnergyStockKey( ZonedDateTime time) +{ + public static EnergyStockKey valueOf(ZonedDateTime time) + { + return new EnergyStockKey(time); + } +} diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/energystock/EnergyStockPO.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/energystock/EnergyStockPO.java new file mode 100644 index 0000000..cf60afa --- /dev/null +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/energystock/EnergyStockPO.java @@ -0,0 +1,19 @@ +package de.hatoka.eos.persistence.capi.energystock; + +import de.hatoka.eos.units.capi.Money; + +public class EnergyStockPO +{ + public static final String COLUMN_DAY_AHEAD = "dayaheadprice"; + private Money dayAheadPrice; + + public Money getDayAheadPrice() + { + return dayAheadPrice; + } + + public void setDayAheadPrice(Money dayAheadPrice) + { + this.dayAheadPrice = dayAheadPrice; + } +} diff --git a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxEnergyStockDao.java b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxEnergyStockDao.java new file mode 100644 index 0000000..f443c27 --- /dev/null +++ b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxEnergyStockDao.java @@ -0,0 +1,130 @@ +package de.hatoka.eos.persistence.influx; + +import com.influxdb.client.DeleteApi; +import com.influxdb.client.InfluxDBClient; +import com.influxdb.client.QueryApi; +import com.influxdb.client.WriteApiBlocking; +import com.influxdb.client.domain.WritePrecision; +import com.influxdb.client.write.Point; +import com.influxdb.query.FluxRecord; +import com.influxdb.query.FluxTable; +import de.hatoka.eos.persistence.capi.energystock.EnergyStockDao; +import de.hatoka.eos.persistence.capi.energystock.EnergyStockKey; +import de.hatoka.eos.persistence.capi.energystock.EnergyStockPO; +import de.hatoka.eos.persistence.capi.weather.WeatherForecastPO; +import de.hatoka.eos.units.capi.Money; +import de.hatoka.eos.units.capi.Percentage; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * InfluxDB implementation for writing forecast data. + */ +@Singleton +public class InfluxEnergyStockDao implements EnergyStockDao +{ + private static final String MEASUREMENT = "energystock_forecast"; + public static final String GET_QUERY = """ + from(bucket: "%s") + |> range(start: %s, stop: %s) + |> filter(fn: (r) => r["_measurement"] == "%s") + |> filter(fn: (r) => r["_field"] == "%s") + |> last() + """; + public static final String DELETE_PREDICATE = """ + _measurement="%s" + """; + + private final WriteApiBlocking writeApi; + private final DeleteApi deleteApi; + private final QueryApi queryApi; + private final String bucketName; + private final String influxdbOrg; + + @Inject + InfluxEnergyStockDao(InfluxDBClient influxDBClient, InfluxDBConfig config) + { + writeApi = influxDBClient.getWriteApiBlocking(); + deleteApi = influxDBClient.getDeleteApi(); + queryApi = influxDBClient.getQueryApi(); + bucketName = config.influxdbBucket; + influxdbOrg = config.influxdbOrg; + } + + @Override + public void update(EnergyStockKey key, EnergyStockPO data) + { + try + { + Point point = Point.measurement(MEASUREMENT) + .time(key.time().toInstant(), WritePrecision.S) + .addTag("currency", data.getDayAheadPrice().currencyMnemonic()) + .addField(EnergyStockPO.COLUMN_DAY_AHEAD, data.getDayAheadPrice().amount().doubleValue()); + + writeApi.writePoint(point); + } + catch(Exception e) + { + throw new RuntimeException("Can't write weather forecast to influx", e); + } + } + + @Override + public void delete(EnergyStockKey key) + { + try + { + // Delete data within a 1-minute window around the specified time + deleteApi.delete(key.time().minusMinutes(1).toOffsetDateTime(), key.time().plusMinutes(1).toOffsetDateTime(), + DELETE_PREDICATE.formatted(MEASUREMENT), bucketName, influxdbOrg); + } + catch(Exception e) + { + throw new RuntimeException("Can't delete weather forecast from influx", e); + } + } + + private String formatTimeForFlux(ZonedDateTime time) + { + return time.format(DateTimeFormatter.ISO_INSTANT); + } + + @Override + public EnergyStockPO get(EnergyStockKey key) + { + String flux = String.format(GET_QUERY, bucketName, formatTimeForFlux(key.time().minusMinutes(1)), + formatTimeForFlux(key.time().plusMinutes(1)), MEASUREMENT, EnergyStockPO.COLUMN_DAY_AHEAD); + for (FluxTable table : queryApi.query(flux)) + { + for (FluxRecord record : table.getRecords()) + { + EnergyStockPO data = convert(record); + if (data != null) + { + return data; + } + } + } + return null; + } + + private EnergyStockPO convert(FluxRecord record) + { + EnergyStockPO data = new EnergyStockPO(); + Object value = record.getValueByKey("_value"); + Object currencyValue= record.getValueByKey("currency"); + if (value instanceof Number aNumber && currencyValue instanceof String currency) + { + data.setDayAheadPrice(new Money(BigDecimal.valueOf(aNumber.doubleValue()), currency)); + } + else + { + return null; + } + return data; + } +} diff --git a/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxEnergyStockDaoTest.java b/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxEnergyStockDaoTest.java new file mode 100644 index 0000000..5d5ead3 --- /dev/null +++ b/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxEnergyStockDaoTest.java @@ -0,0 +1,255 @@ +package de.hatoka.eos.persistence.influx; + +import de.hatoka.eos.persistence.capi.energystock.EnergyStockKey; +import de.hatoka.eos.persistence.capi.energystock.EnergyStockPO; +import de.hatoka.eos.units.capi.Money; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration test for InfluxEnergyStockDao. + * + * This test requires InfluxDB to be running (e.g., via docker-compose). + * The test uses a dedicated test bucket to avoid interfering with production data. + */ +@QuarkusTest +class InfluxEnergyStockDaoTest +{ + @Inject + InfluxEnergyStockDao dao; + + // Test data constants + private static final ZonedDateTime TEST_TIME = ZonedDateTime.of(2024, 1, 15, 12, 0, 0, 0, ZoneId.of("UTC")); + private static final Money TEST_PRICE = Money.ofEur(50.0); // 50 EUR + private static final Money UPDATED_PRICE = Money.ofEur(75.0); // 75 EUR + + @AfterEach + void tearDown() + { + dao.delete(getKey(TEST_TIME)); + dao.delete(getKey(TEST_TIME.plusHours(1))); + dao.delete(getKey(TEST_TIME.minusHours(1))); + } + + private static EnergyStockPO createEnergyStock(Money dayAheadPrice) + { + EnergyStockPO energyStock = new EnergyStockPO(); + energyStock.setDayAheadPrice(dayAheadPrice); + return energyStock; + } + + private static EnergyStockKey getKey(ZonedDateTime time) + { + return EnergyStockKey.valueOf(time); + } + + @Test + void shouldStoreAndRetrieveEnergyStockData() + { + // Given + EnergyStockPO energyStock = createEnergyStock(TEST_PRICE); + + // When + dao.update(getKey(TEST_TIME), energyStock); + + // Then + EnergyStockPO retrievedStock = dao.get(getKey(TEST_TIME)); + + assertNotNull(retrievedStock, "Retrieved energy stock should not be null"); + assertNotNull(retrievedStock.getDayAheadPrice(), "Day ahead price should not be null"); + assertEquals(TEST_PRICE.amount().doubleValue(), retrievedStock.getDayAheadPrice().amount().doubleValue(), 0.001, + "Day ahead price amount should match the stored value"); + assertEquals(TEST_PRICE.currencyMnemonic(), retrievedStock.getDayAheadPrice().currencyMnemonic(), + "Currency should match the stored value"); + } + + @Test + void shouldUpdateExistingEnergyStockData() + { + // Given - store initial data + EnergyStockPO initialStock = createEnergyStock(TEST_PRICE); + dao.update(getKey(TEST_TIME), initialStock); + + // When - update with new data + EnergyStockPO updatedStock = createEnergyStock(UPDATED_PRICE); + dao.update(getKey(TEST_TIME), updatedStock); + + // Then + EnergyStockPO retrievedStock = dao.get(getKey(TEST_TIME)); + + assertNotNull(retrievedStock, "Retrieved energy stock should not be null"); + assertNotNull(retrievedStock.getDayAheadPrice(), "Day ahead price should not be null"); + assertEquals(UPDATED_PRICE.amount().doubleValue(), retrievedStock.getDayAheadPrice().amount().doubleValue(), 0.001, + "Day ahead price should reflect the updated value"); + } + + @Test + void shouldReturnNullWhenNoDataExists() + { + // Given - a time point with no data + ZonedDateTime nonExistentTime = TEST_TIME.minusDays(365); + + // When/Then + assertNull(dao.get(getKey(nonExistentTime)), "Retrieved energy stock should be null for non-existent data"); + } + + @Test + void shouldHandleMultipleTimePoints() + { + // Given - multiple energy stocks at different times + ZonedDateTime time1 = TEST_TIME; + ZonedDateTime time2 = TEST_TIME.plusHours(1); + ZonedDateTime time3 = TEST_TIME.plusHours(2); + + // When + dao.update(getKey(time1), createEnergyStock(Money.ofEur(30.0))); + dao.update(getKey(time2), createEnergyStock(Money.ofEur(45.0))); + dao.update(getKey(time3), createEnergyStock(Money.ofEur(60.0))); + + // Then + assertEquals(30.0, dao.get(getKey(time1)).getDayAheadPrice().amount().doubleValue(), 0.001); + assertEquals(45.0, dao.get(getKey(time2)).getDayAheadPrice().amount().doubleValue(), 0.001); + assertEquals(60.0, dao.get(getKey(time3)).getDayAheadPrice().amount().doubleValue(), 0.001); + + // Clean up additional test data + dao.delete(getKey(time2)); + dao.delete(getKey(time3)); + } + + @Test + void shouldHandlePreciseTimeRetrieval() + { + // Given - energy stock with precise timestamp + ZonedDateTime preciseTime = TEST_TIME.truncatedTo(ChronoUnit.SECONDS); + EnergyStockPO energyStock = createEnergyStock(TEST_PRICE); + + // When + dao.update(getKey(preciseTime), energyStock); + + // Then - should retrieve data even with slight time variations + EnergyStockPO retrieved = dao.get(getKey(preciseTime)); + + assertNotNull(retrieved.getDayAheadPrice()); + assertEquals(TEST_PRICE.amount().doubleValue(), retrieved.getDayAheadPrice().amount().doubleValue(), 0.001); + } + + @Test + void shouldHandleBoundaryPriceValues() + { + // Test with zero price + EnergyStockPO stock0 = createEnergyStock(Money.ZERO); + dao.update(getKey(TEST_TIME), stock0); + + // Test with high price + ZonedDateTime timeHigh = TEST_TIME.plusMinutes(5); + EnergyStockPO stockHigh = createEnergyStock(new Money(BigDecimal.valueOf(1000.0), "EUR")); + dao.update(getKey(timeHigh), stockHigh); + + // Verify boundary values + assertEquals(0.0, dao.get(getKey(TEST_TIME)).getDayAheadPrice().amount().doubleValue(), 0.001); + assertEquals(1000.0, dao.get(getKey(timeHigh)).getDayAheadPrice().amount().doubleValue(), 0.001); + + // Clean up + dao.delete(getKey(timeHigh)); + } + + @Test + void shouldHandleDifferentCurrencies() + { + // Test with USD currency + ZonedDateTime timeUsd = TEST_TIME.plusMinutes(10); + EnergyStockPO stockUsd = createEnergyStock(new Money(BigDecimal.valueOf(60.0), "USD")); + dao.update(getKey(timeUsd), stockUsd); + + // Verify currency is preserved + EnergyStockPO retrieved = dao.get(getKey(timeUsd)); + assertNotNull(retrieved); + assertEquals("USD", retrieved.getDayAheadPrice().currencyMnemonic()); + assertEquals(60.0, retrieved.getDayAheadPrice().amount().doubleValue(), 0.001); + + // Clean up + dao.delete(getKey(timeUsd)); + } + + @Test + void shouldDeleteEnergyStockData() + { + // Given - store some data first + EnergyStockPO energyStock = createEnergyStock(TEST_PRICE); + dao.update(getKey(TEST_TIME), energyStock); + + // Verify data exists + EnergyStockPO retrievedBeforeDelete = dao.get(getKey(TEST_TIME)); + assertNotNull(retrievedBeforeDelete.getDayAheadPrice(), + "Data should exist before deletion"); + + // When - delete the data + dao.delete(getKey(TEST_TIME)); + + // Then - data should no longer exist + assertNull(dao.get(getKey(TEST_TIME)), "Data should not exist after deletion"); + } + + @Test + void shouldDeleteOnlySpecificTimeData() + { + // Given - store data at multiple time points + ZonedDateTime time1 = TEST_TIME; + ZonedDateTime time2 = TEST_TIME.plusHours(1); + ZonedDateTime time3 = TEST_TIME.plusHours(2); + + dao.update(getKey(time1), createEnergyStock(Money.ofEur(20.0))); + dao.update(getKey(time2), createEnergyStock(Money.ofEur(40.0))); + dao.update(getKey(time3), createEnergyStock(Money.ofEur(80.0))); + + // When - delete only the middle data point + dao.delete(getKey(time2)); + + // Then - only the deleted data should be gone + assertEquals(20.0, dao.get(getKey(time1)).getDayAheadPrice().amount().doubleValue(), 0.001); + assertNull(dao.get(getKey(time2)), "Second data point should be deleted"); + assertEquals(80.0, dao.get(getKey(time3)).getDayAheadPrice().amount().doubleValue(), 0.001); + + // Clean up remaining data + dao.delete(getKey(time1)); + dao.delete(getKey(time3)); + } + + @Test + void shouldHandleDeleteOfNonExistentData() + { + // Given - a time point with no data + ZonedDateTime nonExistentTime = TEST_TIME.plusDays(100); + + // When/Then - deleting non-existent data should not throw an exception + assertDoesNotThrow(() -> dao.delete(getKey(nonExistentTime)), + "Deleting non-existent data should not throw an exception"); + } + + @Test + void shouldHandleNegativePrices() + { + // Test with negative price (can occur in energy markets) + ZonedDateTime timeNegative = TEST_TIME.plusMinutes(15); + EnergyStockPO stockNegative = createEnergyStock(new Money(BigDecimal.valueOf(-25.5), "EUR")); + dao.update(getKey(timeNegative), stockNegative); + + // Verify negative value is preserved + EnergyStockPO retrieved = dao.get(getKey(timeNegative)); + assertNotNull(retrieved); + assertEquals(-25.5, retrieved.getDayAheadPrice().amount().doubleValue(), 0.001); + + // Clean up + dao.delete(getKey(timeNegative)); + } +} + diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoEnergyPriceForecast.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoEnergyPriceForecast.java new file mode 100644 index 0000000..8b68aa1 --- /dev/null +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoEnergyPriceForecast.java @@ -0,0 +1,29 @@ +package de.hatoka.eos.simulation.internal.business.forecast; + +import de.hatoka.eos.persistence.capi.energystock.EnergyStockDao; +import de.hatoka.eos.persistence.capi.energystock.EnergyStockKey; +import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; +import de.hatoka.eos.units.capi.Money; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import java.time.ZonedDateTime; + +@Singleton +public class DaoEnergyPriceForecast implements EnergyPriceForecast +{ + @Inject + private EnergyStockDao stockDao; + + @Override + public Money getImportPrice(ZonedDateTime time) + { + return stockDao.get(new EnergyStockKey(time)).getDayAheadPrice(); + } + + @Override + public Money getExportPrice(ZonedDateTime time) + { + return stockDao.get(new EnergyStockKey(time)).getDayAheadPrice(); + } +} diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoWeatherForecast.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoWeatherForecast.java index 9431ec9..29b9157 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoWeatherForecast.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoWeatherForecast.java @@ -6,12 +6,14 @@ import de.hatoka.eos.simulation.capi.business.forecast.WeatherForecast; import de.hatoka.eos.units.capi.Percentage; import jakarta.inject.Inject; +import jakarta.inject.Singleton; import java.time.ZonedDateTime; /** * Weather forecast implementation that reads from InfluxDB via ForecastDAO. */ +@Singleton public class DaoWeatherForecast implements WeatherForecast { private final String station; diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/metrics/InfluxDBMetricsExporter.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/metrics/InfluxDBMetricsExporter.java index c60b332..0990614 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/metrics/InfluxDBMetricsExporter.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/metrics/InfluxDBMetricsExporter.java @@ -12,7 +12,6 @@ import de.hatoka.eos.simulation.capi.business.device.DeviceType; import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; import de.hatoka.eos.simulation.capi.business.forecast.WeatherForecast; -import de.hatoka.eos.simulation.capi.business.metrics.ForecastMetricsExporter; import de.hatoka.eos.simulation.capi.business.metrics.SimulationMetricsExporter; import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; import jakarta.annotation.PostConstruct; @@ -27,7 +26,7 @@ import java.util.Map; @Singleton -public class InfluxDBMetricsExporter implements SimulationMetricsExporter, ForecastMetricsExporter +public class InfluxDBMetricsExporter implements SimulationMetricsExporter { private static final Logger LOGGER = LoggerFactory.getLogger(InfluxDBMetricsExporter.class); public static final String ENERGY_FLOW = "energy_flow"; @@ -210,21 +209,4 @@ private void writeBattery(String simulationId, DeviceRef deviceRef, DeviceState .addField("capacity_kwh", state.maxEnergy().amount()) .time(timestamp, WritePrecision.NS)); } - - @Override - public void export(ZonedDateTime time, EnergyPriceForecast energyPriceProvider) - { - writeApi.writePoint(Point.measurement("forecast_price") - .addField("import_price_eur", energyPriceProvider.getImportPrice(time).amount()) - .addField("export_price_eur", energyPriceProvider.getExportPrice(time).amount()) - .time(time.toInstant(), WritePrecision.NS)); - } - - @Override - public void export(ZonedDateTime time, WeatherForecast weatherService) - { - writeApi.writePoint(Point.measurement("forecast_weather") - .addField("sun_probability_percentage", weatherService.getSunProbability(time).value()) - .time(time.toInstant(), WritePrecision.NS)); - } } \ No newline at end of file From 349d2b476fe5b97dfc232e5d19127f44e01a31cb Mon Sep 17 00:00:00 2001 From: Thomas Bergmann Date: Sun, 16 Nov 2025 17:39:08 +0100 Subject: [PATCH 10/16] rework: use DAO instead of CSV --- ...iceConfig.java => DynamicPriceConfig.java} | 9 +- .../capi/business/config/GridConfig.java | 23 +---- .../business/config/GridPriceConfigType.java | 2 +- .../forecast/DaoEnergyPriceForecast.java | 4 + .../forecast/EnergyPriceForecastFactory.java | 32 +++++++ .../business/config/CsvPriceConfigTest.java | 86 ------------------- .../SimulationMetricsExporterTest.java | 3 +- 7 files changed, 44 insertions(+), 115 deletions(-) rename simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/{CsvPriceConfig.java => DynamicPriceConfig.java} (77%) create mode 100644 simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/EnergyPriceForecastFactory.java delete mode 100644 simulation/src/test/java/de/hatoka/eos/simulation/internal/business/config/CsvPriceConfigTest.java diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/CsvPriceConfig.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/DynamicPriceConfig.java similarity index 77% rename from simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/CsvPriceConfig.java rename to simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/DynamicPriceConfig.java index a9c1d27..5d1af50 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/CsvPriceConfig.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/DynamicPriceConfig.java @@ -10,9 +10,8 @@ * @param currency currency of stock price * @param exportCharge additional charge for exporting energy to the grid * @param importCharge additional charge for importing energy from grid - * @param resource list of CSV resources */ -public record CsvPriceConfig( +public record DynamicPriceConfig( @JsonProperty("currency") @JsonPropertyDescription("Currency code for CSV price data (e.g., EUR, USD)") String currency, @@ -23,9 +22,5 @@ public record CsvPriceConfig( @JsonProperty("importCharge") @JsonPropertyDescription("Import charge configuration (price per energy unit)") - ImportExportChargeConfig importCharge, - - @JsonProperty("resource") - @JsonPropertyDescription("List of CSV resource files containing price data") - List resource + ImportExportChargeConfig importCharge ) { } \ No newline at end of file diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridConfig.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridConfig.java index 1952443..9e23ac2 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridConfig.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridConfig.java @@ -1,16 +1,11 @@ package de.hatoka.eos.simulation.capi.business.config; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; -import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; -import de.hatoka.eos.simulation.internal.business.forecast.FlatPriceService; -import de.hatoka.eos.simulation.internal.business.forecast.CsvStockEnergyPriceProvider; /** * The GridConfig is the container for grid pricing configuration. The type can be CSV or FLAT and selects the related configuration. * @param type CSV or FLAT - * @param csvPriceProvider stock price configuration * @param flatPriceConfig flat price configuration */ public record GridConfig ( @@ -18,25 +13,13 @@ public record GridConfig ( @JsonPropertyDescription("select csvPriceProvider or flatPriceProvider") GridPriceConfigType type, - @JsonProperty("csvPriceProvider") - @JsonPropertyDescription("CSV-based price provider configuration") - CsvPriceConfig csvPriceProvider, + @JsonProperty("dynamicPriceConfig") + @JsonPropertyDescription("price provider based on DAO with configuration") + DynamicPriceConfig dynamicPriceConfig, @JsonProperty("flatPriceProvider") @JsonPropertyDescription("Flat price provider configuration") FlatPriceConfig flatPriceConfig ) { - /** - * @return the configured price forecast - */ - @JsonIgnore - public EnergyPriceForecast getEnergyPriceProvider() - { - return switch(type) - { - case CSV -> new CsvStockEnergyPriceProvider(csvPriceProvider); - case FLAT -> new FlatPriceService(flatPriceConfig); - }; - } } \ No newline at end of file diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridPriceConfigType.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridPriceConfigType.java index 5b35092..1e7d3a5 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridPriceConfigType.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridPriceConfigType.java @@ -5,5 +5,5 @@ */ public enum GridPriceConfigType { - FLAT, CSV + FLAT, DAO } diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoEnergyPriceForecast.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoEnergyPriceForecast.java index 8b68aa1..bbdedb1 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoEnergyPriceForecast.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoEnergyPriceForecast.java @@ -2,6 +2,7 @@ import de.hatoka.eos.persistence.capi.energystock.EnergyStockDao; import de.hatoka.eos.persistence.capi.energystock.EnergyStockKey; +import de.hatoka.eos.simulation.capi.business.config.GridConfig; import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; import de.hatoka.eos.units.capi.Money; import jakarta.inject.Inject; @@ -14,10 +15,13 @@ public class DaoEnergyPriceForecast implements EnergyPriceForecast { @Inject private EnergyStockDao stockDao; + @Inject + private GridConfig config; @Override public Money getImportPrice(ZonedDateTime time) { + // config.dynamicPriceConfig().importCharge() return stockDao.get(new EnergyStockKey(time)).getDayAheadPrice(); } diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/EnergyPriceForecastFactory.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/EnergyPriceForecastFactory.java new file mode 100644 index 0000000..22cb2b8 --- /dev/null +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/EnergyPriceForecastFactory.java @@ -0,0 +1,32 @@ +package de.hatoka.eos.simulation.internal.business.forecast; + +import de.hatoka.eos.persistence.capi.energystock.EnergyStockDao; +import de.hatoka.eos.simulation.capi.business.config.GridConfig; +import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +@Singleton +public class EnergyPriceForecastFactory +{ + @Inject + private EnergyStockDao dao; + + @Inject + private GridConfig gridConfig; + + @Inject + private DaoEnergyPriceForecast daoEnergyPriceForecast; + /** + * @return the configured price forecast + */ + public EnergyPriceForecast getEnergyPriceProvider() + { + return switch(gridConfig.type()) + { + case DAO -> daoEnergyPriceForecast; + case FLAT -> new FlatPriceService(gridConfig.flatPriceConfig()); + }; + } + +} diff --git a/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/config/CsvPriceConfigTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/config/CsvPriceConfigTest.java deleted file mode 100644 index e43dc40..0000000 --- a/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/config/CsvPriceConfigTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package de.hatoka.eos.simulation.internal.business.config; - -import de.hatoka.eos.simulation.capi.business.config.CsvPriceConfig; -import de.hatoka.eos.simulation.capi.business.config.GridConfig; -import de.hatoka.eos.simulation.capi.business.config.InstallationConfig; -import de.hatoka.eos.units.capi.Money; -import io.quarkus.test.junit.QuarkusTest; -import jakarta.inject.Inject; -import org.junit.jupiter.api.Test; - -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.*; - -@QuarkusTest -public class CsvPriceConfigTest -{ - @Inject - private ConfigurationLoader configurationLoader; - - @Test - public void testCsvPriceConfigLoading() throws IOException - { - InstallationConfig config = configurationLoader.load("test-installation-for-csv-prices.yaml"); - - assertNotNull(config); - assertNotNull(config.getGrid()); - - GridConfig gridConfig = config.getGrid(); - assertNotNull(gridConfig.csvPriceProvider()); - - CsvPriceConfig csvPriceConfig = gridConfig.csvPriceProvider(); - assertNotNull(csvPriceConfig.resource()); - assertFalse(csvPriceConfig.resource().isEmpty()); - - assertEquals("stockprices_DE_LU_week_32-33.csv", csvPriceConfig.resource().getFirst()); - - assertNotNull(csvPriceConfig.importCharge()); - assertNotNull(csvPriceConfig.exportCharge()); - - assertEquals("EUR", csvPriceConfig.currency()); - assertEquals(Money.ofEur(0.08), csvPriceConfig.importCharge().price()); - assertEquals(Money.ofEur(0.04), csvPriceConfig.exportCharge().price()); - } - - @Test - public void testCsvStockEnergyPriceProviderCreation() throws IOException - { - InstallationConfig config = configurationLoader.load("test-installation-for-csv-prices.yaml"); - GridConfig gridConfig = config.getGrid(); - - de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast priceProvider = gridConfig.getEnergyPriceProvider(); - - assertNotNull(priceProvider); - assertInstanceOf(de.hatoka.eos.simulation.internal.business.forecast.CsvStockEnergyPriceProvider.class, priceProvider); - - // Test actual price retrieval for a known date/time from the CSV - // CSV contains data for 2025/08/04 at hour 0 with price 81.05 EUR/MWh - java.time.ZoneId zone = java.time.ZoneId.of("Europe/Berlin"); - java.time.ZonedDateTime testTime = java.time.LocalDate.of(2025, 8, 4) - .atStartOfDay(zone); // Hour 0 - - Money importPrice = priceProvider.getImportPrice(testTime); - Money exportPrice = priceProvider.getExportPrice(testTime); - - assertNotNull(importPrice); - assertNotNull(exportPrice); - - // Import price should be market price (81.05/1000 = 0.08105) + import charge (0.08) - // Export price should be market price (81.05/1000 = 0.08105) - export charge (0.04) - // Market price is higher than export charge, so export price should be positive - - assertTrue(importPrice.amount().doubleValue() > 0.08, - "Import price should include market price + charges: " + importPrice.amount()); - assertTrue(exportPrice.amount().doubleValue() > 0.0, - "Export price should be positive: " + exportPrice.amount()); - - // Import price should be higher than export price (as is typical) - assertTrue(importPrice.amount().doubleValue() > exportPrice.amount().doubleValue(), - "Import price should be higher than export price"); - - // Verify currency is correctly applied - assertEquals("EUR", importPrice.currencyMnemonic()); - assertEquals("EUR", exportPrice.currencyMnemonic()); - } -} \ No newline at end of file diff --git a/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics/SimulationMetricsExporterTest.java b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics/SimulationMetricsExporterTest.java index 9e0709f..bb9a3ed 100644 --- a/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics/SimulationMetricsExporterTest.java +++ b/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics/SimulationMetricsExporterTest.java @@ -9,6 +9,7 @@ import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; import de.hatoka.eos.simulation.capi.business.simulation.Simulator; import de.hatoka.eos.simulation.internal.business.config.ConfigurationLoader; +import de.hatoka.eos.simulation.internal.business.forecast.FlatPriceService; import de.hatoka.eos.simulation.internal.business.forecast.FlatWeatherService; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; @@ -56,7 +57,7 @@ public void testExportSimulationMetricsToGrafana() throws IOException SimulationRequest request = new SimulationRequest("today-energy-simulation", startDate, endDate, stepDuration, deviceFactory.createDevices(config.getDevices()), Collections.emptyMap(), - new Forecasts(FlatWeatherService.FULL_FROM_7_to_18, config.getGrid().getEnergyPriceProvider())); + new Forecasts(FlatWeatherService.FULL_FROM_7_to_18, FlatPriceService.GERMAN_RESIDENTIAL)); // Act - Run simulation step by step for 5-minute interval data double seconds = endDate.toInstant().getEpochSecond() - startDate.toInstant().getEpochSecond(); From 3a318e4b2cb79d5f1749e96f41e4757a33a90f30 Mon Sep 17 00:00:00 2001 From: Thomas Bergmann Date: Mon, 29 Dec 2025 16:28:28 +0100 Subject: [PATCH 11/16] feat: use memory DAO for tests --- README-configuration.md | 181 +++++++++++ forecast/build.gradle.kts | 1 + .../AbstractWeatherForecastImporter.java | 2 +- .../forecast/ForecastImportApplication.java | 7 - .../energycharts/EnergyChartsImporter.java | 10 +- .../MeteoMediaWeatherForecastImporter.java | 102 ------ ...MediaWeatherSunshineDurationConverter.java | 295 ------------------ .../OpenMeteoWeatherForecastImporter.java | 4 +- ...MeteoMediaWeatherForecastImporterTest.java | 42 --- ...aWeatherSunshineDurationConverterTest.java | 216 ------------- forecast/src/test/resources/application.yml | 11 +- optimization/build.gradle.kts | 4 +- .../src/test/resources/application.properties | 7 +- .../test-installation-for-optimization.yaml | 4 + persistence-api/build.gradle.kts | 1 + .../capi/weather/WeatherDataSource.java | 1 - .../capi/weather/WeatherForecastKey.java | 4 +- .../capi/weather/WeatherForecastKeyTest.java | 189 +++++++++++ .../influx/InfluxWeatherForecastDao.java | 22 +- persistence-memory/README.md | 77 +++++ persistence-memory/build.gradle.kts | 15 + .../memory/MemoryEnergyStockDao.java | 44 +++ .../memory/MemoryWeatherForecastDao.java | 47 +++ .../src/main/resources/META-INF/beans.xml | 1 + .../memory/MemoryEnergyStockDaoTest.java | 91 ++++++ .../memory/MemoryWeatherForecastDaoTest.java | 111 +++++++ settings.gradle | 1 + simulation/build.gradle.kts | 1 + .../business/config/InstallationConfig.java | 15 + .../forecast/DaoEnergyPriceForecast.java | 3 - .../business/forecast/DaoWeatherForecast.java | 32 -- .../forecast/EnergyPriceForecastFactory.java | 6 +- .../src/test/resources/application.properties | 7 +- .../resources/test-installation-with-car.yaml | 4 + .../test-installation-without-car.yaml | 4 + 35 files changed, 838 insertions(+), 724 deletions(-) create mode 100644 README-configuration.md delete mode 100644 forecast/src/main/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherForecastImporter.java delete mode 100644 forecast/src/main/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherSunshineDurationConverter.java delete mode 100644 forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherForecastImporterTest.java delete mode 100644 forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherSunshineDurationConverterTest.java create mode 100644 persistence-api/src/test/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKeyTest.java create mode 100644 persistence-memory/README.md create mode 100644 persistence-memory/build.gradle.kts create mode 100644 persistence-memory/src/main/java/de/hatoka/eos/persistence/memory/MemoryEnergyStockDao.java create mode 100644 persistence-memory/src/main/java/de/hatoka/eos/persistence/memory/MemoryWeatherForecastDao.java create mode 100644 persistence-memory/src/main/resources/META-INF/beans.xml create mode 100644 persistence-memory/src/test/java/de/hatoka/eos/persistence/memory/MemoryEnergyStockDaoTest.java create mode 100644 persistence-memory/src/test/java/de/hatoka/eos/persistence/memory/MemoryWeatherForecastDaoTest.java delete mode 100644 simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoWeatherForecast.java diff --git a/README-configuration.md b/README-configuration.md new file mode 100644 index 0000000..f312c84 --- /dev/null +++ b/README-configuration.md @@ -0,0 +1,181 @@ +# Configuration Module + +## Overview + +The configuration module provides a centralized way to configure EOS (Energy Optimization System) installations including: +- Weather station selection for weather forecasts +- Device configurations (solar panels, batteries, electric cars, etc.) +- Grid pricing models +- Simulation parameters + +## Business Configuration (YAML) + +Business configuration is stored in YAML files and can be loaded using the `ConfigurationLoader` class. This is separate from infrastructure configuration (application.properties). + +### Installation Configuration + +Installation configuration defines the physical setup of an energy system. + +#### Example: `test-installation.yaml` + +```yaml +devices: + - type: SOLAR_PANEL + production: + amount: 0.45 + unit: "K_W" + statisticsResource: "curved" + panelEfficiency: .92 + count: 13 + + - type: BATTERY + capacity: + amount: 3.0 + unit: "K_WH" + chargeRate: + amount: 2.2 + unit: "K_W" + dischargeRate: + amount: 2.1 + unit: "K_W" + count: 3 + chargingEfficiency: 0.85 + dischargingEfficiency: 0.85 + + - type: GRID + count: 1 + +# Weather station configuration for forecasts +# Available stations: APOLDA, LEIPZIG_STADTWERKE +weatherStation: APOLDA + +grid: + type: "FLAT" + flatPriceProvider: + importPrice: + amount: 0.39 + currency: EUR + exportPrice: + amount: 0.08 + currency: EUR +``` + +### Weather Station Configuration + +The `weatherStation` field in the installation configuration specifies which weather station to use for weather forecasts. + +Available weather stations: +- **APOLDA**: Located at 51.0262°N, 11.5164°E (Station #095550) +- **LEIPZIG_STADTWERKE**: Located at 51.3397°N, 12.3731°E (Station #104700) + +Example: +```yaml +weatherStation: APOLDA +``` + +If not specified, defaults to `APOLDA`. + +### Loading Configuration + +```java +@Inject +private ConfigurationLoader configurationLoader; + +@Inject +private ForecastsFactory forecastsFactory; + +// Load installation configuration +InstallationConfig config = configurationLoader.loadInstallation("my-installation.yaml"); + +// Create forecasts from configuration +Forecasts forecasts = forecastsFactory.createForecasts(config); + +// Use in simulation +SimulationRequest request = new SimulationRequest( + "simulation-id", + startDate, + endDate, + stepDuration, + devices, + initialState, + forecasts // Uses the weather station configured in YAML +); +``` + +## Key Classes + +### `InstallationConfig` +Contains the complete configuration of an energy installation: +- `devices`: List of device configurations +- `grid`: Grid pricing configuration +- `weatherStation`: Weather station enum for forecasts + +### `ForecastsFactory` +Factory class that creates `Forecasts` objects from `InstallationConfig`: +- Automatically creates `DaoWeatherForecast` with the configured weather station +- Falls back to default flat forecasts if configuration is missing + +### `DaoWeatherForecast` +Weather forecast implementation that reads from InfluxDB: +- Uses the configured `WeatherStation` to fetch forecast data +- Accesses data via `WeatherForcastDAO` + +## Adding New Weather Stations + +To add a new weather station: + +1. Add the station to `WeatherStation` enum in `persistence-api`: +```java +public enum WeatherStation { + APOLDA("095550", 51.0262, 11.5164), + LEIPZIG_STADTWERKE("104700", 51.3397, 12.3731), + NEW_STATION("123456", 52.5200, 13.4050); // Add here + // ... +} +``` + +2. Update your YAML configuration: +```yaml +weatherStation: NEW_STATION +``` + +## Migration Guide + +If you're using the old hardcoded weather station approach: + +**Before:** +```java +// Hardcoded station +Forecasts forecasts = Forecasts.STANDARD; +``` + +**After:** +```java +// Load from configuration +InstallationConfig config = configurationLoader.loadInstallation("my-installation.yaml"); +Forecasts forecasts = forecastsFactory.createForecasts(config); +``` + +## Testing + +Test configurations can be found in: +- `simulation/src/test/resources/test-installation-*.yaml` +- `optimization/src/test/resources/test-installation-for-optimization.yaml` + +Example test usage: +```java +@Inject +private ConfigurationLoader configurationLoader; + +@Inject +private ForecastsFactory forecastsFactory; + +@Test +public void testWithConfiguration() { + InstallationConfig config = configurationLoader.loadInstallation("test-installation-with-car.yaml"); + Forecasts forecasts = forecastsFactory.createForecasts(config); + + // forecasts.weather() will use the weather station from YAML +} +``` + diff --git a/forecast/build.gradle.kts b/forecast/build.gradle.kts index 50e4b8f..df52d5e 100644 --- a/forecast/build.gradle.kts +++ b/forecast/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation("org.slf4j:slf4j-api") implementation("jakarta.inject:jakarta.inject-api") + testImplementation(project(":persistence-memory")) testImplementation("io.quarkus:quarkus-junit5") testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java index 9506a6c..b41bd59 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/AbstractWeatherForecastImporter.java @@ -101,7 +101,7 @@ protected void storeSunshineData(Map sunshineDurationPer try { - weatherDao.update(new WeatherForecastKey(station.name(), dateTime, getSource()), forecast); + weatherDao.update(WeatherForecastKey.valueOf(station, dateTime, getSource()), forecast); logger.debug("Stored weather data for {}: {}% sun probability", dateTime, Math.round(sunProbability * 100)); } catch(Exception e) diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java b/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java index 76a5ec1..e8c3ef2 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java @@ -1,7 +1,6 @@ package de.hatoka.eos.forecast; import de.hatoka.eos.forecast.energycharts.EnergyChartsImporter; -import de.hatoka.eos.forecast.meteomedia.MeteoMediaWeatherForecastImporter; import de.hatoka.eos.forecast.openmeteo.OpenMeteoWeatherForecastImporter; import de.hatoka.eos.persistence.capi.weather.WeatherStation; import io.quarkus.runtime.Quarkus; @@ -19,9 +18,6 @@ public class ForecastImportApplication implements QuarkusApplication { private final Logger logger = LoggerFactory.getLogger(ForecastImportApplication.class); - @Inject - MeteoMediaWeatherForecastImporter meteoMediaImporter; - @Inject OpenMeteoWeatherForecastImporter openMeteoImporter; @@ -43,9 +39,6 @@ public int run(String... args) throws Exception energyChartsImporter.importStockData(startDate); // Import from MeteoMedia (can only import data of today and tomorrow) logger.debug("Importing from MeteoMedia..."); - meteoMediaImporter.importWeatherForecast(WeatherStation.APOLDA, startDate); - meteoMediaImporter.importWeatherForecast(WeatherStation.LEIPZIG_STADTWERKE, startDate); - logger.info("Import from MeteoMedia finished."); // Import from OpenMeteo (can import up to 3 days) logger.debug("Importing from OpenMeteo..."); diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java index 0b254d0..dfba2f4 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java @@ -27,7 +27,7 @@ @Singleton public class EnergyChartsImporter { - private static final String URI_FORMAT = "https://energy-charts.info/charts/price_spot_market/data/de/week_15min_%s_%s.json"; // format parameters: year, week + private static final String URI_FORMAT = "https://energy-charts.info/charts/price_spot_market/data/de/week_15min_%s_%02d.json"; // format parameters: year, week private static final String DAY_AHEAD_AUCTION_NAME_EN = "Day Ahead Auction (DE-LU)"; private static final Logger logger = LoggerFactory.getLogger(EnergyChartsImporter.class); private final ObjectMapper objectMapper = new ObjectMapper(); @@ -38,7 +38,7 @@ public class EnergyChartsImporter public void importStockData(ZonedDateTime startDate) throws IOException, InterruptedException { - List responses = downloadData(URI.create(URI_FORMAT.formatted(startDate.getYear(), getWeekOfYear(startDate)))); + List responses = downloadData(URI.create(URI_FORMAT.formatted(getYearOfWeek(startDate), getWeekOfYear(startDate)))); EnergyChartsResponse dayAheadAuction = findDayAheadAuction(responses); Map dayAheadPrices = mapDataToTime(startDate, dayAheadAuction.getData()); String currency = dayAheadAuction.getCurrency(); @@ -80,6 +80,12 @@ private Map mapDataToTime(ZonedDateTime startDate, List downloadAndProcessWeatherData(WeatherStation station, ZonedDateTime startDate) - throws IOException, InterruptedException - { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy_MM_dd"); - // Download the PNG image - byte[] imageData = downloadImage(URI.create(URI_FORMAT.formatted(dateFormat.format(startDate)))); - logger.debug("Downloaded {} bytes of image data", imageData.length); - - // Save to temporary file for processing - Path tempFile = saveTempImage(imageData, startDate); - - try - { - // Process the image and extract sunshine data - return converter.extractSunshineDuration(converter.loadImageFromFile(tempFile.toFile()), startDate); - } - finally - { - // Clean up temporary file - Files.deleteIfExists(tempFile); - } - } - - private byte[] downloadImage(URI imageUrl) throws IOException, InterruptedException - { - try (HttpClient httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(30)) - .followRedirects(HttpClient.Redirect.NORMAL) - .build()) - { - HttpRequest request = HttpRequest.newBuilder() - .uri(imageUrl) - .timeout(Duration.ofSeconds(30)) - .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36") - .header("Accept", "image/png,image/*;q=0.9,*/*;q=0.8") - .GET() - .build(); - - logger.debug("Sending HTTP request to: {}", imageUrl); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); - - if (response.statusCode() != 200) - { - throw new IOException("HTTP request failed with status code: " + response.statusCode()); - } - return response.body(); - } - } - - private Path saveTempImage(byte[] imageData, ZonedDateTime startDate) throws IOException - { - String filename = "weather_forecast_" + startDate.format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmm")) + ".png"; - - Path tempFile = Files.createTempFile("weather_", filename); - Files.write(tempFile, imageData, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - - logger.debug("Saved temporary image file: {}", tempFile); - return tempFile; - } -} diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherSunshineDurationConverter.java b/forecast/src/main/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherSunshineDurationConverter.java deleted file mode 100644 index a876575..0000000 --- a/forecast/src/main/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherSunshineDurationConverter.java +++ /dev/null @@ -1,295 +0,0 @@ -package de.hatoka.eos.forecast.meteomedia; - -import jakarta.inject.Singleton; -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVPrinter; -import org.slf4j.LoggerFactory; - -import javax.imageio.ImageIO; -import java.awt.Color; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringWriter; -import java.time.LocalDate; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Converts weather forecast PNG images to CSV format by extracting sunshine duration data. - * The converter analyzes yellow bars in the PNG to determine sunshine hours per day. - * - * Image structure (from top to bottom): - * 1. Temperature (12-hour max, 12-hour min) - * 2. Wind gusts / Wind speed - * 3. Wind direction - * 4. Relative humidity - * 5. Sunshine duration per day (yellow area) - * 6. Sunshine duration per hour (yellow bars) - THIS IS WHAT WE EXTRACT - * 7. Precipitation 6h - */ -@Singleton -public class MeteoMediaWeatherSunshineDurationConverter -{ - private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd"); - - static final Color YELLOW = new Color(249, 220, 32); - static final Color GRAY = new Color(220, 220, 220); - static final Color BLACK = Color.BLACK; - - // Extended window to include full 24-hour forecast day (hour 2 of day 1 through hour 1 of day 2) - record SunHourRegion(int minX, int maxX, int minY, int maxY) { - SunHourRegion nextDay() - { - return new SunHourRegion(maxX,maxX + (maxX - minX), minY, maxY); - } - } - // picked up region from png image - static final SunHourRegion WINDOW_DAY1 = new SunHourRegion(180, 342, 373, 408); - - // Bar positioning constants (from Excel analysis) - private static final double PIXELS_PER_HOUR = ((double)(WINDOW_DAY1.maxX - WINDOW_DAY1.minX)) / 24; - - /** - * Calculates the X coordinate for a given index in the weather image. - * - * Formula: X = 183 + (index - 2) * 6.75 - * where index is 0-23 (0h, 1h, ..., 23h) - * - * Examples: - * - Hour 2: X = 183 + (2-2) * 6.75 = 183 - * - Hour 9: X = 183 + (9-2) * 6.75 = 183 + 47.25 = 230.25 → 230 - * - Hour 10: X = 183 + (10-2) * 6.75 = 183 + 54 = 237 - * - * @param index the index (0-23) of UTC - * @return the X coordinate in pixels - */ - int calculateHourXPosition(SunHourRegion region, int index) - { - double x = region.minX + PIXELS_PER_HOUR/2 + index * PIXELS_PER_HOUR; - return (int) Math.round(x); - } - - /** - * Exports sunshine duration data from PNG image to CSV format. - * Each row represents a date with hourly sunshine values as columns. - * - * @param startDate the start date of the image - * @param pngResourceName the name of the PNG resource file - * @return CSV string with format: date,hour_values... - * Example: 2025/10/24,0,0,2,26,26,4,9,11,... (date + values for hours present in that date) - * @throws IOException if reading the image fails - */ - public String exportToCSV(ZonedDateTime startDate, String pngResourceName) throws IOException - { - BufferedImage image = loadImage(pngResourceName); - Map sunshineDurationPerHour = extractSunshineDuration(image, startDate); - return exportToCSV(sunshineDurationPerHour, startDate.getZone()); - } - - BufferedImage loadImage(String resourceName) throws IOException - { - try (InputStream is = getClass().getClassLoader().getResourceAsStream(resourceName)) - { - if (is == null) - { - throw new IOException("Resource not found: " + resourceName); - } - return ImageIO.read(is); - } - } - - /** - * Loads an image from a file path. - * - * @param file the image file - * @return the loaded BufferedImage - * @throws IOException if the file cannot be read - */ - BufferedImage loadImageFromFile(File file) throws IOException - { - return ImageIO.read(file); - } - - /** - * Extracts sunshine duration data from the weather forecast image. - * The image contains hourly sunshine duration bars in yellow color. - * Each forecast "day" runs from hour 2 of one day through hour 1 of the next day (24 hours). - * - * @param image the weather forecast image - * @param startDate the start date of the forecast (the date when hour 2 begins) - * @return map of datetime (hour precision in UTC) to sunshine minutes for that hour - */ - Map extractSunshineDuration(BufferedImage image, ZonedDateTime startDate) - { - Map result = new HashMap<>(); - result.putAll(extractSunshineDuration(image, startDate, WINDOW_DAY1)); - result.putAll(extractSunshineDuration(image, startDate.plusDays(1), WINDOW_DAY1.nextDay())); - return result; - } - - /** - * Extracts sunshine duration data from the weather forecast image using a specific region. - * The image contains hourly sunshine duration bars in yellow color. - * Each forecast "day" runs from hour 2 of one day through hour 1 of the next day (24 hours). - * - * @param image the weather forecast image - * @param startDate the start date of the forecast (the date when hour 2 begins) - * @param region the region to extract from - * @return map of datetime (hour precision in UTC) to sunshine minutes for that hour - */ - Map extractSunshineDuration(BufferedImage image, ZonedDateTime startDate, SunHourRegion region) - { - // Find the actual yellow bar region - LoggerFactory.getLogger(getClass()).debug("Find sun hours in region: Y={} to {}, X={} to {}", region.minY, region.maxY, region.minX, region.maxX); - - // Extract hourly values from the yellow bars using the markers - // Returns 24 values: hours 2-23 of startDate, then hours 0-1 of startDate+1 - List hourlyValues = extractHourlyValues(image, region); - LoggerFactory.getLogger(getClass()).info("Day {} hourly values {}.", startDate, hourlyValues); - - // Map hourly values to their datetime - // Forecast day: hour 2 of startDate through hour 1 of startDate+1 - Map result = new HashMap<>(); - if (hourlyValues.isEmpty()) - { - return result; - } - - // Map each hourly value to its datetime (automatically wraps to next day at midnight) - ZonedDateTime currentDateTime = startDate; - for (Integer sunMinutes : hourlyValues) - { - result.put(currentDateTime, sunMinutes); - currentDateTime = currentDateTime.plusHours(1); - } - - return result; - } - - /** - * Extracts hourly sunshine duration values from bars. - * The bar height represents the sunshine duration for that hour. - * - * Strategy: Based on Excel analysis: - * - Average bar width: 6.75 pixels per hour - * - Start at X=183 (middle between 2h and 3h markers) - * - For hour N (starting at 2h): X = 183 + (N-2) * 6.75 - * - Bar height measured from Y=407 (bottom) up to colored pixels - * - Extracts full 24-hour forecast day: hour 2 of day 1 through hour 1 of day 2 - * - * @param image the weather forecast image - * @param region the region containing the sunshine bars - * @return list of sunshine minutes (0-60) for each hour in sequence - */ - private List extractHourlyValues(BufferedImage image, SunHourRegion region) - { - List values = new ArrayList<>(); - - for (int index = 0; index < 24; index++) - { - // Get X position for this linear hour position - int hourX = calculateHourXPosition(region, index); - - if (hourX >= image.getWidth() || hourX >= region.maxX) - { - break; // Reached image or region boundary - } - - // Calculate sunshine minutes for this hour using the refactored calcSunMinutes method - int sunMinutes = calcSunMinutes(image, hourX, region); - values.add(sunMinutes); - } - return values; - } - - int calcSunMinutes(BufferedImage image, int x, SunHourRegion region) - { - int pixel = measureBarHeightToBlackLine(image, x, region); - int pixel100Percent = region.maxY - region.minY; - return Math.ceilDiv(pixel * 60, pixel100Percent); - } - - /** - * Measures the bar height starting from Y=407 (bottom of bar area). - * - * Strategy: - * 1. Check pixel at Y=407 - * - If gray → 0% (no bar) - * - If black → very small value (bar < 1 pixel visible) - * - If yellow → scan upward until black line found - * - * @param image the image - * @param x the X coordinate to measure at - * @return height in pixels (0 = no bar/no sun ) - */ - int measureBarHeightToBlackLine(BufferedImage image, int x, SunHourRegion region) - { - int y = region.maxY - 1; // Bottom of bar area 408 is axe line - int topLimit = region.minY; // Top boundary (where black line is) - - // Check the pixel at Y=407 - Color color = new Color(image.getRGB(x, y)); - if (isGrayPixel(color)) - { - return 0; - } - while(isYellowPixel(color) && topLimit < y) - { - y--; - color = new Color(image.getRGB(x, y)); - } - return region.maxY - y; - } - - private boolean isGrayPixel(Color color) - { - return GRAY.equals(color); - } - - private boolean isYellowPixel(Color color) - { - return YELLOW.equals(color); - } - - /** - * Converts the given map(datetime, number) to a csv string (comma separated). Each line a new date at given timezone. - * @param sunshineDurationPerHour time-based numbers - * @param timeZone timezone to group times by date - * @return comma csv file first column date followed by hours 0-23 - * @throws IOException if the string be written - */ - String exportToCSV(Map sunshineDurationPerHour, ZoneId timeZone) throws IOException - { - StringWriter sw = new StringWriter(); - CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setDelimiter(",").setSkipHeaderRecord(true).get(); - - // Group by date - Map> byDate = new java.util.TreeMap<>(); - sunshineDurationPerHour.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .forEach(entry -> { - LocalDate date = entry.getKey().withZoneSameInstant(timeZone).toLocalDate(); - byDate.computeIfAbsent(date, k -> new ArrayList<>()).add(entry.getValue()); - }); - - try (final CSVPrinter printer = new CSVPrinter(sw, csvFormat)) - { - for (Map.Entry> entry : byDate.entrySet()) - { - List row = new ArrayList<>(); - row.add(DATE_FORMATTER.format(entry.getKey())); - row.addAll(entry.getValue()); - printer.printRecord(row); - } - } - - return sw.toString().trim(); - } -} diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoWeatherForecastImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoWeatherForecastImporter.java index da1ed35..89502e6 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoWeatherForecastImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/openmeteo/OpenMeteoWeatherForecastImporter.java @@ -152,12 +152,12 @@ private Map convertToSunshineDurationMap(OpenMeteoRespon timeString.indexOf("T")).contains("-")) { // Format like "2025-11-05T15:00" - add UTC timezone - dateTime = ZonedDateTime.parse(timeString + "Z", DateTimeFormatter.ISO_OFFSET_DATE_TIME); + dateTime = ZonedDateTime.parse(timeString + "Z", DateTimeFormatter.ISO_ZONED_DATE_TIME); } else { // Format with timezone information - dateTime = ZonedDateTime.parse(timeString, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + dateTime = ZonedDateTime.parse(timeString, DateTimeFormatter.ISO_ZONED_DATE_TIME); } // Convert sunshine duration from seconds to minutes diff --git a/forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherForecastImporterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherForecastImporterTest.java deleted file mode 100644 index d052883..0000000 --- a/forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherForecastImporterTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package de.hatoka.eos.forecast.meteomedia; - -import de.hatoka.eos.persistence.capi.weather.WeatherStation; -import de.hatoka.eos.persistence.capi.weather.WeatherDataSource; -import de.hatoka.eos.persistence.capi.weather.WeatherForcastDAO; -import de.hatoka.eos.persistence.capi.weather.WeatherForecastKey; -import de.hatoka.eos.persistence.capi.weather.WeatherForecastPO; -import io.quarkus.test.junit.QuarkusTest; -import jakarta.inject.Inject; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.time.ZoneId; -import java.time.ZonedDateTime; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Test for WeatherForecastImporter functionality. - * Note: This test requires network access to download actual weather images. - * For CI/CD environments, you might want to mock the HTTP client or use test resources. - */ -@QuarkusTest -class MeteoMediaWeatherForecastImporterTest -{ - @Inject - MeteoMediaWeatherForecastImporter importer; - - @Inject - WeatherForcastDAO weatherDao; - - @Test - void testImportFromValidUrl() throws IOException, InterruptedException - { - // this importer can only import data of today and tomorrow - ZonedDateTime startDate = ZonedDateTime.now().toLocalDate().atStartOfDay(ZoneId.of("UTC")); - ZonedDateTime testDate = importer.importWeatherForecast(WeatherStation.APOLDA, startDate); - WeatherForecastPO retrieved = weatherDao.get(WeatherForecastKey.valueOf(WeatherStation.APOLDA, testDate.plusHours(1), WeatherDataSource.METEOMEDIA)); - assertNotNull(retrieved); - assertNotNull(retrieved.getSunProbability()); - } -} diff --git a/forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherSunshineDurationConverterTest.java b/forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherSunshineDurationConverterTest.java deleted file mode 100644 index aa0524d..0000000 --- a/forecast/src/test/java/de/hatoka/eos/forecast/meteomedia/MeteoMediaWeatherSunshineDurationConverterTest.java +++ /dev/null @@ -1,216 +0,0 @@ -package de.hatoka.eos.forecast.meteomedia; - -import io.quarkus.test.junit.QuarkusTest; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.time.*; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; - -@QuarkusTest -class MeteoMediaWeatherSunshineDurationConverterTest -{ - private static final String TEST_IMAGE_CEST = "weather_20251024.png"; - private static final String TEST_IMAGE_CET = "weather_20251027.png"; - private static final LocalDate START_DATE_CEST = LocalDate.of(2025, 10, 24); - private static final LocalDate START_DATE_CET = LocalDate.of(2025, 10, 27); - private static final ZoneId BERLIN = ZoneId.of("Europe/Berlin"); - private static final ZoneId UTC = ZoneId.of("UTC"); - private final MeteoMediaWeatherSunshineDurationConverter converter = new MeteoMediaWeatherSunshineDurationConverter(); - private final MeteoMediaWeatherSunshineDurationConverter.SunHourRegion region = MeteoMediaWeatherSunshineDurationConverter.WINDOW_DAY1; - - @Test - void testCalculateHourXPosition() - { - // Test all hours 2-23 - System.out.println("\nAll hour positions:"); - for (int position = 0; position < 24; position++) - { - int x = converter.calculateHourXPosition(region, position); - System.out.printf("Hour(UTC) %2d: X=%d%n", position, x); - } - Assertions.assertEquals(183, converter.calculateHourXPosition(region,0), "marker between 180 und 187"); - Assertions.assertEquals(190, converter.calculateHourXPosition(region,1)); - Assertions.assertEquals(231, converter.calculateHourXPosition(region,7)); - Assertions.assertEquals(237, converter.calculateHourXPosition(region,8)); - - } - - @Test - void testColorAtHourPositions() throws IOException - { - BufferedImage image = converter.loadImage(TEST_IMAGE_CEST); - // position 407 is one pixel above the zero line - Assertions.assertEquals( - MeteoMediaWeatherSunshineDurationConverter.GRAY, new Color(image.getRGB(converter.calculateHourXPosition(region,0), 407))); - Assertions.assertEquals( - MeteoMediaWeatherSunshineDurationConverter.BLACK, new Color(image.getRGB(converter.calculateHourXPosition(region,7), 407))); - Assertions.assertEquals( - MeteoMediaWeatherSunshineDurationConverter.YELLOW, new Color(image.getRGB(converter.calculateHourXPosition(region,8), 407))); - } - - @Test - void testMeasureBarHeights() throws IOException - { - BufferedImage image = converter.loadImage(TEST_IMAGE_CEST); - Assertions.assertEquals(0, converter.measureBarHeightToBlackLine(image, converter.calculateHourXPosition(region,0), region)); - Assertions.assertEquals(1, converter.measureBarHeightToBlackLine(image, converter.calculateHourXPosition(region,7), region)); - Assertions.assertEquals(15, converter.measureBarHeightToBlackLine(image, converter.calculateHourXPosition(region,8), region)); - Assertions.assertEquals(5, converter.measureBarHeightToBlackLine(image, converter.calculateHourXPosition(region,11), region)); - } - - @Test - void testSunMinutes() throws IOException - { - BufferedImage image = converter.loadImage(TEST_IMAGE_CEST); - Assertions.assertEquals(0, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,0), region)); - Assertions.assertEquals(2, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,7), region)); - Assertions.assertEquals(26, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,8), region)); - Assertions.assertEquals(26, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,9), region)); - Assertions.assertEquals(4, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,10), region)); - Assertions.assertEquals(9, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,11), region)); - Assertions.assertEquals(11, converter.calcSunMinutes(image, converter.calculateHourXPosition(region,12), region)); - } - - @Test - void testExtractSunshineDuration() throws IOException - { - BufferedImage image = converter.loadImage(TEST_IMAGE_CEST); - Map result = converter.extractSunshineDuration(image, toUTC(START_DATE_CEST)); - - assertNotNull(result); - assertFalse(result.isEmpty()); - assertEquals(48, result.size()); - - // Verify specific hourly values from testSunMinutes (hours 2-23 of day 1) - assertEquals(0, result.get(fromBerlinTimeToUTC(START_DATE_CEST,2))); - assertEquals(2, result.get(fromBerlinTimeToUTC(START_DATE_CEST, 9))); - assertEquals(26, result.get(fromBerlinTimeToUTC(START_DATE_CEST,10))); - assertEquals(26, result.get(fromBerlinTimeToUTC(START_DATE_CEST,11))); - assertEquals(4, result.get(fromBerlinTimeToUTC(START_DATE_CEST, 12))); - assertEquals(9, result.get(fromBerlinTimeToUTC(START_DATE_CEST, 13))); - assertEquals(11, result.get(fromBerlinTimeToUTC(START_DATE_CEST,14))); - - // Verify hours 0 and 1 of next day are included - LocalDate nextDay = START_DATE_CEST.plusDays(1); - assertTrue(result.containsKey(fromBerlinTimeToUTC(nextDay, 0))); - assertTrue(result.containsKey(fromBerlinTimeToUTC(nextDay, 1))); - } - - private ZonedDateTime fromBerlinTimeToUTC(LocalDate startDate, int hours) - { - return LocalDateTime.of(startDate, LocalTime.of(hours, 0)).atZone(BERLIN).withZoneSameInstant(UTC); - } - - private ZonedDateTime toUTC(LocalDate startDate) - { - return LocalDateTime.of(startDate, LocalTime.of(0, 0)).atZone(UTC); - } - - @Test - void testExportToCSV() throws IOException - { - String csv = converter.exportToCSV(toUTC(START_DATE_CEST), TEST_IMAGE_CEST); - assertNotNull(csv); - assertFalse(csv.contains(".")); - - String[] lines = csv.split("\n"); - assertEquals(2, lines.length); - - // First line: date 10/24 with hours 2-23 (22 values) - String[] values1 = lines[0].split(","); - assertEquals(25, values1.length); // date + 22 hour values - assertEquals("2025/10/24", values1[0]); - - // Verify specific values from testSunMinutes (hours 2,9,10,11,12,13,14 are at indices 1,8,9,10,11,12,13) - assertEquals("0", values1[1]); // hour 2 - assertEquals("2", values1[8]); // hour 9 - assertEquals("26", values1[9]); // hour 10 - assertEquals("26", values1[10]); // hour 11 - assertEquals("4", values1[11]); // hour 12 - assertEquals("9", values1[12]); // hour 13 - assertEquals("11", values1[13]); // hour 14 - - // Second line: date 10/25 full (24 hours) - String[] values2 = lines[1].split(","); - assertEquals(25, values2.length); // date + 24 hour values - assertEquals("2025/10/25", values2[0]); - } - - @Test - void testTotalSunshineHours() throws IOException - { - BufferedImage image = converter.loadImage(TEST_IMAGE_CEST); - Map result = converter.extractSunshineDuration(image, toUTC(START_DATE_CEST)); - LocalDate day2 = START_DATE_CEST.plusDays(1); - - assertEquals(1.6 * 60, getTotalMinutes(result, toUTC(START_DATE_CEST)), 6); - assertEquals(1.2 * 60, getTotalMinutes(result, toUTC(day2)), 6); - } - - private static int getTotalMinutes(Map result, ZonedDateTime day) - { - return result.entrySet() - .stream() - .filter(e -> e.getKey().getDayOfMonth() == day.getDayOfMonth()) - .map(Map.Entry::getValue) - .mapToInt(Integer::intValue) - .sum(); - } - - @Test - void testSecondDay() throws IOException - { - BufferedImage image = converter.loadImage(TEST_IMAGE_CEST); - LocalDate day2 = START_DATE_CEST.plusDays(1); - LocalDate day3 = START_DATE_CEST.plusDays(2); - - Map result = converter.extractSunshineDuration(image, toUTC(START_DATE_CEST)); - assertEquals(48, result.size()); // startDate and 2 now - assertTrue(result.containsKey(fromBerlinTimeToUTC(START_DATE_CEST, 2)), "day1 first hour"); - assertTrue(result.containsKey(fromBerlinTimeToUTC(day2, 2)), "day2 first hour"); - assertTrue(result.containsKey(fromBerlinTimeToUTC(day3, 1)), "day2 last hour on day3"); - - // Verify specific hourly values from testSunMinutes (hours 2-23 of day 1) - assertEquals(0, result.get(fromBerlinTimeToUTC(day2, 2))); - assertEquals(9, result.get(fromBerlinTimeToUTC(day2, 9))); - assertEquals(14, result.get(fromBerlinTimeToUTC(day2,10))); - assertEquals(18, result.get(fromBerlinTimeToUTC(day2,11))); - assertEquals(14, result.get(fromBerlinTimeToUTC(day2, 12))); - assertEquals(14, result.get(fromBerlinTimeToUTC(day2, 13))); - assertEquals(7, result.get(fromBerlinTimeToUTC(day2, 14))); - assertEquals(0, result.get(fromBerlinTimeToUTC(day2, 15))); - } - - @Test - void testCET() throws IOException - { - BufferedImage image = converter.loadImage(TEST_IMAGE_CET); - - Map result = converter.extractSunshineDuration(image, toUTC(START_DATE_CET)); - assertEquals(48, result.size()); // startDate and 2 now - assertTrue(result.containsKey(fromBerlinTimeToUTC(START_DATE_CET, 1)), "day1 first hour"); - assertTrue(result.containsKey(fromBerlinTimeToUTC(START_DATE_CET.plusDays(1), 2)), "day2 first hour"); - - // Verify specific hourly values from testSunMinutes (hours 2-23 of day 1) - assertEquals(0, result.get(fromBerlinTimeToUTC(START_DATE_CET, 1))); - assertEquals(0, result.get(fromBerlinTimeToUTC(START_DATE_CET, 6))); - assertEquals(4, result.get(fromBerlinTimeToUTC(START_DATE_CET, 7))); - assertEquals(21, result.get(fromBerlinTimeToUTC(START_DATE_CET, 8))); - assertEquals(7, result.get(fromBerlinTimeToUTC(START_DATE_CET, 9))); - assertEquals(6, result.get(fromBerlinTimeToUTC(START_DATE_CET, 10))); - assertEquals(43, result.get(fromBerlinTimeToUTC(START_DATE_CET, 11))); - assertEquals(31, result.get(fromBerlinTimeToUTC(START_DATE_CET, 12))); - assertEquals(11, result.get(fromBerlinTimeToUTC(START_DATE_CET, 13))); - assertEquals(0, result.get(fromBerlinTimeToUTC(START_DATE_CET, 14))); - assertEquals(31, result.get(fromBerlinTimeToUTC(START_DATE_CET, 15))); - assertEquals(0, result.get(fromBerlinTimeToUTC(START_DATE_CET, 16))); - assertEquals(19, result.get(fromBerlinTimeToUTC(START_DATE_CET, 17))); - assertEquals(0, result.get(fromBerlinTimeToUTC(START_DATE_CET, 18))); - } -} diff --git a/forecast/src/test/resources/application.yml b/forecast/src/test/resources/application.yml index 4c5fdb1..f3f899d 100644 --- a/forecast/src/test/resources/application.yml +++ b/forecast/src/test/resources/application.yml @@ -1,8 +1,13 @@ -# Test configuration for InfluxDB using environment variables +# Test configuration - use in-memory persistence instead of InfluxDB +quarkus: + arc: + exclude-types: + - de.hatoka.eos.persistence.influx.** + eos: influxdb: - url: ${INFLUX_URI} - token: ${INFLUX_TOKEN} + url: ${INFLUX_URI:http://localhost:8086} + token: ${INFLUX_TOKEN:test-token} org: eos bucket: energy_simulation diff --git a/optimization/build.gradle.kts b/optimization/build.gradle.kts index c884ae3..b07b5ff 100644 --- a/optimization/build.gradle.kts +++ b/optimization/build.gradle.kts @@ -19,9 +19,7 @@ dependencies { implementation("io.quarkus:quarkus-arc") - // InfluxDB client for Grafana visualization - // implementation("com.influxdb:influxdb-client-java") - + testImplementation(project(":persistence-memory")) testImplementation("io.quarkus:quarkus-junit5") testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/optimization/src/test/resources/application.properties b/optimization/src/test/resources/application.properties index 98ce92a..f564011 100644 --- a/optimization/src/test/resources/application.properties +++ b/optimization/src/test/resources/application.properties @@ -1,8 +1,11 @@ quarkus.log.level=INFO quarkus.log.category."com.intershop".level=DEBUG -# InfluxDB Metrics Configuration for tests -eos.metrics.influxdb.enabled=true +# Exclude InfluxDB implementations from CDI, use in-memory persistence for tests +quarkus.arc.exclude-types=de.hatoka.eos.persistence.influx.** + +# InfluxDB Metrics Configuration for tests (not used with in-memory persistence) +eos.metrics.influxdb.enabled=false eos.metrics.influxdb.url=http://localhost:8086 eos.metrics.influxdb.token=eos-token-12345678901234567890 eos.metrics.influxdb.org=eos diff --git a/optimization/src/test/resources/test-installation-for-optimization.yaml b/optimization/src/test/resources/test-installation-for-optimization.yaml index 8dede86..b3cda0e 100644 --- a/optimization/src/test/resources/test-installation-for-optimization.yaml +++ b/optimization/src/test/resources/test-installation-for-optimization.yaml @@ -56,6 +56,10 @@ devices: - type: GRID count: 1 +# Weather station configuration for forecasts +# Available stations: APOLDA, LEIPZIG_STADTWERKE +weatherStation: APOLDA + grid: type: "CSV" flatPriceProvider: diff --git a/persistence-api/build.gradle.kts b/persistence-api/build.gradle.kts index 11f5d37..b17a223 100644 --- a/persistence-api/build.gradle.kts +++ b/persistence-api/build.gradle.kts @@ -9,4 +9,5 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind") testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherDataSource.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherDataSource.java index 83edcf9..ce65c77 100644 --- a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherDataSource.java +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherDataSource.java @@ -5,7 +5,6 @@ */ public enum WeatherDataSource { - METEOMEDIA("meteomedia"), OPENMETEO("openmeteo"), TEST("test"); diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKey.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKey.java index f996080..9e4aa0b 100644 --- a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKey.java +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKey.java @@ -2,10 +2,10 @@ import java.time.ZonedDateTime; -public record WeatherForecastKey(String station, ZonedDateTime time, WeatherDataSource source) +public record WeatherForecastKey(WeatherStation station, long time, WeatherDataSource source) { public static WeatherForecastKey valueOf(WeatherStation station, ZonedDateTime time, WeatherDataSource source) { - return new WeatherForecastKey(station.name(), time, source); + return new WeatherForecastKey(station, time.toEpochSecond(), source); } } diff --git a/persistence-api/src/test/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKeyTest.java b/persistence-api/src/test/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKeyTest.java new file mode 100644 index 0000000..c734ef0 --- /dev/null +++ b/persistence-api/src/test/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKeyTest.java @@ -0,0 +1,189 @@ +package de.hatoka.eos.persistence.capi.weather; + +import org.junit.jupiter.api.Test; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test class for WeatherForecastKey to verify equality and hashCode behavior, + * especially with ZonedDateTime instances. + */ +class WeatherForecastKeyTest +{ + @Test + void testEqualityWithSameInstances() + { + // Given + ZonedDateTime time = ZonedDateTime.parse("2024-01-01T12:00:00Z"); + WeatherForecastKey key1 = WeatherForecastKey.valueOf(WeatherStation.APOLDA, time, WeatherDataSource.OPENMETEO); + WeatherForecastKey key2 = WeatherForecastKey.valueOf(WeatherStation.APOLDA, time, WeatherDataSource.OPENMETEO); + + // Then + assertEquals(key1, key2); + assertEquals(key1.hashCode(), key2.hashCode()); + } + + @Test + void testEqualityWithDifferentZonedDateTimeInstances_SameInstant() + { + // Given - Create two separate ZonedDateTime instances representing the same instant + ZonedDateTime time1 = ZonedDateTime.parse("2024-01-01T12:00:00Z"); + ZonedDateTime time2 = ZonedDateTime.parse("2024-01-01T12:00:00Z"); + + WeatherForecastKey key1 = WeatherForecastKey.valueOf(WeatherStation.APOLDA, time1, WeatherDataSource.OPENMETEO); + WeatherForecastKey key2 = WeatherForecastKey.valueOf(WeatherStation.APOLDA, time2, WeatherDataSource.OPENMETEO); + + // Then - They should be equal even though they are different object instances + assertEquals(key1, key2); + assertEquals(key1.hashCode(), key2.hashCode()); + } + + @Test + void testEqualityWithDifferentTimeZones_ButSameTime() + { + // Given - Same instant but different time zones + ZonedDateTime timeUTC = ZonedDateTime.parse("2024-01-01T12:00:00Z"); + ZonedDateTime timePlusOne = ZonedDateTime.parse("2024-01-01T13:00:00+01:00"); + + WeatherForecastKey key1 = WeatherForecastKey.valueOf(WeatherStation.APOLDA, timeUTC, WeatherDataSource.OPENMETEO); + WeatherForecastKey key2 = WeatherForecastKey.valueOf(WeatherStation.APOLDA, timePlusOne, WeatherDataSource.OPENMETEO); + + // Then - They should be equal (because it's at the same moment in time) + assertEquals(key1, key2); + } + + @Test + void testEqualityWithDifferentInstants() + { + // Given + ZonedDateTime time1 = ZonedDateTime.parse("2024-01-01T12:00:00Z"); + ZonedDateTime time2 = ZonedDateTime.parse("2024-01-01T13:00:00Z"); + + WeatherForecastKey key1 = WeatherForecastKey.valueOf(WeatherStation.APOLDA, time1, WeatherDataSource.OPENMETEO); + WeatherForecastKey key2 = WeatherForecastKey.valueOf(WeatherStation.APOLDA, time2, WeatherDataSource.OPENMETEO); + + // Then + assertNotEquals(key1, key2); + } + + @Test + void testEqualityWithDifferentStations() + { + // Given + ZonedDateTime time = ZonedDateTime.parse("2024-01-01T12:00:00Z"); + WeatherForecastKey key1 = WeatherForecastKey.valueOf(WeatherStation.APOLDA, time, WeatherDataSource.OPENMETEO); + WeatherForecastKey key2 = WeatherForecastKey.valueOf(WeatherStation.LEIPZIG_STADTWERKE, time, WeatherDataSource.OPENMETEO); + + // Then + assertNotEquals(key1, key2); + } + + @Test + void testEqualityWithDifferentSources() + { + // Given + ZonedDateTime time = ZonedDateTime.parse("2024-01-01T12:00:00Z"); + WeatherForecastKey key1 = WeatherForecastKey.valueOf(WeatherStation.APOLDA, time, WeatherDataSource.OPENMETEO); + WeatherForecastKey key2 = WeatherForecastKey.valueOf(WeatherStation.APOLDA, time, WeatherDataSource.TEST); + + // Then + assertNotEquals(key1, key2); + } + + @Test + void testAsMapKey() + { + // Given - Simulate how it's used in MemoryWeatherForecastDao + Map map = new HashMap<>(); + + ZonedDateTime time = ZonedDateTime.parse("2024-01-01T12:00:00Z"); + WeatherForecastKey key1 = WeatherForecastKey.valueOf(WeatherStation.APOLDA, time, WeatherDataSource.OPENMETEO); + + map.put(key1, "test-data"); + + // When - Create a new key with same values but different ZonedDateTime instance + ZonedDateTime time2 = ZonedDateTime.parse("2024-01-01T12:00:00Z"); + WeatherForecastKey key2 = WeatherForecastKey.valueOf(WeatherStation.APOLDA, time2, WeatherDataSource.OPENMETEO); + + // Then - Should find the value using the new key + assertTrue(map.containsKey(key2)); + assertEquals("test-data", map.get(key2)); + } + + @Test + void testAsMapKey_WithParsingFromString() + { + // Store with a ZonedDateTime created one way + ZonedDateTime storedTime = ZonedDateTime.now().toLocalDate().atStartOfDay(ZoneId.of("UTC")).plusHours(1); + WeatherForecastKey storedKey = WeatherForecastKey.valueOf(WeatherStation.APOLDA, storedTime, WeatherDataSource.OPENMETEO); + + // Retrieve with a ZonedDateTime created from parsing (like from API) + ZonedDateTime parsedTime = ZonedDateTime.parse(storedTime.format(DateTimeFormatter.ISO_ZONED_DATE_TIME), DateTimeFormatter.ISO_ZONED_DATE_TIME); + assertEquals(storedTime, parsedTime, "ZonedDateTime instances should be equal"); + + WeatherForecastKey retrieveKey = WeatherForecastKey.valueOf(WeatherStation.APOLDA, parsedTime, WeatherDataSource.OPENMETEO); + + // Then - Should find the value + assertEquals(storedKey, retrieveKey, "Keys should be equal"); + } + + @Test + void testAsMapKey_WithAddingZ() + { + // Given - This simulates the OpenMeteo parsing scenario + Map map = new HashMap<>(); + + // Store with standard UTC time + ZonedDateTime storedTime = ZonedDateTime.parse("2024-01-01T12:00:00Z", DateTimeFormatter.ISO_OFFSET_DATE_TIME); + WeatherForecastKey storedKey = WeatherForecastKey.valueOf(WeatherStation.APOLDA, storedTime, WeatherDataSource.OPENMETEO); + map.put(storedKey, "test-data"); + + // Retrieve by parsing time string with Z appended (like OpenMeteo does) + String timeWithoutZ = "2024-01-01T12:00:00"; + ZonedDateTime parsedTime = ZonedDateTime.parse(timeWithoutZ + "Z", DateTimeFormatter.ISO_OFFSET_DATE_TIME); + WeatherForecastKey retrieveKey = WeatherForecastKey.valueOf(WeatherStation.APOLDA, parsedTime, WeatherDataSource.OPENMETEO); + + // Then - Should find the value + assertEquals(storedTime, parsedTime, "ZonedDateTime instances should be equal"); + assertEquals(storedKey, retrieveKey, "Keys should be equal"); + assertTrue(map.containsKey(retrieveKey), "Map should contain the key"); + assertEquals("test-data", map.get(retrieveKey)); + } + + @Test + void testValueOfFactory() + { + // Given + ZonedDateTime time = ZonedDateTime.parse("2024-01-01T12:00:00Z"); + + // When + WeatherForecastKey key = WeatherForecastKey.valueOf(WeatherStation.APOLDA, time, WeatherDataSource.OPENMETEO); + + // Then + assertEquals(WeatherStation.APOLDA, key.station()); + assertEquals(time.toEpochSecond(), key.time()); + assertEquals(WeatherDataSource.OPENMETEO, key.source()); + } + + @Test + void testValueOfFactory_Equality() + { + // Given + ZonedDateTime time = ZonedDateTime.parse("2024-01-01T12:00:00Z"); + + // When + WeatherForecastKey key1 = WeatherForecastKey.valueOf(WeatherStation.APOLDA, time, WeatherDataSource.OPENMETEO); + WeatherForecastKey key2 = WeatherForecastKey.valueOf(WeatherStation.APOLDA, time, WeatherDataSource.OPENMETEO); + + // Then + assertEquals(key1, key2); + assertEquals(key1.hashCode(), key2.hashCode()); + } +} + diff --git a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java index fa442bc..3823434 100644 --- a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java +++ b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java @@ -15,6 +15,8 @@ import jakarta.inject.Inject; import jakarta.inject.Singleton; +import java.time.Instant; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -62,8 +64,8 @@ public void update(WeatherForecastKey key, WeatherForecastPO data) try { Point point = Point.measurement(WEATHER_MEASUREMENT) - .time(key.time().toInstant(), WritePrecision.S) - .addTag(COLUMN_STATION, key.station()) + .time(getInstantOf(key.time()), WritePrecision.S) + .addTag(COLUMN_STATION, key.station().name()) .addTag(COLUMN_SOURCE, key.source().getIdentifier()) .addField(WeatherForecastPO.COLUMN_SUN_PROBABILITY, data.getSunProbability().value()); @@ -75,13 +77,23 @@ public void update(WeatherForecastKey key, WeatherForecastPO data) } } + Instant getInstantOf(long time) + { + return Instant.ofEpochMilli(time); + } + + ZonedDateTime getZonedOf(long time) + { + return getInstantOf(time).atZone(ZoneId.of("UTC")); + } + @Override public void delete(WeatherForecastKey key) { try { // Delete data within a 1-minute window around the specified time - deleteApi.delete(key.time().minusMinutes(1).toOffsetDateTime(), key.time().plusMinutes(1).toOffsetDateTime(), + deleteApi.delete(getZonedOf(key.time()).minusMinutes(1).toOffsetDateTime(), getZonedOf(key.time()).plusMinutes(1).toOffsetDateTime(), DELETE_PREDICATE.formatted(WEATHER_MEASUREMENT, key.station(), key.source().getIdentifier()), bucketName, influxdbOrg); } catch(Exception e) @@ -98,8 +110,8 @@ private String formatTimeForFlux(ZonedDateTime time) @Override public WeatherForecastPO get(WeatherForecastKey key) { - String flux = String.format(GET_QUERY, bucketName, formatTimeForFlux(key.time().minusMinutes(1)), - formatTimeForFlux(key.time().plusMinutes(1)), WEATHER_MEASUREMENT, key.station(), key.source().getIdentifier(), WeatherForecastPO.COLUMN_SUN_PROBABILITY); + String flux = String.format(GET_QUERY, bucketName, formatTimeForFlux(getZonedOf(key.time()).minusMinutes(1)), + formatTimeForFlux(getZonedOf(key.time()).plusMinutes(1)), WEATHER_MEASUREMENT, key.station(), key.source().getIdentifier(), WeatherForecastPO.COLUMN_SUN_PROBABILITY); for (FluxTable table : queryApi.query(flux)) { for (FluxRecord record : table.getRecords()) diff --git a/persistence-memory/README.md b/persistence-memory/README.md new file mode 100644 index 0000000..d044e5e --- /dev/null +++ b/persistence-memory/README.md @@ -0,0 +1,77 @@ +# Persistence Memory Module + +This module provides in-memory implementations of the Data Access Objects (DAOs) defined in `persistence-api`. + +## Purpose + +The primary purpose of this module is to enable testing without requiring external dependencies like InfluxDB. It's particularly useful for: + +- **CI/CD pipelines** (e.g., GitHub Actions) where you want fast tests without spinning up database services +- **Unit tests** where you want complete control over the data +- **Local development** when you don't want to run InfluxDB + +## Implementation + +The module provides thread-safe, in-memory implementations using `ConcurrentHashMap`: + +- `MemoryWeatherForecastDao` - implements `WeatherForcastDAO` +- `MemoryEnergyStockDao` - implements `EnergyStockDao` + +### Key Design Decisions + +1. **No Defensive Copying**: The implementations store and return PO objects directly without copying. This is safe because: + - The value types (`Money`, `Percentage`) are immutable records + - Even if callers modify the PO objects themselves (via setters), the contained values remain immutable + - This approach is simpler and more performant than defensive copying + +2. **Thread Safety**: Uses `ConcurrentHashMap` for thread-safe access to the storage + +3. **Test Utilities**: Each DAO provides a `clear()` method to reset state between tests + +## Usage + +### In Tests + +Add as a test dependency in your module's `build.gradle.kts`: + +```kotlin +dependencies { + // ... + testImplementation(project(":persistence-memory")) +} +``` + +### Example + +```java +@Test +void testWeatherForecast() { + MemoryWeatherForecastDao dao = new MemoryWeatherForecastDao(); + + WeatherForecastKey key = WeatherForecastKey.valueOf( + WeatherStation.APOLDA, + ZonedDateTime.now(), + WeatherDataSource.OPENMETEO + ); + + WeatherForecastPO data = new WeatherForecastPO(); + data.setSunProbability(new Percentage(0.75)); + + dao.update(key, data); + + WeatherForecastPO result = dao.get(key); + assertEquals(0.75, result.getSunProbability().value(), 0.001); + + dao.clear(); // Clean up for next test +} +``` + +## Benefits for CI/CD + +With this module, GitHub Actions workflows no longer need to: +- Start InfluxDB service containers +- Wait for database initialization +- Configure database connections + +This makes tests faster and more reliable in CI environments. + diff --git a/persistence-memory/build.gradle.kts b/persistence-memory/build.gradle.kts new file mode 100644 index 0000000..c091e05 --- /dev/null +++ b/persistence-memory/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + `java-library` +} + +dependencies { + implementation(project(":units")) + implementation(project(":persistence-api")) + + implementation("org.slf4j:slf4j-api") + implementation("jakarta.inject:jakarta.inject-api") + + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + diff --git a/persistence-memory/src/main/java/de/hatoka/eos/persistence/memory/MemoryEnergyStockDao.java b/persistence-memory/src/main/java/de/hatoka/eos/persistence/memory/MemoryEnergyStockDao.java new file mode 100644 index 0000000..5af77ae --- /dev/null +++ b/persistence-memory/src/main/java/de/hatoka/eos/persistence/memory/MemoryEnergyStockDao.java @@ -0,0 +1,44 @@ +package de.hatoka.eos.persistence.memory; + +import de.hatoka.eos.persistence.capi.energystock.EnergyStockDao; +import de.hatoka.eos.persistence.capi.energystock.EnergyStockKey; +import de.hatoka.eos.persistence.capi.energystock.EnergyStockPO; +import jakarta.inject.Singleton; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * In-memory implementation for EnergyStockDao + */ +@Singleton +public class MemoryEnergyStockDao implements EnergyStockDao +{ + private static final ConcurrentHashMap STORAGE = new ConcurrentHashMap<>(); + + @Override + public void update(EnergyStockKey key, EnergyStockPO data) + { + STORAGE.put(key, data); + } + + @Override + public void delete(EnergyStockKey key) + { + STORAGE.remove(key); + } + + @Override + public EnergyStockPO get(EnergyStockKey key) + { + return STORAGE.get(key); + } + + /** + * Clear all data (useful for testing) + */ + public void clear() + { + STORAGE.clear(); + } +} + diff --git a/persistence-memory/src/main/java/de/hatoka/eos/persistence/memory/MemoryWeatherForecastDao.java b/persistence-memory/src/main/java/de/hatoka/eos/persistence/memory/MemoryWeatherForecastDao.java new file mode 100644 index 0000000..e2340de --- /dev/null +++ b/persistence-memory/src/main/java/de/hatoka/eos/persistence/memory/MemoryWeatherForecastDao.java @@ -0,0 +1,47 @@ +package de.hatoka.eos.persistence.memory; + +import de.hatoka.eos.persistence.capi.weather.WeatherForcastDAO; +import de.hatoka.eos.persistence.capi.weather.WeatherForecastKey; +import de.hatoka.eos.persistence.capi.weather.WeatherForecastPO; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * In-memory implementation for WeatherForecastDAO + */ +@Singleton +public class MemoryWeatherForecastDao implements WeatherForcastDAO +{ + private static final Map STORAGE = new ConcurrentHashMap<>(); + + @Override + public void update(WeatherForecastKey key, WeatherForecastPO data) + { + STORAGE.put(key, data); + } + + @Override + public void delete(WeatherForecastKey key) + { + STORAGE.remove(key); + } + + @Override + public WeatherForecastPO get(WeatherForecastKey key) + { + return STORAGE.get(key); + } + + /** + * Clear all data (useful for testing) + */ + public void clear() + { + STORAGE.clear(); + } +} + diff --git a/persistence-memory/src/main/resources/META-INF/beans.xml b/persistence-memory/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..2d69bfb --- /dev/null +++ b/persistence-memory/src/main/resources/META-INF/beans.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/persistence-memory/src/test/java/de/hatoka/eos/persistence/memory/MemoryEnergyStockDaoTest.java b/persistence-memory/src/test/java/de/hatoka/eos/persistence/memory/MemoryEnergyStockDaoTest.java new file mode 100644 index 0000000..e34d9e3 --- /dev/null +++ b/persistence-memory/src/test/java/de/hatoka/eos/persistence/memory/MemoryEnergyStockDaoTest.java @@ -0,0 +1,91 @@ +package de.hatoka.eos.persistence.memory; + +import de.hatoka.eos.persistence.capi.energystock.EnergyStockKey; +import de.hatoka.eos.persistence.capi.energystock.EnergyStockPO; +import de.hatoka.eos.units.capi.Money; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +class MemoryEnergyStockDaoTest +{ + private MemoryEnergyStockDao dao; + + @BeforeEach + void setUp() + { + dao = new MemoryEnergyStockDao(); + } + + @Test + void testUpdateAndGet() + { + // Given + EnergyStockKey key = EnergyStockKey.valueOf(ZonedDateTime.now()); + EnergyStockPO data = new EnergyStockPO(); + data.setDayAheadPrice(Money.ofEur(0.12)); + + // When + dao.update(key, data); + EnergyStockPO result = dao.get(key); + + // Then + assertNotNull(result); + assertEquals(0.12, result.getDayAheadPrice().amount().doubleValue(), 0.001); + assertEquals("EUR", result.getDayAheadPrice().currencyMnemonic()); + } + + @Test + void testGetNonExistent() + { + // Given + EnergyStockKey key = EnergyStockKey.valueOf(ZonedDateTime.now()); + + // When + EnergyStockPO result = dao.get(key); + + // Then + assertNull(result); + } + + @Test + void testDelete() + { + // Given + EnergyStockKey key = EnergyStockKey.valueOf(ZonedDateTime.now()); + EnergyStockPO data = new EnergyStockPO(); + data.setDayAheadPrice(Money.ofEur(0.12)); + dao.update(key, data); + + // When + dao.delete(key); + EnergyStockPO result = dao.get(key); + + // Then + assertNull(result); + } + + @Test + void testClear() + { + // Given + EnergyStockKey key1 = EnergyStockKey.valueOf(ZonedDateTime.now()); + EnergyStockKey key2 = EnergyStockKey.valueOf(ZonedDateTime.now().plusHours(1)); + EnergyStockPO data = new EnergyStockPO(); + data.setDayAheadPrice(Money.ofEur(0.12)); + dao.update(key1, data); + dao.update(key2, data); + + // When + dao.clear(); + + // Then + assertNull(dao.get(key1)); + assertNull(dao.get(key2)); + } +} + diff --git a/persistence-memory/src/test/java/de/hatoka/eos/persistence/memory/MemoryWeatherForecastDaoTest.java b/persistence-memory/src/test/java/de/hatoka/eos/persistence/memory/MemoryWeatherForecastDaoTest.java new file mode 100644 index 0000000..74bffa1 --- /dev/null +++ b/persistence-memory/src/test/java/de/hatoka/eos/persistence/memory/MemoryWeatherForecastDaoTest.java @@ -0,0 +1,111 @@ +package de.hatoka.eos.persistence.memory; + +import de.hatoka.eos.persistence.capi.weather.WeatherDataSource; +import de.hatoka.eos.persistence.capi.weather.WeatherForecastKey; +import de.hatoka.eos.persistence.capi.weather.WeatherForecastPO; +import de.hatoka.eos.persistence.capi.weather.WeatherStation; +import de.hatoka.eos.units.capi.Percentage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +class MemoryWeatherForecastDaoTest +{ + private MemoryWeatherForecastDao dao; + + @BeforeEach + void setUp() + { + dao = new MemoryWeatherForecastDao(); + } + + @Test + void testUpdateAndGet() + { + // Given + WeatherForecastKey key = WeatherForecastKey.valueOf( + WeatherStation.APOLDA, + ZonedDateTime.now(), + WeatherDataSource.OPENMETEO + ); + WeatherForecastPO data = new WeatherForecastPO(); + data.setSunProbability(new Percentage(0.755)); + + // When + dao.update(key, data); + WeatherForecastPO result = dao.get(key); + + // Then + assertNotNull(result); + assertEquals(0.755, result.getSunProbability().value(), 0.001); + } + + @Test + void testGetNonExistent() + { + // Given + WeatherForecastKey key = WeatherForecastKey.valueOf( + WeatherStation.APOLDA, + ZonedDateTime.now(), + WeatherDataSource.OPENMETEO + ); + + // When + WeatherForecastPO result = dao.get(key); + + // Then + assertNull(result); + } + + @Test + void testDelete() + { + // Given + WeatherForecastKey key = WeatherForecastKey.valueOf( + WeatherStation.APOLDA, + ZonedDateTime.now(), + WeatherDataSource.OPENMETEO + ); + WeatherForecastPO data = new WeatherForecastPO(); + data.setSunProbability(new Percentage(0.755)); + dao.update(key, data); + + // When + dao.delete(key); + WeatherForecastPO result = dao.get(key); + + // Then + assertNull(result); + } + + @Test + void testClear() + { + // Given + WeatherForecastKey key1 = WeatherForecastKey.valueOf( + WeatherStation.APOLDA, + ZonedDateTime.now(), + WeatherDataSource.OPENMETEO + ); + WeatherForecastKey key2 = WeatherForecastKey.valueOf( + WeatherStation.APOLDA, + ZonedDateTime.now().plusHours(1), + WeatherDataSource.OPENMETEO + ); + WeatherForecastPO data = new WeatherForecastPO(); + data.setSunProbability(new Percentage(0.755)); + dao.update(key1, data); + dao.update(key2, data); + + // When + dao.clear(); + + // Then + assertNull(dao.get(key1)); + assertNull(dao.get(key2)); + } +} + diff --git a/settings.gradle b/settings.gradle index cc3f717..9fe92ad 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,6 +16,7 @@ include 'versions-test' include 'units' include 'persistence-api' include 'persistence-influx' +include 'persistence-memory' include 'simulation' include 'optimization' include 'forecast' diff --git a/simulation/build.gradle.kts b/simulation/build.gradle.kts index 4955fa1..03c5f5e 100644 --- a/simulation/build.gradle.kts +++ b/simulation/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { // InfluxDB client for Grafana visualization implementation("com.influxdb:influxdb-client-java") + testImplementation(project(":persistence-memory")) testImplementation("io.quarkus:quarkus-junit5") testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/InstallationConfig.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/InstallationConfig.java index 89608c5..57739c5 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/InstallationConfig.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/InstallationConfig.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import de.hatoka.eos.persistence.capi.weather.WeatherStation; import java.util.List; @@ -19,6 +20,10 @@ public class InstallationConfig @JsonPropertyDescription("Grid connection configuration including import/export pricing") private GridConfig grid; + @JsonProperty("weatherStation") + @JsonPropertyDescription("Weather station to use for weather forecasts (APOLDA, LEIPZIG_STADTWERKE)") + private WeatherStation weatherStation = WeatherStation.APOLDA; // default value + public List getDevices() { return devices; @@ -38,4 +43,14 @@ public void setGrid(GridConfig grid) { this.grid = grid; } + + public WeatherStation getWeatherStation() + { + return weatherStation; + } + + public void setWeatherStation(WeatherStation weatherStation) + { + this.weatherStation = weatherStation; + } } \ No newline at end of file diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoEnergyPriceForecast.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoEnergyPriceForecast.java index bbdedb1..e46b7a6 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoEnergyPriceForecast.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoEnergyPriceForecast.java @@ -2,7 +2,6 @@ import de.hatoka.eos.persistence.capi.energystock.EnergyStockDao; import de.hatoka.eos.persistence.capi.energystock.EnergyStockKey; -import de.hatoka.eos.simulation.capi.business.config.GridConfig; import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; import de.hatoka.eos.units.capi.Money; import jakarta.inject.Inject; @@ -15,8 +14,6 @@ public class DaoEnergyPriceForecast implements EnergyPriceForecast { @Inject private EnergyStockDao stockDao; - @Inject - private GridConfig config; @Override public Money getImportPrice(ZonedDateTime time) diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoWeatherForecast.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoWeatherForecast.java deleted file mode 100644 index 29b9157..0000000 --- a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoWeatherForecast.java +++ /dev/null @@ -1,32 +0,0 @@ -package de.hatoka.eos.simulation.internal.business.forecast; - -import de.hatoka.eos.persistence.capi.weather.WeatherDataSource; -import de.hatoka.eos.persistence.capi.weather.WeatherForcastDAO; -import de.hatoka.eos.persistence.capi.weather.WeatherForecastKey; -import de.hatoka.eos.simulation.capi.business.forecast.WeatherForecast; -import de.hatoka.eos.units.capi.Percentage; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; - -import java.time.ZonedDateTime; - -/** - * Weather forecast implementation that reads from InfluxDB via ForecastDAO. - */ -@Singleton -public class DaoWeatherForecast implements WeatherForecast -{ - private final String station; - @Inject - private WeatherForcastDAO forecastDAO; - - public DaoWeatherForecast(String station) - { - this.station = station; - } - @Override - public Percentage getSunProbability(ZonedDateTime time) - { - return forecastDAO.get(new WeatherForecastKey(station, time, WeatherDataSource.METEOMEDIA)).getSunProbability(); - } -} diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/EnergyPriceForecastFactory.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/EnergyPriceForecastFactory.java index 22cb2b8..4ee73fc 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/EnergyPriceForecastFactory.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/EnergyPriceForecastFactory.java @@ -12,15 +12,13 @@ public class EnergyPriceForecastFactory @Inject private EnergyStockDao dao; - @Inject - private GridConfig gridConfig; - @Inject private DaoEnergyPriceForecast daoEnergyPriceForecast; + /** * @return the configured price forecast */ - public EnergyPriceForecast getEnergyPriceProvider() + public EnergyPriceForecast getEnergyPriceProvider(GridConfig gridConfig) { return switch(gridConfig.type()) { diff --git a/simulation/src/test/resources/application.properties b/simulation/src/test/resources/application.properties index c260bcc..8f94cf8 100644 --- a/simulation/src/test/resources/application.properties +++ b/simulation/src/test/resources/application.properties @@ -1,7 +1,10 @@ quarkus.profile=production -# InfluxDB Metrics Configuration -eos.metrics.influxdb.enabled=true +# Exclude InfluxDB implementations from CDI, use in-memory persistence for tests +quarkus.arc.exclude-types=de.hatoka.eos.persistence.influx.** + +# InfluxDB Metrics Configuration (not used with in-memory persistence) +eos.metrics.influxdb.enabled=false eos.metrics.influxdb.url=http://localhost:8086 eos.metrics.influxdb.token=eos-token-12345678901234567890 eos.metrics.influxdb.org=eos diff --git a/simulation/src/test/resources/test-installation-with-car.yaml b/simulation/src/test/resources/test-installation-with-car.yaml index b4b6792..1b494ed 100644 --- a/simulation/src/test/resources/test-installation-with-car.yaml +++ b/simulation/src/test/resources/test-installation-with-car.yaml @@ -45,6 +45,10 @@ devices: - type: GRID count: 1 +# Weather station configuration for forecasts +# Available stations: APOLDA, LEIPZIG_STADTWERKE +weatherStation: APOLDA + grid: type: "FLAT" flatPriceProvider: diff --git a/simulation/src/test/resources/test-installation-without-car.yaml b/simulation/src/test/resources/test-installation-without-car.yaml index b1541fb..cc00f91 100644 --- a/simulation/src/test/resources/test-installation-without-car.yaml +++ b/simulation/src/test/resources/test-installation-without-car.yaml @@ -31,6 +31,10 @@ devices: - type: GRID count: 1 +# Weather station configuration for forecasts +# Available stations: APOLDA, LEIPZIG_STADTWERKE +weatherStation: APOLDA + grid: type: "FLAT" flatPriceProvider: From 032d96d1a885a987729cde42ac49042cc89068df Mon Sep 17 00:00:00 2001 From: Thomas Bergmann Date: Mon, 29 Dec 2025 16:39:37 +0100 Subject: [PATCH 12/16] add csv for tests again --- .../capi/business/config/CsvPriceConfig.java | 30 +++++++++++++++++++ .../capi/business/config/GridConfig.java | 6 +++- .../business/config/GridPriceConfigType.java | 2 +- .../forecast/EnergyPriceForecastFactory.java | 1 + 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/CsvPriceConfig.java diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/CsvPriceConfig.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/CsvPriceConfig.java new file mode 100644 index 0000000..66d190c --- /dev/null +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/CsvPriceConfig.java @@ -0,0 +1,30 @@ +package de.hatoka.eos.simulation.capi.business.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; + +import java.util.List; + +/** + * The CsvPriceConfig declares, how stock prices are provided via CSV files + * @param currency currency of stock price + * @param exportCharge additional charge for exporting energy to the grid + * @param importCharge additional charge for importing energy from grid + */ +public record CsvPriceConfig( + @JsonProperty("currency") + @JsonPropertyDescription("Currency code for CSV price data (e.g., EUR, USD)") + String currency, + + @JsonProperty("exportCharge") + @JsonPropertyDescription("Export charge configuration (price per energy unit)") + ImportExportChargeConfig exportCharge, + + @JsonProperty("importCharge") + @JsonPropertyDescription("Import charge configuration (price per energy unit)") + ImportExportChargeConfig importCharge, + + @JsonProperty("resource") + @JsonPropertyDescription("List of CSV resource files containing price data") + List resource +) { } \ No newline at end of file diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridConfig.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridConfig.java index 9e23ac2..46e5084 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridConfig.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridConfig.java @@ -19,7 +19,11 @@ public record GridConfig ( @JsonProperty("flatPriceProvider") @JsonPropertyDescription("Flat price provider configuration") - FlatPriceConfig flatPriceConfig + FlatPriceConfig flatPriceConfig, + + @JsonProperty("csvPriceProvider") + @JsonPropertyDescription("CSV price provider configuration") + CsvPriceConfig csvPriceConfig ) { } \ No newline at end of file diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridPriceConfigType.java b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridPriceConfigType.java index 1e7d3a5..237cb03 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridPriceConfigType.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/capi/business/config/GridPriceConfigType.java @@ -5,5 +5,5 @@ */ public enum GridPriceConfigType { - FLAT, DAO + FLAT, DAO, CSV } diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/EnergyPriceForecastFactory.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/EnergyPriceForecastFactory.java index 4ee73fc..c0479b6 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/EnergyPriceForecastFactory.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/EnergyPriceForecastFactory.java @@ -24,6 +24,7 @@ public EnergyPriceForecast getEnergyPriceProvider(GridConfig gridConfig) { case DAO -> daoEnergyPriceForecast; case FLAT -> new FlatPriceService(gridConfig.flatPriceConfig()); + case CSV -> daoEnergyPriceForecast; }; } From 5462ee9722cad935c0ae5f6886c6a9807dafafad Mon Sep 17 00:00:00 2001 From: Thomas Bergmann Date: Fri, 2 Jan 2026 13:05:26 +0100 Subject: [PATCH 13/16] extract: metrics --- .../src/test/resources/application.properties | 5 + forecast/src/test/resources/application.yml | 13 --- metrics-influx/build.gradle.kts | 19 ++++ .../influx}/InfluxDBMetricsExporter.java | 103 ++++-------------- .../src/main/resources/META-INF/beans.xml | 1 + .../SimulationMetricsExporterTest.java | 13 +-- .../src/test/resources/application.properties | 8 ++ metrics-memory/build.gradle.kts | 16 +++ .../memory/MemorySimulationExporter.java | 17 +++ .../src/main/resources/META-INF/beans.xml | 2 + optimization/build.gradle.kts | 4 +- .../src/test/resources/application.properties | 10 -- persistence-influx/build.gradle.kts | 1 - .../influx/{ => config}/InfluxDBConfig.java | 21 ++-- .../{ => dao}/InfluxEnergyStockDao.java | 34 +++--- .../{ => dao}/InfluxWeatherForecastDao.java | 19 ++-- .../{ => dao}/InfluxEnergyStockDaoTest.java | 2 +- .../InfluxWeatherForecastDaoTest.java | 2 +- .../src/test/resources/application.properties | 8 ++ .../src/test/resources/application.yml | 8 -- settings.gradle | 3 +- simulation/build.gradle.kts | 5 +- .../forecast/EnergyPriceForecastFactory.java | 31 ------ 23 files changed, 141 insertions(+), 204 deletions(-) create mode 100644 forecast/src/test/resources/application.properties delete mode 100644 forecast/src/test/resources/application.yml create mode 100644 metrics-influx/build.gradle.kts rename {simulation/src/main/java/de/hatoka/eos/simulation/internal/business/metrics => metrics-influx/src/main/java/de/hatoka/eos/metrics/influx}/InfluxDBMetricsExporter.java (70%) create mode 100644 metrics-influx/src/main/resources/META-INF/beans.xml rename {simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics => metrics-influx/src/test/java/de/hatoka/eos/metrics/influx}/SimulationMetricsExporterTest.java (85%) create mode 100644 metrics-influx/src/test/resources/application.properties create mode 100644 metrics-memory/build.gradle.kts create mode 100644 metrics-memory/src/main/java/de/hatoka/eos/metrics/memory/MemorySimulationExporter.java create mode 100644 metrics-memory/src/main/resources/META-INF/beans.xml rename persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/{ => config}/InfluxDBConfig.java (53%) rename persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/{ => dao}/InfluxEnergyStockDao.java (79%) rename persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/{ => dao}/InfluxWeatherForecastDao.java (88%) rename persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/{ => dao}/InfluxEnergyStockDaoTest.java (99%) rename persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/{ => dao}/InfluxWeatherForecastDaoTest.java (99%) create mode 100644 persistence-influx/src/test/resources/application.properties delete mode 100644 persistence-influx/src/test/resources/application.yml delete mode 100644 simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/EnergyPriceForecastFactory.java diff --git a/forecast/src/test/resources/application.properties b/forecast/src/test/resources/application.properties new file mode 100644 index 0000000..f5df22f --- /dev/null +++ b/forecast/src/test/resources/application.properties @@ -0,0 +1,5 @@ +quarkus.log.level=INFO +quarkus.log.category."com.intershop".level=DEBUG + +# Exclude InfluxDB implementations from CDI, use in-memory persistence for tests +# quarkus.arc.exclude-types=de.hatoka.eos.persistence.influx.** diff --git a/forecast/src/test/resources/application.yml b/forecast/src/test/resources/application.yml deleted file mode 100644 index f3f899d..0000000 --- a/forecast/src/test/resources/application.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Test configuration - use in-memory persistence instead of InfluxDB -quarkus: - arc: - exclude-types: - - de.hatoka.eos.persistence.influx.** - -eos: - influxdb: - url: ${INFLUX_URI:http://localhost:8086} - token: ${INFLUX_TOKEN:test-token} - org: eos - bucket: energy_simulation - diff --git a/metrics-influx/build.gradle.kts b/metrics-influx/build.gradle.kts new file mode 100644 index 0000000..1a151d0 --- /dev/null +++ b/metrics-influx/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + `java-library` + id("io.quarkus") +} + +dependencies { + implementation(project(":units")) + implementation(project(":persistence-api")) + implementation(project(":persistence-influx")) + implementation(project(":simulation")) + + implementation("jakarta.inject:jakarta.inject-api") + implementation("io.quarkus:quarkus-arc") + + implementation("com.influxdb:influxdb-client-java") + + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("io.quarkus:quarkus-junit5") +} diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/metrics/InfluxDBMetricsExporter.java b/metrics-influx/src/main/java/de/hatoka/eos/metrics/influx/InfluxDBMetricsExporter.java similarity index 70% rename from simulation/src/main/java/de/hatoka/eos/simulation/internal/business/metrics/InfluxDBMetricsExporter.java rename to metrics-influx/src/main/java/de/hatoka/eos/metrics/influx/InfluxDBMetricsExporter.java index 0990614..2770903 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/metrics/InfluxDBMetricsExporter.java +++ b/metrics-influx/src/main/java/de/hatoka/eos/metrics/influx/InfluxDBMetricsExporter.java @@ -1,95 +1,45 @@ -package de.hatoka.eos.simulation.internal.business.metrics; +package de.hatoka.eos.metrics.influx; import com.influxdb.client.DeleteApi; import com.influxdb.client.InfluxDBClient; -import com.influxdb.client.InfluxDBClientFactory; import com.influxdb.client.WriteApi; import com.influxdb.client.domain.DeletePredicateRequest; import com.influxdb.client.domain.WritePrecision; import com.influxdb.client.write.Point; +import de.hatoka.eos.persistence.influx.config.InfluxDBConfig; import de.hatoka.eos.simulation.capi.business.device.DeviceRef; import de.hatoka.eos.simulation.capi.business.device.DeviceState; import de.hatoka.eos.simulation.capi.business.device.DeviceType; -import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; -import de.hatoka.eos.simulation.capi.business.forecast.WeatherForecast; import de.hatoka.eos.simulation.capi.business.metrics.SimulationMetricsExporter; import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; +import jakarta.inject.Inject; import jakarta.inject.Singleton; -import org.eclipse.microprofile.config.inject.ConfigProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Instant; -import java.time.ZonedDateTime; import java.util.Map; @Singleton public class InfluxDBMetricsExporter implements SimulationMetricsExporter { + private static final String BUCKET = "metrics"; private static final Logger LOGGER = LoggerFactory.getLogger(InfluxDBMetricsExporter.class); - public static final String ENERGY_FLOW = "energy_flow"; - public static final String TAG_SIMULATION = "simulation"; + private static final String MEASUREMENT = "energy_flow"; + private static final String TAG_SIMULATION = "simulation"; - @ConfigProperty(name = "eos.metrics.influxdb.url", defaultValue = "http://localhost:8086") - String influxUrl; + private final WriteApi writeApi; + private final DeleteApi deleteApi; + private final String influxDbOrg; - @ConfigProperty(name = "eos.metrics.influxdb.token") - String influxToken; - - @ConfigProperty(name = "eos.metrics.influxdb.org", defaultValue = "eos") - String influxOrg; - - @ConfigProperty(name = "eos.metrics.influxdb.bucket", defaultValue = "energy_simulation") - String influxBucket; - - @ConfigProperty(name = "eos.metrics.influxdb.enabled", defaultValue = "false") - boolean enabled; - - private InfluxDBClient influxClient; - private WriteApi writeApi; - private DeleteApi deleteAPI; - - @PostConstruct - void initialize() - { - if (enabled && influxToken != null) - { - try - { - influxClient = InfluxDBClientFactory.create(influxUrl, influxToken.toCharArray(), influxOrg, influxBucket); - writeApi = influxClient.makeWriteApi(); - deleteAPI = influxClient.getDeleteApi(); - LOGGER.info("InfluxDB metrics exporter initialized: {}", influxUrl); - } - catch(Exception e) - { - LOGGER.warn("Failed to initialize InfluxDB client: {}", e.getMessage()); - enabled = false; - } - } - else - { - LOGGER.info("InfluxDB metrics exporter disabled (enabled={}, token present={})", enabled, influxToken != null); - } - } - - @PreDestroy - void cleanup() + @Inject + InfluxDBMetricsExporter(InfluxDBConfig config) { - if (writeApi != null) - { - writeApi.flush(); - writeApi.close(); - writeApi = null; - } - if (influxClient != null) - { - influxClient.close(); - } + InfluxDBClient client = config.getClient(BUCKET); + writeApi = client.makeWriteApi(); + deleteApi = client.getDeleteApi(); + influxDbOrg = config.getOrg(); } - /** * Delete data from former simulation result * @param result former/current simulation result (with same simulation-id, from and to date) @@ -98,12 +48,12 @@ private void deleteOldData(SimulationResult result) { // https://docs.influxdata.com/influxdb/v2/write-data/delete-data/ // '_measurement="example-measurement" AND exampleTag="exampleTagValue"' - String condition = String.format("_measurement=\"%s\" AND %s=\"%s\"", quotePredicate(ENERGY_FLOW), TAG_SIMULATION, + String condition = String.format("_measurement=\"%s\" AND %s=\"%s\"", quotePredicate(MEASUREMENT), TAG_SIMULATION, quotePredicate(result.request().simulationId())); DeletePredicateRequest predicate = new DeletePredicateRequest()//.predicate(condition) .start(result.step().startDate().toOffsetDateTime()) .stop(result.step().endDate().toOffsetDateTime()); - deleteAPI.delete(predicate, influxBucket, influxOrg); + deleteApi.delete(predicate, BUCKET, influxDbOrg); } /** @@ -117,7 +67,7 @@ private String quotePredicate(String value) for (int i = 0; i < value.length(); i++) { switch (value.charAt(i)) { case '\\': - case '\"': + case '"': sb.append('\\'); default: sb.append(value.charAt(i)); @@ -126,28 +76,15 @@ private String quotePredicate(String value) return sb.toString(); } - /** - * @return true of influx db is available and writeApi could be created - */ - boolean isAvailable() - { - return enabled && influxClient != null && writeApi != null; - } - @Override public void exportMetrics(SimulationResult result) { - if (!isAvailable()) - { - LOGGER.warn("InfluxDB exporter not available, skipping metrics export"); - return; - } deleteOldData(result); try { Instant timestamp = result.step().startDate().toInstant(); // Export energy flow metrics - writeApi.writePoint(Point.measurement(ENERGY_FLOW) + writeApi.writePoint(Point.measurement(MEASUREMENT) .addTag(TAG_SIMULATION, result.request().simulationId()) .addField("produced_kwh", result.system().produced().amount()) .addField("consumed_kwh", -result.system().consumed().amount()) @@ -209,4 +146,4 @@ private void writeBattery(String simulationId, DeviceRef deviceRef, DeviceState .addField("capacity_kwh", state.maxEnergy().amount()) .time(timestamp, WritePrecision.NS)); } -} \ No newline at end of file +} diff --git a/metrics-influx/src/main/resources/META-INF/beans.xml b/metrics-influx/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..2d69bfb --- /dev/null +++ b/metrics-influx/src/main/resources/META-INF/beans.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics/SimulationMetricsExporterTest.java b/metrics-influx/src/test/java/de/hatoka/eos/metrics/influx/SimulationMetricsExporterTest.java similarity index 85% rename from simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics/SimulationMetricsExporterTest.java rename to metrics-influx/src/test/java/de/hatoka/eos/metrics/influx/SimulationMetricsExporterTest.java index bb9a3ed..655cef0 100644 --- a/simulation/src/test/java/de/hatoka/eos/simulation/internal/business/metrics/SimulationMetricsExporterTest.java +++ b/metrics-influx/src/test/java/de/hatoka/eos/metrics/influx/SimulationMetricsExporterTest.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.simulation.internal.business.metrics; +package de.hatoka.eos.metrics.influx; import de.hatoka.eos.simulation.capi.business.config.InstallationConfig; import de.hatoka.eos.simulation.capi.business.config.SimulationConfig; @@ -79,16 +79,5 @@ public void testExportSimulationMetricsToGrafana() throws IOException LOGGER.info("Grid Import: {} -> {}", result.system().imported(), result.system().importRevenue()); LOGGER.info("Grid Export: {} -> {}", result.system().exported(), result.system().exportRevenue()); LOGGER.info("Grid Net Transfer: {}", result.system().getEnergyRevenue()); - - // Export to InfluxDB/Grafana - if (metricsExporter instanceof InfluxDBMetricsExporter influxDBMetricsExporter && !influxDBMetricsExporter.isAvailable()) - { - LOGGER.warn("⚠️ InfluxDB exporter not available. Start with: docker-compose up -d"); - LOGGER.info("💡 To enable metrics export:"); - LOGGER.info(" 1. Run: docker-compose up -d"); - LOGGER.info(" 2. Wait for services to start"); - LOGGER.info(" 3. Re-run this test"); - LOGGER.info(" 4. Open Grafana: http://localhost:3000 (admin/admin123)"); - } } } diff --git a/metrics-influx/src/test/resources/application.properties b/metrics-influx/src/test/resources/application.properties new file mode 100644 index 0000000..7383804 --- /dev/null +++ b/metrics-influx/src/test/resources/application.properties @@ -0,0 +1,8 @@ +# InfluxDB Metrics Configuration +eos.influxdb.url=http://localhost:8086 +eos.influxdb.token=eos-token-12345678901234567890 +eos.influxdb.org=eos + +# Quarkus Configuration +quarkus.log.level=INFO +quarkus.log.category."de.hatoka.eos".level=DEBUG diff --git a/metrics-memory/build.gradle.kts b/metrics-memory/build.gradle.kts new file mode 100644 index 0000000..f22ea14 --- /dev/null +++ b/metrics-memory/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + `java-library` +} + +dependencies { + implementation(project(":units")) + implementation(project(":persistence-api")) + implementation(project(":simulation")) + + implementation("org.slf4j:slf4j-api") + implementation("jakarta.inject:jakarta.inject-api") + + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + diff --git a/metrics-memory/src/main/java/de/hatoka/eos/metrics/memory/MemorySimulationExporter.java b/metrics-memory/src/main/java/de/hatoka/eos/metrics/memory/MemorySimulationExporter.java new file mode 100644 index 0000000..d02b81b --- /dev/null +++ b/metrics-memory/src/main/java/de/hatoka/eos/metrics/memory/MemorySimulationExporter.java @@ -0,0 +1,17 @@ +package de.hatoka.eos.metrics.memory; + +import de.hatoka.eos.simulation.capi.business.metrics.SimulationMetricsExporter; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; +import jakarta.inject.Singleton; + +@Singleton +public class MemorySimulationExporter implements SimulationMetricsExporter +{ + + @Override + public void exportMetrics(SimulationResult result) + { + + } +} + diff --git a/metrics-memory/src/main/resources/META-INF/beans.xml b/metrics-memory/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..cc362e3 --- /dev/null +++ b/metrics-memory/src/main/resources/META-INF/beans.xml @@ -0,0 +1,2 @@ + + diff --git a/optimization/build.gradle.kts b/optimization/build.gradle.kts index b07b5ff..6ddd606 100644 --- a/optimization/build.gradle.kts +++ b/optimization/build.gradle.kts @@ -10,8 +10,7 @@ dependencies { implementation("org.slf4j:slf4j-api") - // implementation("jakarta.inject:jakarta.inject-api") - // implementation("jakarta.validation:jakarta.validation-api") + implementation("jakarta.inject:jakarta.inject-api") implementation("com.fasterxml.jackson.core:jackson-annotations") implementation("com.fasterxml.jackson.core:jackson-databind") @@ -20,6 +19,7 @@ dependencies { implementation("io.quarkus:quarkus-arc") testImplementation(project(":persistence-memory")) + testImplementation(project(":metrics-memory")) testImplementation("io.quarkus:quarkus-junit5") testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/optimization/src/test/resources/application.properties b/optimization/src/test/resources/application.properties index f564011..ba258fe 100644 --- a/optimization/src/test/resources/application.properties +++ b/optimization/src/test/resources/application.properties @@ -1,12 +1,2 @@ quarkus.log.level=INFO quarkus.log.category."com.intershop".level=DEBUG - -# Exclude InfluxDB implementations from CDI, use in-memory persistence for tests -quarkus.arc.exclude-types=de.hatoka.eos.persistence.influx.** - -# InfluxDB Metrics Configuration for tests (not used with in-memory persistence) -eos.metrics.influxdb.enabled=false -eos.metrics.influxdb.url=http://localhost:8086 -eos.metrics.influxdb.token=eos-token-12345678901234567890 -eos.metrics.influxdb.org=eos -eos.metrics.influxdb.bucket=energy_simulation diff --git a/persistence-influx/build.gradle.kts b/persistence-influx/build.gradle.kts index 5b6a9cb..78d6700 100644 --- a/persistence-influx/build.gradle.kts +++ b/persistence-influx/build.gradle.kts @@ -9,7 +9,6 @@ dependencies { implementation("jakarta.inject:jakarta.inject-api") implementation("io.quarkus:quarkus-arc") - implementation("io.quarkus:quarkus-config-yaml") implementation("com.influxdb:influxdb-client-java") diff --git a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxDBConfig.java b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/config/InfluxDBConfig.java similarity index 53% rename from persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxDBConfig.java rename to persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/config/InfluxDBConfig.java index 67e4e77..1487f51 100644 --- a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxDBConfig.java +++ b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/config/InfluxDBConfig.java @@ -1,8 +1,7 @@ -package de.hatoka.eos.persistence.influx; +package de.hatoka.eos.persistence.influx.config; import com.influxdb.client.InfluxDBClient; import com.influxdb.client.InfluxDBClientFactory; -import jakarta.enterprise.inject.Produces; import jakarta.inject.Singleton; import org.eclipse.microprofile.config.inject.ConfigProperty; @@ -13,21 +12,21 @@ public class InfluxDBConfig { @ConfigProperty(name = "eos.influxdb.url", defaultValue = "http://localhost:8086") - String influxdbUrl; + private String influxUrl; @ConfigProperty(name = "eos.influxdb.token", defaultValue = "") - String influxdbToken; + private String influxToken; @ConfigProperty(name = "eos.influxdb.org", defaultValue = "eos") - String influxdbOrg; + private String influxOrg; - @ConfigProperty(name = "eos.influxdb.bucket", defaultValue = "forecast") - String influxdbBucket; + public InfluxDBClient getClient(String bucket) + { + return InfluxDBClientFactory.create(influxUrl, influxToken.toCharArray(), influxOrg, bucket); + } - @Produces - @Singleton - public InfluxDBClient influxDBClient() + public String getOrg() { - return InfluxDBClientFactory.create(influxdbUrl, influxdbToken.toCharArray(), influxdbOrg, influxdbBucket); + return influxOrg; } } diff --git a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxEnergyStockDao.java b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxEnergyStockDao.java similarity index 79% rename from persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxEnergyStockDao.java rename to persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxEnergyStockDao.java index f443c27..d8c7238 100644 --- a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxEnergyStockDao.java +++ b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxEnergyStockDao.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.persistence.influx; +package de.hatoka.eos.persistence.influx.dao; import com.influxdb.client.DeleteApi; import com.influxdb.client.InfluxDBClient; @@ -8,12 +8,11 @@ import com.influxdb.client.write.Point; import com.influxdb.query.FluxRecord; import com.influxdb.query.FluxTable; +import de.hatoka.eos.persistence.influx.config.InfluxDBConfig; import de.hatoka.eos.persistence.capi.energystock.EnergyStockDao; import de.hatoka.eos.persistence.capi.energystock.EnergyStockKey; import de.hatoka.eos.persistence.capi.energystock.EnergyStockPO; -import de.hatoka.eos.persistence.capi.weather.WeatherForecastPO; import de.hatoka.eos.units.capi.Money; -import de.hatoka.eos.units.capi.Percentage; import jakarta.inject.Inject; import jakarta.inject.Singleton; @@ -27,32 +26,33 @@ @Singleton public class InfluxEnergyStockDao implements EnergyStockDao { + private static final String BUCKET = "forecast"; private static final String MEASUREMENT = "energystock_forecast"; - public static final String GET_QUERY = """ + private static final String TAG_CURRENCY = "currency"; + private static final String GET_QUERY = """ from(bucket: "%s") |> range(start: %s, stop: %s) |> filter(fn: (r) => r["_measurement"] == "%s") |> filter(fn: (r) => r["_field"] == "%s") |> last() """; - public static final String DELETE_PREDICATE = """ + private static final String DELETE_PREDICATE = """ _measurement="%s" """; private final WriteApiBlocking writeApi; private final DeleteApi deleteApi; private final QueryApi queryApi; - private final String bucketName; - private final String influxdbOrg; + private final String influxDbOrg; @Inject - InfluxEnergyStockDao(InfluxDBClient influxDBClient, InfluxDBConfig config) + InfluxEnergyStockDao(InfluxDBConfig config) { - writeApi = influxDBClient.getWriteApiBlocking(); - deleteApi = influxDBClient.getDeleteApi(); - queryApi = influxDBClient.getQueryApi(); - bucketName = config.influxdbBucket; - influxdbOrg = config.influxdbOrg; + InfluxDBClient client = config.getClient(BUCKET); + writeApi = client.getWriteApiBlocking(); + deleteApi = client.getDeleteApi(); + queryApi = client.getQueryApi(); + influxDbOrg = config.getOrg(); } @Override @@ -62,7 +62,7 @@ public void update(EnergyStockKey key, EnergyStockPO data) { Point point = Point.measurement(MEASUREMENT) .time(key.time().toInstant(), WritePrecision.S) - .addTag("currency", data.getDayAheadPrice().currencyMnemonic()) + .addTag(TAG_CURRENCY, data.getDayAheadPrice().currencyMnemonic()) .addField(EnergyStockPO.COLUMN_DAY_AHEAD, data.getDayAheadPrice().amount().doubleValue()); writeApi.writePoint(point); @@ -80,7 +80,7 @@ public void delete(EnergyStockKey key) { // Delete data within a 1-minute window around the specified time deleteApi.delete(key.time().minusMinutes(1).toOffsetDateTime(), key.time().plusMinutes(1).toOffsetDateTime(), - DELETE_PREDICATE.formatted(MEASUREMENT), bucketName, influxdbOrg); + DELETE_PREDICATE.formatted(MEASUREMENT), BUCKET, influxDbOrg); } catch(Exception e) { @@ -96,7 +96,7 @@ private String formatTimeForFlux(ZonedDateTime time) @Override public EnergyStockPO get(EnergyStockKey key) { - String flux = String.format(GET_QUERY, bucketName, formatTimeForFlux(key.time().minusMinutes(1)), + String flux = String.format(GET_QUERY, BUCKET, formatTimeForFlux(key.time().minusMinutes(1)), formatTimeForFlux(key.time().plusMinutes(1)), MEASUREMENT, EnergyStockPO.COLUMN_DAY_AHEAD); for (FluxTable table : queryApi.query(flux)) { @@ -116,7 +116,7 @@ private EnergyStockPO convert(FluxRecord record) { EnergyStockPO data = new EnergyStockPO(); Object value = record.getValueByKey("_value"); - Object currencyValue= record.getValueByKey("currency"); + Object currencyValue= record.getValueByKey(TAG_CURRENCY); if (value instanceof Number aNumber && currencyValue instanceof String currency) { data.setDayAheadPrice(new Money(BigDecimal.valueOf(aNumber.doubleValue()), currency)); diff --git a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDao.java similarity index 88% rename from persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java rename to persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDao.java index 3823434..c98b4ed 100644 --- a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDao.java +++ b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDao.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.persistence.influx; +package de.hatoka.eos.persistence.influx.dao; import com.influxdb.client.DeleteApi; import com.influxdb.client.InfluxDBClient; @@ -8,6 +8,7 @@ import com.influxdb.client.write.Point; import com.influxdb.query.FluxRecord; import com.influxdb.query.FluxTable; +import de.hatoka.eos.persistence.influx.config.InfluxDBConfig; import de.hatoka.eos.persistence.capi.weather.WeatherForcastDAO; import de.hatoka.eos.persistence.capi.weather.WeatherForecastKey; import de.hatoka.eos.persistence.capi.weather.WeatherForecastPO; @@ -26,10 +27,11 @@ @Singleton public class InfluxWeatherForecastDao implements WeatherForcastDAO { + private static final String BUCKET = "forecast"; private static final String WEATHER_MEASUREMENT = "weather_forecast"; private static final String COLUMN_STATION = "station"; private static final String COLUMN_SOURCE = "source"; - public static final String GET_QUERY = """ + private static final String GET_QUERY = """ from(bucket: "%s") |> range(start: %s, stop: %s) |> filter(fn: (r) => r["_measurement"] == "%s") @@ -38,24 +40,23 @@ public class InfluxWeatherForecastDao implements WeatherForcastDAO |> filter(fn: (r) => r["_field"] == "%s") |> last() """; - public static final String DELETE_PREDICATE = """ + private static final String DELETE_PREDICATE = """ _measurement="%s" AND station="%s" AND source="%s" """; private final WriteApiBlocking writeApi; private final DeleteApi deleteApi; private final QueryApi queryApi; - private final String bucketName; private final String influxdbOrg; @Inject - InfluxWeatherForecastDao(InfluxDBClient influxDBClient, InfluxDBConfig config) + InfluxWeatherForecastDao(InfluxDBConfig config) { + InfluxDBClient influxDBClient = config.getClient(BUCKET); writeApi = influxDBClient.getWriteApiBlocking(); deleteApi = influxDBClient.getDeleteApi(); queryApi = influxDBClient.getQueryApi(); - bucketName = config.influxdbBucket; - influxdbOrg = config.influxdbOrg; + influxdbOrg = config.getOrg(); } @Override @@ -94,7 +95,7 @@ public void delete(WeatherForecastKey key) { // Delete data within a 1-minute window around the specified time deleteApi.delete(getZonedOf(key.time()).minusMinutes(1).toOffsetDateTime(), getZonedOf(key.time()).plusMinutes(1).toOffsetDateTime(), - DELETE_PREDICATE.formatted(WEATHER_MEASUREMENT, key.station(), key.source().getIdentifier()), bucketName, influxdbOrg); + DELETE_PREDICATE.formatted(WEATHER_MEASUREMENT, key.station(), key.source().getIdentifier()), BUCKET, influxdbOrg); } catch(Exception e) { @@ -110,7 +111,7 @@ private String formatTimeForFlux(ZonedDateTime time) @Override public WeatherForecastPO get(WeatherForecastKey key) { - String flux = String.format(GET_QUERY, bucketName, formatTimeForFlux(getZonedOf(key.time()).minusMinutes(1)), + String flux = String.format(GET_QUERY, BUCKET, formatTimeForFlux(getZonedOf(key.time()).minusMinutes(1)), formatTimeForFlux(getZonedOf(key.time()).plusMinutes(1)), WEATHER_MEASUREMENT, key.station(), key.source().getIdentifier(), WeatherForecastPO.COLUMN_SUN_PROBABILITY); for (FluxTable table : queryApi.query(flux)) { diff --git a/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxEnergyStockDaoTest.java b/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/dao/InfluxEnergyStockDaoTest.java similarity index 99% rename from persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxEnergyStockDaoTest.java rename to persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/dao/InfluxEnergyStockDaoTest.java index 5d5ead3..6b1a1ea 100644 --- a/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxEnergyStockDaoTest.java +++ b/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/dao/InfluxEnergyStockDaoTest.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.persistence.influx; +package de.hatoka.eos.persistence.influx.dao; import de.hatoka.eos.persistence.capi.energystock.EnergyStockKey; import de.hatoka.eos.persistence.capi.energystock.EnergyStockPO; diff --git a/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java b/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDaoTest.java similarity index 99% rename from persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java rename to persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDaoTest.java index 939ef4d..791f155 100644 --- a/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/InfluxWeatherForecastDaoTest.java +++ b/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDaoTest.java @@ -1,4 +1,4 @@ -package de.hatoka.eos.persistence.influx; +package de.hatoka.eos.persistence.influx.dao; import de.hatoka.eos.persistence.capi.weather.WeatherStation; import de.hatoka.eos.persistence.capi.weather.WeatherDataSource; diff --git a/persistence-influx/src/test/resources/application.properties b/persistence-influx/src/test/resources/application.properties new file mode 100644 index 0000000..7383804 --- /dev/null +++ b/persistence-influx/src/test/resources/application.properties @@ -0,0 +1,8 @@ +# InfluxDB Metrics Configuration +eos.influxdb.url=http://localhost:8086 +eos.influxdb.token=eos-token-12345678901234567890 +eos.influxdb.org=eos + +# Quarkus Configuration +quarkus.log.level=INFO +quarkus.log.category."de.hatoka.eos".level=DEBUG diff --git a/persistence-influx/src/test/resources/application.yml b/persistence-influx/src/test/resources/application.yml deleted file mode 100644 index 4c5fdb1..0000000 --- a/persistence-influx/src/test/resources/application.yml +++ /dev/null @@ -1,8 +0,0 @@ -# Test configuration for InfluxDB using environment variables -eos: - influxdb: - url: ${INFLUX_URI} - token: ${INFLUX_TOKEN} - org: eos - bucket: energy_simulation - diff --git a/settings.gradle b/settings.gradle index 9fe92ad..ffddedc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,4 +20,5 @@ include 'persistence-memory' include 'simulation' include 'optimization' include 'forecast' -// include 'service-eos' +include 'metrics-influx' +include 'metrics-memory' diff --git a/simulation/build.gradle.kts b/simulation/build.gradle.kts index 03c5f5e..fd95672 100644 --- a/simulation/build.gradle.kts +++ b/simulation/build.gradle.kts @@ -7,7 +7,6 @@ plugins { dependencies { implementation(project(":units")) implementation(project(":persistence-api")) - implementation(project(":persistence-influx")) implementation("org.slf4j:slf4j-api") implementation("org.apache.commons:commons-csv") @@ -22,10 +21,8 @@ dependencies { implementation("io.quarkus:quarkus-arc") - // InfluxDB client for Grafana visualization - implementation("com.influxdb:influxdb-client-java") - testImplementation(project(":persistence-memory")) + testImplementation(project(":metrics-memory")) testImplementation("io.quarkus:quarkus-junit5") testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/EnergyPriceForecastFactory.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/EnergyPriceForecastFactory.java deleted file mode 100644 index c0479b6..0000000 --- a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/EnergyPriceForecastFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -package de.hatoka.eos.simulation.internal.business.forecast; - -import de.hatoka.eos.persistence.capi.energystock.EnergyStockDao; -import de.hatoka.eos.simulation.capi.business.config.GridConfig; -import de.hatoka.eos.simulation.capi.business.forecast.EnergyPriceForecast; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; - -@Singleton -public class EnergyPriceForecastFactory -{ - @Inject - private EnergyStockDao dao; - - @Inject - private DaoEnergyPriceForecast daoEnergyPriceForecast; - - /** - * @return the configured price forecast - */ - public EnergyPriceForecast getEnergyPriceProvider(GridConfig gridConfig) - { - return switch(gridConfig.type()) - { - case DAO -> daoEnergyPriceForecast; - case FLAT -> new FlatPriceService(gridConfig.flatPriceConfig()); - case CSV -> daoEnergyPriceForecast; - }; - } - -} From f5102a1686eae71e75040c3d00ff363beae83ff6 Mon Sep 17 00:00:00 2001 From: Thomas Bergmann Date: Fri, 2 Jan 2026 13:12:35 +0100 Subject: [PATCH 14/16] fix tests --- forecast/build.gradle.kts | 1 - .../energycharts/EnergyChartsImporter.java | 1 + .../influx/InfluxDBMetricsExporter.java | 10 ++- .../influx/SimulationMetricsExporterTest.java | 2 +- .../test-installation-for-metrics.yaml | 85 +++++++++++++++++++ .../test-simulation-with-csv-prices.yaml | 6 ++ .../influx/config/InfluxDBConfig.java | 58 ++++++++++++- .../influx/dao/InfluxEnergyStockDao.java | 5 +- .../influx/dao/InfluxWeatherForecastDao.java | 5 +- 9 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 metrics-influx/src/test/resources/test-installation-for-metrics.yaml create mode 100644 metrics-influx/src/test/resources/test-simulation-with-csv-prices.yaml diff --git a/forecast/build.gradle.kts b/forecast/build.gradle.kts index df52d5e..b33f8e1 100644 --- a/forecast/build.gradle.kts +++ b/forecast/build.gradle.kts @@ -7,7 +7,6 @@ plugins { dependencies { implementation(project(":units")) implementation(project(":persistence-api")) - implementation(project(":persistence-influx")) implementation("com.fasterxml.jackson.core:jackson-annotations") implementation("com.fasterxml.jackson.core:jackson-databind") diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java index dfba2f4..34d5517 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java @@ -43,6 +43,7 @@ public void importStockData(ZonedDateTime startDate) Map dayAheadPrices = mapDataToTime(startDate, dayAheadAuction.getData()); String currency = dayAheadAuction.getCurrency(); Map dayAheadPricesAsMoney = dayAheadPrices.entrySet().stream() + .filter(e -> e.getValue() != null) // may some prices in the future are still not defined (null) .collect(java.util.stream.Collectors.toMap( Map.Entry::getKey, entry -> new Money(java.math.BigDecimal.valueOf(entry.getValue()), currency) diff --git a/metrics-influx/src/main/java/de/hatoka/eos/metrics/influx/InfluxDBMetricsExporter.java b/metrics-influx/src/main/java/de/hatoka/eos/metrics/influx/InfluxDBMetricsExporter.java index 2770903..ef808a5 100644 --- a/metrics-influx/src/main/java/de/hatoka/eos/metrics/influx/InfluxDBMetricsExporter.java +++ b/metrics-influx/src/main/java/de/hatoka/eos/metrics/influx/InfluxDBMetricsExporter.java @@ -6,6 +6,7 @@ import com.influxdb.client.domain.DeletePredicateRequest; import com.influxdb.client.domain.WritePrecision; import com.influxdb.client.write.Point; +import com.influxdb.exceptions.NotFoundException; import de.hatoka.eos.persistence.influx.config.InfluxDBConfig; import de.hatoka.eos.simulation.capi.business.device.DeviceRef; import de.hatoka.eos.simulation.capi.business.device.DeviceState; @@ -53,7 +54,14 @@ private void deleteOldData(SimulationResult result) DeletePredicateRequest predicate = new DeletePredicateRequest()//.predicate(condition) .start(result.step().startDate().toOffsetDateTime()) .stop(result.step().endDate().toOffsetDateTime()); - deleteApi.delete(predicate, BUCKET, influxDbOrg); + try + { + deleteApi.delete(predicate, BUCKET, influxDbOrg); + } + catch(NotFoundException e) + { + // ignore if old data or bucket not found + } } /** diff --git a/metrics-influx/src/test/java/de/hatoka/eos/metrics/influx/SimulationMetricsExporterTest.java b/metrics-influx/src/test/java/de/hatoka/eos/metrics/influx/SimulationMetricsExporterTest.java index 655cef0..7cc4b10 100644 --- a/metrics-influx/src/test/java/de/hatoka/eos/metrics/influx/SimulationMetricsExporterTest.java +++ b/metrics-influx/src/test/java/de/hatoka/eos/metrics/influx/SimulationMetricsExporterTest.java @@ -45,7 +45,7 @@ public class SimulationMetricsExporterTest public void testExportSimulationMetricsToGrafana() throws IOException { // Arrange - Load test configuration without electric car for cleaner curves - InstallationConfig config = configurationLoader.load("test-installation-for-grafana.yaml"); + InstallationConfig config = configurationLoader.load("test-installation-for-metrics.yaml"); SimulationConfig simConfig = configurationLoader.loadSimulation("test-simulation-with-csv-prices.yaml"); // Simulate from today midnight with 5-minute intervals for smooth curves diff --git a/metrics-influx/src/test/resources/test-installation-for-metrics.yaml b/metrics-influx/src/test/resources/test-installation-for-metrics.yaml new file mode 100644 index 0000000..5486fd9 --- /dev/null +++ b/metrics-influx/src/test/resources/test-installation-for-metrics.yaml @@ -0,0 +1,85 @@ +devices: + - type: SOLAR_PANEL + production: + amount: 0.45 + unit: "K_W" + statisticsResource: "curved" + panelEfficiency: .92 # 92% efficiency + count: 13 + + - type: NOISY_USAGE + consumption: + amount: 0.3 + unit: "K_W" + count: 1 + + - type: BATTERY + capacity: + amount: 3.0 + unit: "K_WH" + chargeRate: + amount: 2.2 + unit: "K_W" + dischargeRate: + amount: 2.1 + unit: "K_W" + count: 3 + chargingEfficiency: 0.85 + dischargingEfficiency: 0.85 + dailyStorageLoss: 0.01 + forceChargingLimit: 0.1 + + - type: ELECTRIC_CAR + count: 1 + capacity: + amount: 50.0 + unit: "K_WH" + chargeRate: + amount: 11.0 + unit: "K_W" + dischargeRate: + amount: 10.0 + unit: "K_W" + chargingEfficiency: 0.90 + dischargingEfficiency: 0.90 + dailyStorageLoss: 0.01 + forceChargingLimit: 0.1 + usageProfile: + days: [TUESDAY, WEDNESDAY] + startUsage: "08:00" + endUsage: "17:00" + energyConsumption: + amount: 15.0 + unit: "K_WH" + + - type: GRID + count: 1 + +grid: + type: "CSV" + flatPriceProvider: + importPrice: + amount: 0.39 + currency: EUR + exportPrice: + amount: 0.08 + currency: EUR + csvPriceProvider: + currency: EUR + exportCharge: + price: + amount: 0.00 + currency: EUR + energy: + amount: 1 + unit: "K_WH" + importCharge: + price: + amount: 0.08 + currency: EUR + energy: + amount: 1 + unit: "K_WH" + resource: + - stockprices_DE_LU_week_32-33.csv + diff --git a/metrics-influx/src/test/resources/test-simulation-with-csv-prices.yaml b/metrics-influx/src/test/resources/test-simulation-with-csv-prices.yaml new file mode 100644 index 0000000..2889998 --- /dev/null +++ b/metrics-influx/src/test/resources/test-simulation-with-csv-prices.yaml @@ -0,0 +1,6 @@ +id: "test-simulation" +simulation: + timezone: "Europe/Berlin" + start: "2025/08/04-00:00" + end: "2025/08/17-00:00" + stepDuration: "PT1H" \ No newline at end of file diff --git a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/config/InfluxDBConfig.java b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/config/InfluxDBConfig.java index 1487f51..6ce3e15 100644 --- a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/config/InfluxDBConfig.java +++ b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/config/InfluxDBConfig.java @@ -1,16 +1,32 @@ package de.hatoka.eos.persistence.influx.config; +import com.influxdb.client.BucketsApi; import com.influxdb.client.InfluxDBClient; import com.influxdb.client.InfluxDBClientFactory; +import com.influxdb.client.domain.Bucket; +import com.influxdb.client.domain.Organization; import jakarta.inject.Singleton; import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * Configuration for InfluxDB client. + * Ensures required buckets exist on startup. */ @Singleton public class InfluxDBConfig { + private static final Logger LOGGER = LoggerFactory.getLogger(InfluxDBConfig.class); + + /** + * created or existing buckets + */ + private static final Map ENSURED_BUCKETS = new ConcurrentHashMap<>(); + @ConfigProperty(name = "eos.influxdb.url", defaultValue = "http://localhost:8086") private String influxUrl; @@ -20,13 +36,53 @@ public class InfluxDBConfig @ConfigProperty(name = "eos.influxdb.org", defaultValue = "eos") private String influxOrg; + /** + * @param bucket bucket name + * @return client for bucket + */ public InfluxDBClient getClient(String bucket) { - return InfluxDBClientFactory.create(influxUrl, influxToken.toCharArray(), influxOrg, bucket); + return InfluxDBClientFactory.create(influxUrl, influxToken.toCharArray(), influxOrg, ENSURED_BUCKETS.computeIfAbsent(bucket, this::ensureBucketExists).getName()); } public String getOrg() { return influxOrg; } + + /** + * Ensures that a bucket exists in InfluxDB, creating it if necessary. + * This method is idempotent and thread-safe. + * + * @param bucketName the name of the bucket to ensure exists + */ + private Bucket ensureBucketExists(String bucketName) + { + try (InfluxDBClient client = InfluxDBClientFactory.create(influxUrl, influxToken.toCharArray(), influxOrg)) + { + BucketsApi bucketsApi = client.getBucketsApi(); + + // Check if bucket exists + Bucket existingBucket = bucketsApi.findBucketByName(bucketName); + if (existingBucket != null) + { + return existingBucket; + } + // Create the bucket + Organization org = client.getOrganizationsApi() + .findOrganizations() + .stream() + .filter(o -> o.getName().equals(influxOrg)) + .findFirst() + .orElseThrow(() -> new RuntimeException("Organization not found: " + influxOrg)); + + return bucketsApi.createBucket(bucketName, org); + } + catch (Exception e) + { + LOGGER.warn("Could not ensure InfluxDB bucket '{}' exists. You may need to create it manually: influx bucket create -n {} -o {} -r 0", + bucketName, bucketName, influxOrg); + throw e; + } + } } diff --git a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxEnergyStockDao.java b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxEnergyStockDao.java index d8c7238..e9c7405 100644 --- a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxEnergyStockDao.java +++ b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxEnergyStockDao.java @@ -6,6 +6,7 @@ import com.influxdb.client.WriteApiBlocking; import com.influxdb.client.domain.WritePrecision; import com.influxdb.client.write.Point; +import com.influxdb.exceptions.NotFoundException; import com.influxdb.query.FluxRecord; import com.influxdb.query.FluxTable; import de.hatoka.eos.persistence.influx.config.InfluxDBConfig; @@ -82,9 +83,9 @@ public void delete(EnergyStockKey key) deleteApi.delete(key.time().minusMinutes(1).toOffsetDateTime(), key.time().plusMinutes(1).toOffsetDateTime(), DELETE_PREDICATE.formatted(MEASUREMENT), BUCKET, influxDbOrg); } - catch(Exception e) + catch(NotFoundException e) { - throw new RuntimeException("Can't delete weather forecast from influx", e); + // ignore if not exists } } diff --git a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDao.java b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDao.java index c98b4ed..9822a38 100644 --- a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDao.java +++ b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDao.java @@ -6,6 +6,7 @@ import com.influxdb.client.WriteApiBlocking; import com.influxdb.client.domain.WritePrecision; import com.influxdb.client.write.Point; +import com.influxdb.exceptions.NotFoundException; import com.influxdb.query.FluxRecord; import com.influxdb.query.FluxTable; import de.hatoka.eos.persistence.influx.config.InfluxDBConfig; @@ -97,9 +98,9 @@ public void delete(WeatherForecastKey key) deleteApi.delete(getZonedOf(key.time()).minusMinutes(1).toOffsetDateTime(), getZonedOf(key.time()).plusMinutes(1).toOffsetDateTime(), DELETE_PREDICATE.formatted(WEATHER_MEASUREMENT, key.station(), key.source().getIdentifier()), BUCKET, influxdbOrg); } - catch(Exception e) + catch(NotFoundException e) { - throw new RuntimeException("Can't delete weather forecast from influx", e); + // ignore if not exists } } From 3fdafbf588f944847b4b80794178e2001049bf2a Mon Sep 17 00:00:00 2001 From: Thomas Bergmann Date: Fri, 2 Jan 2026 14:20:39 +0100 Subject: [PATCH 15/16] fix: influx dao time --- .../energycharts/EnergyChartsImporter.java | 2 +- .../capi/energystock/EnergyStockKey.java | 22 +++++++++++++++++-- .../capi/weather/WeatherForecastKey.java | 21 +++++++++++++++++- .../capi/weather/WeatherForecastKeyTest.java | 5 +++-- .../influx/dao/InfluxEnergyStockDao.java | 8 +++---- .../influx/dao/InfluxWeatherForecastDao.java | 22 +++++-------------- .../dao/InfluxWeatherForecastDaoTest.java | 5 +++++ .../forecast/DaoEnergyPriceForecast.java | 4 ++-- 8 files changed, 60 insertions(+), 29 deletions(-) diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java b/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java index 34d5517..43a30f8 100644 --- a/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java +++ b/forecast/src/main/java/de/hatoka/eos/forecast/energycharts/EnergyChartsImporter.java @@ -55,7 +55,7 @@ private void storePrices(Map dayAheadPrices) { for (Map.Entry dayAheadPrice : dayAheadPrices.entrySet()) { - EnergyStockKey key = new EnergyStockKey(dayAheadPrice.getKey()); + EnergyStockKey key = EnergyStockKey.valueOf(dayAheadPrice.getKey()); EnergyStockPO existingData = stockDao.get(key); if (existingData == null) { diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/energystock/EnergyStockKey.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/energystock/EnergyStockKey.java index 0748151..43971ad 100644 --- a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/energystock/EnergyStockKey.java +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/energystock/EnergyStockKey.java @@ -1,11 +1,29 @@ package de.hatoka.eos.persistence.capi.energystock; +import java.time.Instant; +import java.time.ZoneId; import java.time.ZonedDateTime; -public record EnergyStockKey( ZonedDateTime time) +/** + * @param time epoch milli seconds + */ +public record EnergyStockKey(long time) { + private static final ZoneId UTC = ZoneId.of("UTC"); + public static EnergyStockKey valueOf(ZonedDateTime time) { - return new EnergyStockKey(time); + return new EnergyStockKey(time.toInstant().toEpochMilli()); + } + + public Instant getInstant() + { + return Instant.ofEpochMilli(time); } + + public ZonedDateTime getZonedDateTime() + { + return getInstant().atZone(UTC); + } + } diff --git a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKey.java b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKey.java index 9e4aa0b..2f0be6b 100644 --- a/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKey.java +++ b/persistence-api/src/main/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKey.java @@ -1,11 +1,30 @@ package de.hatoka.eos.persistence.capi.weather; +import java.time.Instant; +import java.time.ZoneId; import java.time.ZonedDateTime; +/** + * @param station weather station + * @param time epoch milli seconds + * @param source weather forecast source + */ public record WeatherForecastKey(WeatherStation station, long time, WeatherDataSource source) { + private static final ZoneId UTC = ZoneId.of("UTC"); + public static WeatherForecastKey valueOf(WeatherStation station, ZonedDateTime time, WeatherDataSource source) { - return new WeatherForecastKey(station, time.toEpochSecond(), source); + return new WeatherForecastKey(station, time.toInstant().toEpochMilli(), source); + } + + public Instant getInstant() + { + return Instant.ofEpochMilli(time); + } + + public ZonedDateTime getZonedDateTime() + { + return getInstant().atZone(UTC); } } diff --git a/persistence-api/src/test/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKeyTest.java b/persistence-api/src/test/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKeyTest.java index c734ef0..d60f969 100644 --- a/persistence-api/src/test/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKeyTest.java +++ b/persistence-api/src/test/java/de/hatoka/eos/persistence/capi/weather/WeatherForecastKeyTest.java @@ -160,14 +160,15 @@ void testAsMapKey_WithAddingZ() void testValueOfFactory() { // Given - ZonedDateTime time = ZonedDateTime.parse("2024-01-01T12:00:00Z"); + ZonedDateTime time = ZonedDateTime.parse("2024-01-01T12:00:00Z").withZoneSameInstant(ZoneId.of("UTC")); // When WeatherForecastKey key = WeatherForecastKey.valueOf(WeatherStation.APOLDA, time, WeatherDataSource.OPENMETEO); // Then assertEquals(WeatherStation.APOLDA, key.station()); - assertEquals(time.toEpochSecond(), key.time()); + assertEquals(time.toInstant().toEpochMilli(), key.time()); + assertEquals(time, key.getZonedDateTime()); assertEquals(WeatherDataSource.OPENMETEO, key.source()); } diff --git a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxEnergyStockDao.java b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxEnergyStockDao.java index e9c7405..79187c7 100644 --- a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxEnergyStockDao.java +++ b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxEnergyStockDao.java @@ -62,7 +62,7 @@ public void update(EnergyStockKey key, EnergyStockPO data) try { Point point = Point.measurement(MEASUREMENT) - .time(key.time().toInstant(), WritePrecision.S) + .time(key.getInstant(), WritePrecision.S) .addTag(TAG_CURRENCY, data.getDayAheadPrice().currencyMnemonic()) .addField(EnergyStockPO.COLUMN_DAY_AHEAD, data.getDayAheadPrice().amount().doubleValue()); @@ -80,7 +80,7 @@ public void delete(EnergyStockKey key) try { // Delete data within a 1-minute window around the specified time - deleteApi.delete(key.time().minusMinutes(1).toOffsetDateTime(), key.time().plusMinutes(1).toOffsetDateTime(), + deleteApi.delete(key.getZonedDateTime().minusMinutes(1).toOffsetDateTime(), key.getZonedDateTime().plusMinutes(1).toOffsetDateTime(), DELETE_PREDICATE.formatted(MEASUREMENT), BUCKET, influxDbOrg); } catch(NotFoundException e) @@ -97,8 +97,8 @@ private String formatTimeForFlux(ZonedDateTime time) @Override public EnergyStockPO get(EnergyStockKey key) { - String flux = String.format(GET_QUERY, BUCKET, formatTimeForFlux(key.time().minusMinutes(1)), - formatTimeForFlux(key.time().plusMinutes(1)), MEASUREMENT, EnergyStockPO.COLUMN_DAY_AHEAD); + String flux = String.format(GET_QUERY, BUCKET, formatTimeForFlux(key.getZonedDateTime().minusMinutes(1)), + formatTimeForFlux(key.getZonedDateTime().plusMinutes(1)), MEASUREMENT, EnergyStockPO.COLUMN_DAY_AHEAD); for (FluxTable table : queryApi.query(flux)) { for (FluxRecord record : table.getRecords()) diff --git a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDao.java b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDao.java index 9822a38..f61a760 100644 --- a/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDao.java +++ b/persistence-influx/src/main/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDao.java @@ -9,16 +9,14 @@ import com.influxdb.exceptions.NotFoundException; import com.influxdb.query.FluxRecord; import com.influxdb.query.FluxTable; -import de.hatoka.eos.persistence.influx.config.InfluxDBConfig; import de.hatoka.eos.persistence.capi.weather.WeatherForcastDAO; import de.hatoka.eos.persistence.capi.weather.WeatherForecastKey; import de.hatoka.eos.persistence.capi.weather.WeatherForecastPO; +import de.hatoka.eos.persistence.influx.config.InfluxDBConfig; import de.hatoka.eos.units.capi.Percentage; import jakarta.inject.Inject; import jakarta.inject.Singleton; -import java.time.Instant; -import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -66,7 +64,7 @@ public void update(WeatherForecastKey key, WeatherForecastPO data) try { Point point = Point.measurement(WEATHER_MEASUREMENT) - .time(getInstantOf(key.time()), WritePrecision.S) + .time(key.getInstant(), WritePrecision.S) .addTag(COLUMN_STATION, key.station().name()) .addTag(COLUMN_SOURCE, key.source().getIdentifier()) .addField(WeatherForecastPO.COLUMN_SUN_PROBABILITY, data.getSunProbability().value()); @@ -79,23 +77,13 @@ public void update(WeatherForecastKey key, WeatherForecastPO data) } } - Instant getInstantOf(long time) - { - return Instant.ofEpochMilli(time); - } - - ZonedDateTime getZonedOf(long time) - { - return getInstantOf(time).atZone(ZoneId.of("UTC")); - } - @Override public void delete(WeatherForecastKey key) { try { // Delete data within a 1-minute window around the specified time - deleteApi.delete(getZonedOf(key.time()).minusMinutes(1).toOffsetDateTime(), getZonedOf(key.time()).plusMinutes(1).toOffsetDateTime(), + deleteApi.delete(key.getZonedDateTime().minusMinutes(1).toOffsetDateTime(), key.getZonedDateTime().plusMinutes(1).toOffsetDateTime(), DELETE_PREDICATE.formatted(WEATHER_MEASUREMENT, key.station(), key.source().getIdentifier()), BUCKET, influxdbOrg); } catch(NotFoundException e) @@ -112,8 +100,8 @@ private String formatTimeForFlux(ZonedDateTime time) @Override public WeatherForecastPO get(WeatherForecastKey key) { - String flux = String.format(GET_QUERY, BUCKET, formatTimeForFlux(getZonedOf(key.time()).minusMinutes(1)), - formatTimeForFlux(getZonedOf(key.time()).plusMinutes(1)), WEATHER_MEASUREMENT, key.station(), key.source().getIdentifier(), WeatherForecastPO.COLUMN_SUN_PROBABILITY); + String flux = String.format(GET_QUERY, BUCKET, formatTimeForFlux(key.getZonedDateTime().minusMinutes(1)), + formatTimeForFlux(key.getZonedDateTime().plusMinutes(1)), WEATHER_MEASUREMENT, key.station(), key.source().getIdentifier(), WeatherForecastPO.COLUMN_SUN_PROBABILITY); for (FluxTable table : queryApi.query(flux)) { for (FluxRecord record : table.getRecords()) diff --git a/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDaoTest.java b/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDaoTest.java index 791f155..40d2e5f 100644 --- a/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDaoTest.java +++ b/persistence-influx/src/test/java/de/hatoka/eos/persistence/influx/dao/InfluxWeatherForecastDaoTest.java @@ -194,6 +194,11 @@ void shouldDeleteOnlySpecificTimeData() dao.update(getKey(time2), createForecast(new Percentage(0.6))); dao.update(getKey(time3), createForecast(new Percentage(0.9))); + // check that values are correct stored + assertEquals(0.3, dao.get(getKey(time1)).getSunProbability().value(), 0.001); + assertEquals(0.6, dao.get(getKey(time2)).getSunProbability().value(), 0.001); + assertEquals(0.9, dao.get(getKey(time3)).getSunProbability().value(), 0.001); + // When - delete only the middle data point dao.delete(getKey(time2)); diff --git a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoEnergyPriceForecast.java b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoEnergyPriceForecast.java index e46b7a6..cfec50f 100644 --- a/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoEnergyPriceForecast.java +++ b/simulation/src/main/java/de/hatoka/eos/simulation/internal/business/forecast/DaoEnergyPriceForecast.java @@ -19,12 +19,12 @@ public class DaoEnergyPriceForecast implements EnergyPriceForecast public Money getImportPrice(ZonedDateTime time) { // config.dynamicPriceConfig().importCharge() - return stockDao.get(new EnergyStockKey(time)).getDayAheadPrice(); + return stockDao.get(EnergyStockKey.valueOf(time)).getDayAheadPrice(); } @Override public Money getExportPrice(ZonedDateTime time) { - return stockDao.get(new EnergyStockKey(time)).getDayAheadPrice(); + return stockDao.get(EnergyStockKey.valueOf(time)).getDayAheadPrice(); } } From 0e49654db781428a56d410375ecc60bb5f42bf79 Mon Sep 17 00:00:00 2001 From: Thomas Bergmann Date: Fri, 2 Jan 2026 15:55:01 +0100 Subject: [PATCH 16/16] move applications --- .../forecast/ForecastImportApplication.java | 57 ------------------- .../energy-simulation-dashboard.json | 40 ++++++------- .../influx/InfluxDBMetricsExporter.java | 7 ++- persistence-api/build.gradle.kts | 2 +- .../src/main/resources/META-INF/beans.xml | 1 + service/build.gradle.kts | 18 ++++++ {forecast => service}/run-forecast-import.sh | 0 .../de/hatoka/eos/service/EosApplication.java | 28 +++++++++ .../de/hatoka/eos/service/ForecastImport.java | 37 ++++++++++++ .../de/hatoka/eos/service/SimulationNow.java | 50 ++++++++++++++++ service/src/main/resources/META-INF/beans.xml | 2 + .../src/main/resources/application.properties | 1 + .../resources/installation-without-car.yaml | 47 +++++++++++++++ settings.gradle | 1 + .../src/main/resources/application.properties | 7 --- units/src/main/resources/META-INF/beans.xml | 1 + 16 files changed, 212 insertions(+), 87 deletions(-) delete mode 100644 forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java create mode 100644 persistence-api/src/main/resources/META-INF/beans.xml create mode 100644 service/build.gradle.kts rename {forecast => service}/run-forecast-import.sh (100%) create mode 100644 service/src/main/java/de/hatoka/eos/service/EosApplication.java create mode 100644 service/src/main/java/de/hatoka/eos/service/ForecastImport.java create mode 100644 service/src/main/java/de/hatoka/eos/service/SimulationNow.java create mode 100644 service/src/main/resources/META-INF/beans.xml rename {forecast => service}/src/main/resources/application.properties (99%) create mode 100644 service/src/main/resources/installation-without-car.yaml create mode 100644 units/src/main/resources/META-INF/beans.xml diff --git a/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java b/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java deleted file mode 100644 index e8c3ef2..0000000 --- a/forecast/src/main/java/de/hatoka/eos/forecast/ForecastImportApplication.java +++ /dev/null @@ -1,57 +0,0 @@ -package de.hatoka.eos.forecast; - -import de.hatoka.eos.forecast.energycharts.EnergyChartsImporter; -import de.hatoka.eos.forecast.openmeteo.OpenMeteoWeatherForecastImporter; -import de.hatoka.eos.persistence.capi.weather.WeatherStation; -import io.quarkus.runtime.Quarkus; -import io.quarkus.runtime.QuarkusApplication; -import io.quarkus.runtime.annotations.QuarkusMain; -import jakarta.inject.Inject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.time.ZoneId; -import java.time.ZonedDateTime; - -@QuarkusMain -public class ForecastImportApplication implements QuarkusApplication -{ - private final Logger logger = LoggerFactory.getLogger(ForecastImportApplication.class); - - @Inject - OpenMeteoWeatherForecastImporter openMeteoImporter; - - @Inject - EnergyChartsImporter energyChartsImporter; - - public static void main(String[] args) - { - Quarkus.run(ForecastImportApplication.class, args); - } - - @Override - public int run(String... args) throws Exception - { - logger.info("Starting weather forecast import..."); - try - { - ZonedDateTime startDate = ZonedDateTime.now().toLocalDate().atStartOfDay(ZoneId.of("UTC")); - energyChartsImporter.importStockData(startDate); - // Import from MeteoMedia (can only import data of today and tomorrow) - logger.debug("Importing from MeteoMedia..."); - - // Import from OpenMeteo (can import up to 3 days) - logger.debug("Importing from OpenMeteo..."); - openMeteoImporter.importWeatherForecast(WeatherStation.APOLDA, startDate); - openMeteoImporter.importWeatherForecast(WeatherStation.LEIPZIG_STADTWERKE, startDate); - logger.info("Import from OpenMeteo finished."); - - return 0; - } - catch (Exception e) - { - logger.error("Weather forecast import failed", e); - return 1; - } - } -} diff --git a/grafana/provisioning/dashboards/energy-simulation-dashboard.json b/grafana/provisioning/dashboards/energy-simulation-dashboard.json index dd0db29..13c7392 100644 --- a/grafana/provisioning/dashboards/energy-simulation-dashboard.json +++ b/grafana/provisioning/dashboards/energy-simulation-dashboard.json @@ -80,7 +80,7 @@ "uid": "influxdb" }, "hide": false, - "query": "from(bucket: \"energy_simulation\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"energy_costs\")\n |> filter(fn: (r) => r.simulation == \"today-energy-simulation\")\n |> filter(fn: (r) => r._field == \"grid_net_eur\")\n |> cumulativeSum()\n |> yield(name: \"net\")", + "query": "from(bucket: \"metrics\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"energy_flow\")\n |> filter(fn: (r) => r.simulation == \"today-energy-simulation\")\n |> filter(fn: (r) => r._field == \"grid_net_eur\")\n |> cumulativeSum()\n |> yield(name: \"net\")", "refId": "Net" } ], @@ -145,7 +145,7 @@ "uid": "influxdb" }, "hide": false, - "query": "from(bucket: \"energy_simulation\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"energy_costs\")\n |> filter(fn: (r) => r.simulation == \"today-energy-simulation\")\n |> filter(fn: (r) => r._field == \"grid_import_eur\")\n |> cumulativeSum()\n |> yield(name: \"import\")", + "query": "from(bucket: \"metrics\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"energy_flow\")\n |> filter(fn: (r) => r.simulation == \"today-energy-simulation\")\n |> filter(fn: (r) => r._field == \"grid_import_eur\")\n |> cumulativeSum()\n |> yield(name: \"import\")", "refId": "Import" } ], @@ -210,7 +210,7 @@ "uid": "influxdb" }, "hide": false, - "query": "from(bucket: \"energy_simulation\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"energy_costs\")\n |> filter(fn: (r) => r.simulation == \"today-energy-simulation\")\n |> filter(fn: (r) => r._field == \"grid_export_eur\")\n |> cumulativeSum()\n |> yield(name: \"export\")", + "query": "from(bucket: \"metrics\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"energy_flow\")\n |> filter(fn: (r) => r.simulation == \"today-energy-simulation\")\n |> filter(fn: (r) => r._field == \"grid_export_eur\")\n |> cumulativeSum()\n |> yield(name: \"export\")", "refId": "export" } ], @@ -286,7 +286,7 @@ { "id": "color", "value": { - "fixedColor": "orange", + "fixedColor": "red", "mode": "fixed" } }, @@ -305,7 +305,7 @@ { "id": "color", "value": { - "fixedColor": "purple", + "fixedColor": "blue", "mode": "fixed" } }, @@ -342,7 +342,7 @@ "type": "influxdb", "uid": "influxdb" }, - "query": "from(bucket: \"energy_simulation\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"energy_flow\")\n |> filter(fn: (r) => r.simulation == \"today-energy-simulation\")\n |> filter(fn: (r) => r._field == \"imported_kwh\" or r._field == \"exported_kwh\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "query": "from(bucket: \"metrics\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> filter(fn: (r) => r._field == \"imported_kwh\" or r._field == \"exported_kwh\")\n |> yield(name: \"mean\")", "refId": "A" } ], @@ -488,7 +488,7 @@ { "matcher": { "id": "byName", - "options": "imported_kwh today-energy-simulation" + "options": "imported_kwh" }, "properties": [ { @@ -507,7 +507,7 @@ { "matcher": { "id": "byName", - "options": "exported_kwh today-energy-simulation" + "options": "exported_kwh" }, "properties": [ { @@ -550,7 +550,7 @@ "type": "influxdb", "uid": "influxdb" }, - "query": "from(bucket: \"energy_simulation\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"energy_flow\")\n |> filter(fn: (r) => r.simulation == \"today-energy-simulation\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "query": "from(bucket: \"metrics\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"energy_flow\")\n |> filter(fn: (r) => r.simulation == \"today-energy-simulation\")\n |> filter(fn: (r) => r.kind == \"flow\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "refId": "A" } ], @@ -702,7 +702,7 @@ "type": "influxdb", "uid": "influxdb" }, - "query": "from(bucket: \"energy_simulation\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"forecast_price\")\n |> filter(fn: (r) => r._field == \"import_price_eur\" or r._field == \"export_price_eur\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "query": "from(bucket: \"forecast\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"energystock_forecast\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "refId": "A" } ], @@ -816,7 +816,7 @@ "type": "influxdb", "uid": "influxdb" }, - "query": "from(bucket: \"energy_simulation\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"battery_storage\")\n |> filter(fn: (r) => r.simulation == \"today-energy-simulation\")\n |> filter(fn: (r) => r.type == \"BATTERY\")\n |> filter(fn: (r) => r._field == \"stored_energy_kwh\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"device\"])\n |> yield(name: \"mean\")", + "query": "from(bucket: \"metrics\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"energy_flow\")\n |> filter(fn: (r) => r.simulation == \"today-energy-simulation\")\n |> filter(fn: (r) => r._field == \"stored_energy_kwh\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"device\"])\n |> yield(name: \"mean\")", "refId": "Batteries" } ], @@ -932,7 +932,7 @@ "uid": "influxdb" }, "hide": false, - "query": "from(bucket: \"energy_simulation\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"battery_storage\")\n |> filter(fn: (r) => r.simulation == \"today-energy-simulation\")\n |> filter(fn: (r) => r.type == \"ELECTRIC_CAR\")\n |> filter(fn: (r) => r._field == \"storage_percentage\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"device\"])\n |> yield(name: \"mean\")", + "query": "from(bucket: \"metrics\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"energy_flow\")\n |> filter(fn: (r) => r.simulation == \"today-energy-simulation\")\n |> filter(fn: (r) => r._field == \"storage_percentage\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"device\"])\n |> yield(name: \"mean\")", "refId": "A" } ], @@ -1004,7 +1004,7 @@ { "matcher": { "id": "byName", - "options": "grid_import_eur today-energy-simulation" + "options": "grid_import_eur" }, "properties": [ { @@ -1016,14 +1016,14 @@ }, { "id": "displayName", - "value": "import" + "value": "Grid Import" } ] }, { "matcher": { "id": "byName", - "options": "grid_export_eur today-energy-simulation" + "options": "grid_export_eur" }, "properties": [ { @@ -1035,7 +1035,7 @@ }, { "id": "displayName", - "value": "export" + "value": "Grid Export" } ] } @@ -1066,7 +1066,7 @@ "type": "influxdb", "uid": "influxdb" }, - "query": "from(bucket: \"energy_simulation\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"energy_costs\")\n |> filter(fn: (r) => r.simulation == \"today-energy-simulation\")\n |> filter(fn: (r) => r._field == \"grid_import_eur\" or r._field == \"grid_export_eur\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "query": "from(bucket: \"metrics\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"energy_flow\")\n |> filter(fn: (r) => r.simulation == \"today-energy-simulation\")\n |> filter(fn: (r) => r._field == \"grid_import_eur\" or r._field == \"grid_export_eur\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> keep(columns: [\"_time\", \"_value\", \"_field\"])\n |> yield(name: \"mean\")", "refId": "A" } ], @@ -1085,13 +1085,13 @@ "list": [] }, "time": { - "from": "now-24h", - "to": "now" + "from": "now", + "to": "now+1d" }, "timepicker": {}, "timezone": "", "title": "Energy System Simulation (EOS)", "uid": "eos-simulation", - "version": 26, + "version": 9, "weekStart": "" } \ No newline at end of file diff --git a/metrics-influx/src/main/java/de/hatoka/eos/metrics/influx/InfluxDBMetricsExporter.java b/metrics-influx/src/main/java/de/hatoka/eos/metrics/influx/InfluxDBMetricsExporter.java index ef808a5..42b9597 100644 --- a/metrics-influx/src/main/java/de/hatoka/eos/metrics/influx/InfluxDBMetricsExporter.java +++ b/metrics-influx/src/main/java/de/hatoka/eos/metrics/influx/InfluxDBMetricsExporter.java @@ -94,6 +94,7 @@ public void exportMetrics(SimulationResult result) // Export energy flow metrics writeApi.writePoint(Point.measurement(MEASUREMENT) .addTag(TAG_SIMULATION, result.request().simulationId()) + .addTag("kind", "flow") .addField("produced_kwh", result.system().produced().amount()) .addField("consumed_kwh", -result.system().consumed().amount()) .addField("charged_kwh", -result.system().charged().amount()) @@ -103,8 +104,9 @@ public void exportMetrics(SimulationResult result) .time(timestamp, WritePrecision.NS)); // Export cost metrics - writeApi.writePoint(Point.measurement("energy_costs") + writeApi.writePoint(Point.measurement(MEASUREMENT) .addTag(TAG_SIMULATION, result.request().simulationId()) + .addTag("kind", "costs") .addField("grid_import_eur", result.system().importRevenue().amount().doubleValue()) .addField("grid_export_eur", result.system().exportRevenue().amount().doubleValue()) .addField("grid_net_eur", result.system().getEnergyRevenue().amount().doubleValue()) @@ -145,8 +147,9 @@ public void exportMetrics(SimulationResult result) */ private void writeBattery(String simulationId, DeviceRef deviceRef, DeviceState state, Instant timestamp) { - writeApi.writePoint(Point.measurement("battery_storage") + writeApi.writePoint(Point.measurement(MEASUREMENT) .addTag(TAG_SIMULATION, simulationId) + .addTag("kind", "device") .addTag("device", deviceRef.id()) .addTag("type", deviceRef.type().name()) .addField("storage_percentage", state.percentage().value()) // Convert to percentage diff --git a/persistence-api/build.gradle.kts b/persistence-api/build.gradle.kts index b17a223..9e08919 100644 --- a/persistence-api/build.gradle.kts +++ b/persistence-api/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - java + `java-library` } dependencies { diff --git a/persistence-api/src/main/resources/META-INF/beans.xml b/persistence-api/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..2d69bfb --- /dev/null +++ b/persistence-api/src/main/resources/META-INF/beans.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/service/build.gradle.kts b/service/build.gradle.kts new file mode 100644 index 0000000..adb1fac --- /dev/null +++ b/service/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + java + id("io.quarkus") +} + +dependencies { + implementation(project(":units")) + implementation(project(":persistence-api")) + implementation(project(":persistence-influx")) + implementation(project(":forecast")) + implementation(project(":simulation")) + implementation(project(":metrics-influx")) + + implementation("io.quarkus:quarkus-arc") + implementation("org.slf4j:slf4j-api") + implementation("jakarta.inject:jakarta.inject-api") +} + diff --git a/forecast/run-forecast-import.sh b/service/run-forecast-import.sh similarity index 100% rename from forecast/run-forecast-import.sh rename to service/run-forecast-import.sh diff --git a/service/src/main/java/de/hatoka/eos/service/EosApplication.java b/service/src/main/java/de/hatoka/eos/service/EosApplication.java new file mode 100644 index 0000000..638e3e2 --- /dev/null +++ b/service/src/main/java/de/hatoka/eos/service/EosApplication.java @@ -0,0 +1,28 @@ +package de.hatoka.eos.service; + +import io.quarkus.runtime.Quarkus; +import io.quarkus.runtime.QuarkusApplication; +import io.quarkus.runtime.annotations.QuarkusMain; +import jakarta.inject.Inject; + +@QuarkusMain +public class EosApplication implements QuarkusApplication +{ + @Inject + private SimulationNow simulation; + @Inject + private ForecastImport forecast; + + public static void main(String[] args) + { + Quarkus.run(EosApplication.class, args); + } + + @Override + public int run(String... args) throws Exception + { + // forecast.run(); + simulation.run(); + return 0; + } +} \ No newline at end of file diff --git a/service/src/main/java/de/hatoka/eos/service/ForecastImport.java b/service/src/main/java/de/hatoka/eos/service/ForecastImport.java new file mode 100644 index 0000000..6969138 --- /dev/null +++ b/service/src/main/java/de/hatoka/eos/service/ForecastImport.java @@ -0,0 +1,37 @@ +package de.hatoka.eos.service; + +import de.hatoka.eos.forecast.energycharts.EnergyChartsImporter; +import de.hatoka.eos.forecast.openmeteo.OpenMeteoWeatherForecastImporter; +import de.hatoka.eos.persistence.capi.weather.WeatherStation; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +@Singleton +public class ForecastImport +{ + private final Logger logger = LoggerFactory.getLogger(ForecastImport.class); + + @Inject + OpenMeteoWeatherForecastImporter openMeteoImporter; + @Inject + EnergyChartsImporter energyChartsImporter; + + public void run() throws Exception + { + logger.info("Importing stock from Energy Charts..."); + ZonedDateTime startDate = ZonedDateTime.now().toLocalDate().atStartOfDay(ZoneId.of("UTC")); + energyChartsImporter.importStockData(startDate); + logger.info("Importing from Energy Charts finished."); + + // Import from OpenMeteo (can import up to 3 days) + logger.debug("Importing from weather from OpenMeteo..."); + openMeteoImporter.importWeatherForecast(WeatherStation.APOLDA, startDate); + openMeteoImporter.importWeatherForecast(WeatherStation.LEIPZIG_STADTWERKE, startDate); + logger.info("Import from OpenMeteo finished."); + } +} diff --git a/service/src/main/java/de/hatoka/eos/service/SimulationNow.java b/service/src/main/java/de/hatoka/eos/service/SimulationNow.java new file mode 100644 index 0000000..7243fff --- /dev/null +++ b/service/src/main/java/de/hatoka/eos/service/SimulationNow.java @@ -0,0 +1,50 @@ +package de.hatoka.eos.service; + +import de.hatoka.eos.simulation.capi.business.config.InstallationConfig; +import de.hatoka.eos.simulation.capi.business.device.DeviceFactory; +import de.hatoka.eos.simulation.capi.business.forecast.Forecasts; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationRequest; +import de.hatoka.eos.simulation.capi.business.simulation.SimulationResult; +import de.hatoka.eos.simulation.capi.business.simulation.Simulator; +import de.hatoka.eos.simulation.internal.business.config.ConfigurationLoader; +import io.quarkus.runtime.Quarkus; +import io.quarkus.runtime.QuarkusApplication; +import io.quarkus.runtime.annotations.QuarkusMain; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Collections; + +@Singleton +public class SimulationNow +{ + @Inject + private DeviceFactory deviceFactory; + @Inject + private ConfigurationLoader configurationLoader; + @Inject + private Simulator simulator; + + private static final Logger LOGGER = LoggerFactory.getLogger(SimulationNow.class); + + public void run() throws Exception + { + LOGGER.info("Starting simulation..."); + // Arrange - Load actual test-installation.yaml configuration + InstallationConfig config = configurationLoader.loadInstallation("installation-without-car.yaml"); + + // Simulate one full day (24 hours) with 15,-hour steps + ZonedDateTime startDate = ZonedDateTime.now().withZoneSameInstant(ZoneId.of("UTC")); + ZonedDateTime endDate = startDate.plusDays(1); + Duration stepDuration = Duration.ofMinutes(15); + + SimulationRequest request = new SimulationRequest("today-energy-simulation", startDate, endDate, stepDuration, deviceFactory.createDevices(config.getDevices()), Collections.emptyMap(), Forecasts.STANDARD); + SimulationResult result = simulator.simulate(request); + LOGGER.info("Simulation finished with revenue {}", result.system().getEnergyRevenue()); + } +} \ No newline at end of file diff --git a/service/src/main/resources/META-INF/beans.xml b/service/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..cc362e3 --- /dev/null +++ b/service/src/main/resources/META-INF/beans.xml @@ -0,0 +1,2 @@ + + diff --git a/forecast/src/main/resources/application.properties b/service/src/main/resources/application.properties similarity index 99% rename from forecast/src/main/resources/application.properties rename to service/src/main/resources/application.properties index b4466ee..06973b1 100644 --- a/forecast/src/main/resources/application.properties +++ b/service/src/main/resources/application.properties @@ -9,3 +9,4 @@ eos.influxdb.bucket=forecast # Quarkus Configuration quarkus.log.level=INFO quarkus.log.category."de.hatoka.eos".level=DEBUG + diff --git a/service/src/main/resources/installation-without-car.yaml b/service/src/main/resources/installation-without-car.yaml new file mode 100644 index 0000000..1e6a92e --- /dev/null +++ b/service/src/main/resources/installation-without-car.yaml @@ -0,0 +1,47 @@ +devices: + - type: SOLAR_PANEL + production: + amount: 0.45 + unit: "K_W" + statisticsResource: "curved" + panelEfficiency: .92 # 92% efficiency + count: 13 + + - type: NOISY_USAGE + consumption: + amount: 0.25 + unit: "K_W" + count: 1 + + - type: BATTERY + count: 3 + capacity: + amount: 3.4 + unit: "K_WH" + chargeRate: + amount: 2.2 + unit: "K_W" + dischargeRate: + amount: 2.1 + unit: "K_W" + chargingEfficiency: 0.85 + dischargingEfficiency: 0.85 + dailyStorageLoss: 0.01 + + - type: GRID + count: 1 + +# Weather station configuration for forecasts +# Available stations: APOLDA, LEIPZIG_STADTWERKE +weatherStation: APOLDA + +grid: + type: "DAO" + flatPriceProvider: + importPrice: + amount: 0.39 + currency: EUR + exportPrice: + amount: 0.08 + currency: EUR + diff --git a/settings.gradle b/settings.gradle index ffddedc..334a6c5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,3 +22,4 @@ include 'optimization' include 'forecast' include 'metrics-influx' include 'metrics-memory' +include 'service' diff --git a/simulation/src/main/resources/application.properties b/simulation/src/main/resources/application.properties index c260bcc..ba11ba1 100644 --- a/simulation/src/main/resources/application.properties +++ b/simulation/src/main/resources/application.properties @@ -1,12 +1,5 @@ quarkus.profile=production -# InfluxDB Metrics Configuration -eos.metrics.influxdb.enabled=true -eos.metrics.influxdb.url=http://localhost:8086 -eos.metrics.influxdb.token=eos-token-12345678901234567890 -eos.metrics.influxdb.org=eos -eos.metrics.influxdb.bucket=energy_simulation - # Quarkus Configuration quarkus.log.level=INFO quarkus.log.category."de.hatoka.eos".level=DEBUG diff --git a/units/src/main/resources/META-INF/beans.xml b/units/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..2d69bfb --- /dev/null +++ b/units/src/main/resources/META-INF/beans.xml @@ -0,0 +1 @@ + \ No newline at end of file