From 4c2997dbd6c3077bfc3df5ef208bb7386ed36d34 Mon Sep 17 00:00:00 2001 From: Bobby Noelte Date: Sun, 16 Nov 2025 13:26:18 +0100 Subject: [PATCH] feat: add bidding zone to energy charts price prediction (#765) Energy charts supports bidding zones. Allow to specifiy the bidding zone in the configuration. Extend and simplify ElecPrice configuration structure and setup config migration to automatically update the configuration file. Signed-off-by: Bobby Noelte --- docs/_generated/configelecprice.md | 79 ++++++++-------- docs/_generated/configexample.md | 8 +- docs/akkudoktoreos/prediction.md | 5 +- openapi.json | 90 ++++++++++++------- src/akkudoktoreos/config/configmigrate.py | 9 +- src/akkudoktoreos/prediction/elecprice.py | 31 +++---- .../prediction/elecpriceenergycharts.py | 43 ++++++++- .../prediction/elecpriceimport.py | 12 +-- tests/test_elecpriceimport.py | 22 ++--- 9 files changed, 177 insertions(+), 122 deletions(-) diff --git a/docs/_generated/configelecprice.md b/docs/_generated/configelecprice.md index bc912d3..9110848 100644 --- a/docs/_generated/configelecprice.md +++ b/docs/_generated/configelecprice.md @@ -8,8 +8,9 @@ | Name | Environment Variable | Type | Read-Only | Default | Description | | ---- | -------------------- | ---- | --------- | ------- | ----------- | | charges_kwh | `EOS_ELECPRICE__CHARGES_KWH` | `Optional[float]` | `rw` | `None` | Electricity price charges [€/kWh]. Will be added to variable market price. | +| elecpriceimport | `EOS_ELECPRICE__ELECPRICEIMPORT` | `ElecPriceImportCommonSettings` | `rw` | `required` | Import provider settings. | +| energycharts | `EOS_ELECPRICE__ENERGYCHARTS` | `ElecPriceEnergyChartsCommonSettings` | `rw` | `required` | Energy Charts provider settings. | | provider | `EOS_ELECPRICE__PROVIDER` | `Optional[str]` | `rw` | `None` | Electricity price provider id of provider to be used. | -| provider_settings | `EOS_ELECPRICE__PROVIDER_SETTINGS` | `ElecPriceCommonProviderSettings` | `rw` | `required` | Provider settings | | vat_rate | `EOS_ELECPRICE__VAT_RATE` | `Optional[float]` | `rw` | `1.19` | VAT rate factor applied to electricity price when charges are used. | ::: @@ -25,8 +26,41 @@ "provider": "ElecPriceAkkudoktor", "charges_kwh": 0.21, "vat_rate": 1.19, - "provider_settings": { - "ElecPriceImport": null + "elecpriceimport": { + "import_file_path": null, + "import_json": null + }, + "energycharts": { + "bidding_zone": "DE-LU" + } + } + } +``` + + +### Common settings for Energy Charts electricity price provider + + +:::{table} elecprice::energycharts +:widths: 10 10 5 5 30 +:align: left + +| Name | Type | Read-Only | Default | Description | +| ---- | ---- | --------- | ------- | ----------- | +| bidding_zone | `` | `rw` | `EnergyChartsBiddingZones.DE_LU` | Bidding Zone: 'AT', 'BE', 'CH', 'CZ', 'DE-LU', 'DE-AT-LU', 'DK1', 'DK2', 'FR', 'HU', 'IT-NORTH', 'NL', 'NO2', 'PL', 'SE4' or 'SI' | +::: + + + +**Example Input/Output** + + + +```json + { + "elecprice": { + "energycharts": { + "bidding_zone": "AT" } } } @@ -36,7 +70,7 @@ ### Common settings for elecprice data import from file or JSON String -:::{table} elecprice::provider_settings::ElecPriceImport +:::{table} elecprice::elecpriceimport :widths: 10 10 5 5 30 :align: left @@ -55,40 +89,9 @@ ```json { "elecprice": { - "provider_settings": { - "ElecPriceImport": { - "import_file_path": null, - "import_json": "{\"elecprice_marketprice_wh\": [0.0003384, 0.0003318, 0.0003284]}" - } - } - } - } -``` - - -### Electricity Price Prediction Provider Configuration - - -:::{table} elecprice::provider_settings -:widths: 10 10 5 5 30 -:align: left - -| Name | Type | Read-Only | Default | Description | -| ---- | ---- | --------- | ------- | ----------- | -| ElecPriceImport | `Optional[akkudoktoreos.prediction.elecpriceimport.ElecPriceImportCommonSettings]` | `rw` | `None` | ElecPriceImport settings | -::: - - - -**Example Input/Output** - - - -```json - { - "elecprice": { - "provider_settings": { - "ElecPriceImport": null + "elecpriceimport": { + "import_file_path": null, + "import_json": "{\"elecprice_marketprice_wh\": [0.0003384, 0.0003318, 0.0003284]}" } } } diff --git a/docs/_generated/configexample.md b/docs/_generated/configexample.md index 3789e8a..389a3ac 100644 --- a/docs/_generated/configexample.md +++ b/docs/_generated/configexample.md @@ -71,8 +71,12 @@ "provider": "ElecPriceAkkudoktor", "charges_kwh": 0.21, "vat_rate": 1.19, - "provider_settings": { - "ElecPriceImport": null + "elecpriceimport": { + "import_file_path": null, + "import_json": null + }, + "energycharts": { + "bidding_zone": "DE-LU" } }, "ems": { diff --git a/docs/akkudoktoreos/prediction.md b/docs/akkudoktoreos/prediction.md index e4d0163..d40aedc 100644 --- a/docs/akkudoktoreos/prediction.md +++ b/docs/akkudoktoreos/prediction.md @@ -124,8 +124,9 @@ Configuration options: - `charges_kwh`: Electricity price charges (€/kWh). - `vat_rate`: VAT rate factor applied to electricity price when charges are used (default: 1.19). - - `provider_settings.import_file_path`: Path to the file to import electricity price forecast data from. - - `provider_settings.import_json`: JSON string, dictionary of electricity price forecast value lists. + - `elecpriceimport.import_file_path`: Path to the file to import electricity price forecast data from. + - `elecpriceimport.import_json`: JSON string, dictionary of electricity price forecast value lists. + - `energycharts.bidding_zone`: Bidding zone Energy Charts shall provide price data for. ### ElecPriceAkkudoktor Provider diff --git a/openapi.json b/openapi.json index a51c8c3..fe1b1fe 100644 --- a/openapi.json +++ b/openapi.json @@ -2469,7 +2469,10 @@ "$ref": "#/components/schemas/ElecPriceCommonSettings-Output", "default": { "vat_rate": 1.19, - "provider_settings": {} + "elecpriceimport": {}, + "energycharts": { + "bidding_zone": "DE-LU" + } } }, "feedintariff": { @@ -2975,27 +2978,6 @@ "title": "DevicesCommonSettings", "description": "Base configuration for devices simulation settings." }, - "ElecPriceCommonProviderSettings": { - "properties": { - "ElecPriceImport": { - "anyOf": [ - { - "$ref": "#/components/schemas/ElecPriceImportCommonSettings" - }, - { - "type": "null" - } - ], - "description": "ElecPriceImport settings", - "examples": [ - null - ] - } - }, - "type": "object", - "title": "ElecPriceCommonProviderSettings", - "description": "Electricity Price Prediction Provider Configuration." - }, "ElecPriceCommonSettings-Input": { "properties": { "provider": { @@ -3046,12 +3028,13 @@ 1.19 ] }, - "provider_settings": { - "$ref": "#/components/schemas/ElecPriceCommonProviderSettings", - "description": "Provider settings", - "examples": [ - {} - ] + "elecpriceimport": { + "$ref": "#/components/schemas/ElecPriceImportCommonSettings", + "description": "Import provider settings." + }, + "energycharts": { + "$ref": "#/components/schemas/ElecPriceEnergyChartsCommonSettings", + "description": "Energy Charts provider settings." } }, "type": "object", @@ -3108,18 +3091,34 @@ 1.19 ] }, - "provider_settings": { - "$ref": "#/components/schemas/ElecPriceCommonProviderSettings", - "description": "Provider settings", - "examples": [ - {} - ] + "elecpriceimport": { + "$ref": "#/components/schemas/ElecPriceImportCommonSettings", + "description": "Import provider settings." + }, + "energycharts": { + "$ref": "#/components/schemas/ElecPriceEnergyChartsCommonSettings", + "description": "Energy Charts provider settings." } }, "type": "object", "title": "ElecPriceCommonSettings", "description": "Electricity Price Prediction Configuration." }, + "ElecPriceEnergyChartsCommonSettings": { + "properties": { + "bidding_zone": { + "$ref": "#/components/schemas/EnergyChartsBiddingZones", + "description": "Bidding Zone: 'AT', 'BE', 'CH', 'CZ', 'DE-LU', 'DE-AT-LU', 'DK1', 'DK2', 'FR', 'HU', 'IT-NORTH', 'NL', 'NO2', 'PL', 'SE4' or 'SI'", + "default": "DE-LU", + "examples": [ + "AT" + ] + } + }, + "type": "object", + "title": "ElecPriceEnergyChartsCommonSettings", + "description": "Common settings for Energy Charts electricity price provider." + }, "ElecPriceImportCommonSettings": { "properties": { "import_file_path": { @@ -3375,6 +3374,29 @@ "title": "ElectricVehicleResult", "description": "Result class containing information related to the electric vehicle's charging and discharging behavior." }, + "EnergyChartsBiddingZones": { + "type": "string", + "enum": [ + "AT", + "BE", + "CH", + "CZ", + "DE-LU", + "DE-AT-LU", + "DK1", + "DK2", + "FR", + "HU", + "IT-NORTH", + "NL", + "NO2", + "PL", + "SE4", + "SI" + ], + "title": "EnergyChartsBiddingZones", + "description": "Energy Charts Bidding Zones." + }, "EnergyManagementCommonSettings": { "properties": { "startup_delay": { diff --git a/src/akkudoktoreos/config/configmigrate.py b/src/akkudoktoreos/config/configmigrate.py index ebbc4ae..e4a3d6b 100644 --- a/src/akkudoktoreos/config/configmigrate.py +++ b/src/akkudoktoreos/config/configmigrate.py @@ -21,11 +21,14 @@ if TYPE_CHECKING: # - tuple[str, Callable[[Any], Any]] (new path + transform) # - None (drop) MIGRATION_MAP: Dict[str, Union[str, Tuple[str, Callable[[Any], Any]], None]] = { - # 0.1.0 -> 0.2.0 + # 0.2.0 -> 0.2.0+dev + "elecprice/provider_settings/ElecPriceImport/import_file_path": "elecprice/elecpriceimport/import_file_path", + "elecprice/provider_settings/ElecPriceImport/import_json": "elecprice/elecpriceimport/import_json", + # 0.1.0 -> 0.2.0+dev "devices/batteries/0/initial_soc_percentage": None, "devices/electric_vehicles/0/initial_soc_percentage": None, - "elecprice/provider_settings/import_file_path": "elecprice/provider_settings/ElecPriceImport/import_file_path", - "elecprice/provider_settings/import_json": "elecprice/provider_settings/ElecPriceImport/import_json", + "elecprice/provider_settings/import_file_path": "elecprice/elecpriceimport/import_file_path", + "elecprice/provider_settings/import_json": "elecprice/elecpriceimport/import_json", "load/provider_settings/import_file_path": "load/provider_settings/LoadImport/import_file_path", "load/provider_settings/import_json": "load/provider_settings/LoadImport/import_json", "load/provider_settings/loadakkudoktor_year_energy": "load/provider_settings/LoadAkkudoktor/loadakkudoktor_year_energy_kwh", diff --git a/src/akkudoktoreos/prediction/elecprice.py b/src/akkudoktoreos/prediction/elecprice.py index 7d8c20f..a67c3cf 100644 --- a/src/akkudoktoreos/prediction/elecprice.py +++ b/src/akkudoktoreos/prediction/elecprice.py @@ -4,6 +4,9 @@ from pydantic import Field, field_validator from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider +from akkudoktoreos.prediction.elecpriceenergycharts import ( + ElecPriceEnergyChartsCommonSettings, +) from akkudoktoreos.prediction.elecpriceimport import ElecPriceImportCommonSettings from akkudoktoreos.prediction.prediction import get_prediction @@ -17,15 +20,6 @@ elecprice_providers = [ ] -class ElecPriceCommonProviderSettings(SettingsBaseModel): - """Electricity Price Prediction Provider Configuration.""" - - ElecPriceImport: Optional[ElecPriceImportCommonSettings] = Field( - default=None, - json_schema_extra={"description": "ElecPriceImport settings", "examples": [None]}, - ) - - class ElecPriceCommonSettings(SettingsBaseModel): """Electricity Price Prediction Configuration.""" @@ -53,17 +47,14 @@ class ElecPriceCommonSettings(SettingsBaseModel): }, ) - provider_settings: ElecPriceCommonProviderSettings = Field( - default_factory=ElecPriceCommonProviderSettings, - json_schema_extra={ - "description": "Provider settings", - "examples": [ - # Example 1: Empty/default settings (all providers None) - { - "ElecPriceImport": None, - }, - ], - }, + elecpriceimport: ElecPriceImportCommonSettings = Field( + default_factory=ElecPriceImportCommonSettings, + json_schema_extra={"description": "Import provider settings."}, + ) + + energycharts: ElecPriceEnergyChartsCommonSettings = Field( + default_factory=ElecPriceEnergyChartsCommonSettings, + json_schema_extra={"description": "Energy Charts provider settings."}, ) # Validators diff --git a/src/akkudoktoreos/prediction/elecpriceenergycharts.py b/src/akkudoktoreos/prediction/elecpriceenergycharts.py index 016d5da..334a711 100644 --- a/src/akkudoktoreos/prediction/elecpriceenergycharts.py +++ b/src/akkudoktoreos/prediction/elecpriceenergycharts.py @@ -7,21 +7,44 @@ format, enabling consistent access to forecasted and historical electricity pric """ from datetime import datetime +from enum import Enum from typing import Any, List, Optional, Union import numpy as np import pandas as pd import requests from loguru import logger -from pydantic import ValidationError +from pydantic import Field, ValidationError from statsmodels.tsa.holtwinters import ExponentialSmoothing +from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.core.cache import cache_in_file from akkudoktoreos.core.pydantic import PydanticBaseModel from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration +class EnergyChartsBiddingZones(str, Enum): + """Energy Charts Bidding Zones.""" + + AT = "AT" + BE = "BE" + CH = "CH" + CZ = "CZ" + DE_LU = "DE-LU" + DE_AT_LU = "DE-AT-LU" + DK1 = "DK1" + DK2 = "DK2" + FR = "FR" + HU = "HU" + IT_North = "IT-NORTH" + NL = "NL" + NO2 = "NO2" + PL = "PL" + SE4 = "SE4" + SI = "SI" + + class EnergyChartsElecPrice(PydanticBaseModel): license_info: str unix_seconds: List[int] @@ -30,6 +53,21 @@ class EnergyChartsElecPrice(PydanticBaseModel): deprecated: bool +class ElecPriceEnergyChartsCommonSettings(SettingsBaseModel): + """Common settings for Energy Charts electricity price provider.""" + + bidding_zone: EnergyChartsBiddingZones = Field( + default=EnergyChartsBiddingZones.DE_LU, + json_schema_extra={ + "description": ( + "Bidding Zone: 'AT', 'BE', 'CH', 'CZ', 'DE-LU', 'DE-AT-LU', 'DK1', 'DK2', 'FR', " + "'HU', 'IT-NORTH', 'NL', 'NO2', 'PL', 'SE4' or 'SI'" + ), + "examples": ["AT"], + }, + ) + + class ElecPriceEnergyCharts(ElecPriceProvider): """Fetch and process electricity price forecast data from Energy-Charts. @@ -95,7 +133,8 @@ class ElecPriceEnergyCharts(ElecPriceProvider): ) last_date = to_datetime(self.end_datetime, as_string="YYYY-MM-DD") - url = f"{source}/price?bzn=DE-LU&start={start_date}&end={last_date}" + bidding_zone = str(self.config.elecprice.energycharts.bidding_zone) + url = f"{source}/price?bzn={bidding_zone}&start={start_date}&end={last_date}" response = requests.get(url, timeout=30) logger.debug(f"Response from {url}: {response}") response.raise_for_status() # Raise an error for bad responses diff --git a/src/akkudoktoreos/prediction/elecpriceimport.py b/src/akkudoktoreos/prediction/elecpriceimport.py index 65d89c7..5ac2687 100644 --- a/src/akkudoktoreos/prediction/elecpriceimport.py +++ b/src/akkudoktoreos/prediction/elecpriceimport.py @@ -9,7 +9,6 @@ format, enabling consistent access to forecasted and historical elecprice attrib from pathlib import Path from typing import Optional, Union -from loguru import logger from pydantic import Field, field_validator from akkudoktoreos.config.configabc import SettingsBaseModel @@ -65,16 +64,13 @@ class ElecPriceImport(ElecPriceProvider, PredictionImportProvider): return "ElecPriceImport" def _update_data(self, force_update: Optional[bool] = False) -> None: - if self.config.elecprice.provider_settings.ElecPriceImport is None: - logger.debug(f"{self.provider_id()} data update without provider settings.") - return - if self.config.elecprice.provider_settings.ElecPriceImport.import_file_path: + if self.config.elecprice.elecpriceimport.import_file_path: self.import_from_file( - self.config.elecprice.provider_settings.ElecPriceImport.import_file_path, + self.config.elecprice.elecpriceimport.import_file_path, key_prefix="elecprice", ) - if self.config.elecprice.provider_settings.ElecPriceImport.import_json: + if self.config.elecprice.elecpriceimport.import_json: self.import_from_json( - self.config.elecprice.provider_settings.ElecPriceImport.import_json, + self.config.elecprice.elecpriceimport.import_json, key_prefix="elecprice", ) diff --git a/tests/test_elecpriceimport.py b/tests/test_elecpriceimport.py index 6f44596..09f20e1 100644 --- a/tests/test_elecpriceimport.py +++ b/tests/test_elecpriceimport.py @@ -18,11 +18,9 @@ def provider(sample_import_1_json, config_eos): settings = { "elecprice": { "provider": "ElecPriceImport", - "provider_settings": { - "ElecPriceImport": { - "import_file_path": str(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON), - "import_json": json.dumps(sample_import_1_json), - }, + "elecpriceimport": { + "import_file_path": str(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON), + "import_json": json.dumps(sample_import_1_json), }, } } @@ -56,10 +54,8 @@ def test_invalid_provider(provider, config_eos): settings = { "elecprice": { "provider": "", - "provider_settings": { - "ElecPriceImport": { - "import_file_path": str(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON), - }, + "elecpriceimport": { + "import_file_path": str(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON), }, } } @@ -90,11 +86,11 @@ def test_import(provider, sample_import_1_json, start_datetime, from_file, confi ems_eos = get_ems() ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin")) if from_file: - config_eos.elecprice.provider_settings.ElecPriceImport.import_json = None - assert config_eos.elecprice.provider_settings.ElecPriceImport.import_json is None + config_eos.elecprice.elecpriceimport.import_json = None + assert config_eos.elecprice.elecpriceimport.import_json is None else: - config_eos.elecprice.provider_settings.ElecPriceImport.import_file_path = None - assert config_eos.elecprice.provider_settings.ElecPriceImport.import_file_path is None + config_eos.elecprice.elecpriceimport.import_file_path = None + assert config_eos.elecprice.elecpriceimport.import_file_path is None provider.clear() # Call the method