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 <b0661n0e17e@gmail.com>
This commit is contained in:
Bobby Noelte
2025-11-16 13:26:18 +01:00
committed by GitHub
parent edff649a5e
commit 4c2997dbd6
9 changed files with 177 additions and 122 deletions

View File

@@ -8,8 +8,9 @@
| Name | Environment Variable | Type | Read-Only | Default | Description | | 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. | | 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 | `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. | | vat_rate | `EOS_ELECPRICE__VAT_RATE` | `Optional[float]` | `rw` | `1.19` | VAT rate factor applied to electricity price when charges are used. |
::: :::
<!-- pyml enable line-length --> <!-- pyml enable line-length -->
@@ -25,8 +26,41 @@
"provider": "ElecPriceAkkudoktor", "provider": "ElecPriceAkkudoktor",
"charges_kwh": 0.21, "charges_kwh": 0.21,
"vat_rate": 1.19, "vat_rate": 1.19,
"provider_settings": { "elecpriceimport": {
"ElecPriceImport": null "import_file_path": null,
"import_json": null
},
"energycharts": {
"bidding_zone": "DE-LU"
}
}
}
```
<!-- pyml enable line-length -->
### Common settings for Energy Charts electricity price provider
<!-- pyml disable line-length -->
:::{table} elecprice::energycharts
:widths: 10 10 5 5 30
:align: left
| Name | Type | Read-Only | Default | Description |
| ---- | ---- | --------- | ------- | ----------- |
| bidding_zone | `<enum 'EnergyChartsBiddingZones'>` | `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' |
:::
<!-- pyml enable line-length -->
<!-- pyml disable no-emphasis-as-heading -->
**Example Input/Output**
<!-- pyml enable no-emphasis-as-heading -->
<!-- pyml disable line-length -->
```json
{
"elecprice": {
"energycharts": {
"bidding_zone": "AT"
} }
} }
} }
@@ -36,7 +70,7 @@
### Common settings for elecprice data import from file or JSON String ### Common settings for elecprice data import from file or JSON String
<!-- pyml disable line-length --> <!-- pyml disable line-length -->
:::{table} elecprice::provider_settings::ElecPriceImport :::{table} elecprice::elecpriceimport
:widths: 10 10 5 5 30 :widths: 10 10 5 5 30
:align: left :align: left
@@ -55,42 +89,11 @@
```json ```json
{ {
"elecprice": { "elecprice": {
"provider_settings": { "elecpriceimport": {
"ElecPriceImport": {
"import_file_path": null, "import_file_path": null,
"import_json": "{\"elecprice_marketprice_wh\": [0.0003384, 0.0003318, 0.0003284]}" "import_json": "{\"elecprice_marketprice_wh\": [0.0003384, 0.0003318, 0.0003284]}"
} }
} }
} }
}
```
<!-- pyml enable line-length -->
### Electricity Price Prediction Provider Configuration
<!-- pyml disable line-length -->
:::{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 |
:::
<!-- pyml enable line-length -->
<!-- pyml disable no-emphasis-as-heading -->
**Example Input/Output**
<!-- pyml enable no-emphasis-as-heading -->
<!-- pyml disable line-length -->
```json
{
"elecprice": {
"provider_settings": {
"ElecPriceImport": null
}
}
}
``` ```
<!-- pyml enable line-length --> <!-- pyml enable line-length -->

View File

@@ -71,8 +71,12 @@
"provider": "ElecPriceAkkudoktor", "provider": "ElecPriceAkkudoktor",
"charges_kwh": 0.21, "charges_kwh": 0.21,
"vat_rate": 1.19, "vat_rate": 1.19,
"provider_settings": { "elecpriceimport": {
"ElecPriceImport": null "import_file_path": null,
"import_json": null
},
"energycharts": {
"bidding_zone": "DE-LU"
} }
}, },
"ems": { "ems": {

View File

@@ -124,8 +124,9 @@ Configuration options:
- `charges_kwh`: Electricity price charges (€/kWh). - `charges_kwh`: Electricity price charges (€/kWh).
- `vat_rate`: VAT rate factor applied to electricity price when charges are used (default: 1.19). - `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. - `elecpriceimport.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_json`: JSON string, dictionary of electricity price forecast value lists.
- `energycharts.bidding_zone`: Bidding zone Energy Charts shall provide price data for.
### ElecPriceAkkudoktor Provider ### ElecPriceAkkudoktor Provider

View File

@@ -2469,7 +2469,10 @@
"$ref": "#/components/schemas/ElecPriceCommonSettings-Output", "$ref": "#/components/schemas/ElecPriceCommonSettings-Output",
"default": { "default": {
"vat_rate": 1.19, "vat_rate": 1.19,
"provider_settings": {} "elecpriceimport": {},
"energycharts": {
"bidding_zone": "DE-LU"
}
} }
}, },
"feedintariff": { "feedintariff": {
@@ -2975,27 +2978,6 @@
"title": "DevicesCommonSettings", "title": "DevicesCommonSettings",
"description": "Base configuration for devices simulation settings." "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": { "ElecPriceCommonSettings-Input": {
"properties": { "properties": {
"provider": { "provider": {
@@ -3046,12 +3028,13 @@
1.19 1.19
] ]
}, },
"provider_settings": { "elecpriceimport": {
"$ref": "#/components/schemas/ElecPriceCommonProviderSettings", "$ref": "#/components/schemas/ElecPriceImportCommonSettings",
"description": "Provider settings", "description": "Import provider settings."
"examples": [ },
{} "energycharts": {
] "$ref": "#/components/schemas/ElecPriceEnergyChartsCommonSettings",
"description": "Energy Charts provider settings."
} }
}, },
"type": "object", "type": "object",
@@ -3108,18 +3091,34 @@
1.19 1.19
] ]
}, },
"provider_settings": { "elecpriceimport": {
"$ref": "#/components/schemas/ElecPriceCommonProviderSettings", "$ref": "#/components/schemas/ElecPriceImportCommonSettings",
"description": "Provider settings", "description": "Import provider settings."
"examples": [ },
{} "energycharts": {
] "$ref": "#/components/schemas/ElecPriceEnergyChartsCommonSettings",
"description": "Energy Charts provider settings."
} }
}, },
"type": "object", "type": "object",
"title": "ElecPriceCommonSettings", "title": "ElecPriceCommonSettings",
"description": "Electricity Price Prediction Configuration." "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": { "ElecPriceImportCommonSettings": {
"properties": { "properties": {
"import_file_path": { "import_file_path": {
@@ -3375,6 +3374,29 @@
"title": "ElectricVehicleResult", "title": "ElectricVehicleResult",
"description": "Result class containing information related to the electric vehicle's charging and discharging behavior." "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": { "EnergyManagementCommonSettings": {
"properties": { "properties": {
"startup_delay": { "startup_delay": {

View File

@@ -21,11 +21,14 @@ if TYPE_CHECKING:
# - tuple[str, Callable[[Any], Any]] (new path + transform) # - tuple[str, Callable[[Any], Any]] (new path + transform)
# - None (drop) # - None (drop)
MIGRATION_MAP: Dict[str, Union[str, Tuple[str, Callable[[Any], Any]], None]] = { 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/batteries/0/initial_soc_percentage": None,
"devices/electric_vehicles/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_file_path": "elecprice/elecpriceimport/import_file_path",
"elecprice/provider_settings/import_json": "elecprice/provider_settings/ElecPriceImport/import_json", "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_file_path": "load/provider_settings/LoadImport/import_file_path",
"load/provider_settings/import_json": "load/provider_settings/LoadImport/import_json", "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", "load/provider_settings/loadakkudoktor_year_energy": "load/provider_settings/LoadAkkudoktor/loadakkudoktor_year_energy_kwh",

View File

@@ -4,6 +4,9 @@ from pydantic import Field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider
from akkudoktoreos.prediction.elecpriceenergycharts import (
ElecPriceEnergyChartsCommonSettings,
)
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImportCommonSettings from akkudoktoreos.prediction.elecpriceimport import ElecPriceImportCommonSettings
from akkudoktoreos.prediction.prediction import get_prediction 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): class ElecPriceCommonSettings(SettingsBaseModel):
"""Electricity Price Prediction Configuration.""" """Electricity Price Prediction Configuration."""
@@ -53,17 +47,14 @@ class ElecPriceCommonSettings(SettingsBaseModel):
}, },
) )
provider_settings: ElecPriceCommonProviderSettings = Field( elecpriceimport: ElecPriceImportCommonSettings = Field(
default_factory=ElecPriceCommonProviderSettings, default_factory=ElecPriceImportCommonSettings,
json_schema_extra={ json_schema_extra={"description": "Import provider settings."},
"description": "Provider settings", )
"examples": [
# Example 1: Empty/default settings (all providers None) energycharts: ElecPriceEnergyChartsCommonSettings = Field(
{ default_factory=ElecPriceEnergyChartsCommonSettings,
"ElecPriceImport": None, json_schema_extra={"description": "Energy Charts provider settings."},
},
],
},
) )
# Validators # Validators

View File

@@ -7,21 +7,44 @@ format, enabling consistent access to forecasted and historical electricity pric
""" """
from datetime import datetime from datetime import datetime
from enum import Enum
from typing import Any, List, Optional, Union from typing import Any, List, Optional, Union
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import requests import requests
from loguru import logger from loguru import logger
from pydantic import ValidationError from pydantic import Field, ValidationError
from statsmodels.tsa.holtwinters import ExponentialSmoothing from statsmodels.tsa.holtwinters import ExponentialSmoothing
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.cache import cache_in_file from akkudoktoreos.core.cache import cache_in_file
from akkudoktoreos.core.pydantic import PydanticBaseModel from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration 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): class EnergyChartsElecPrice(PydanticBaseModel):
license_info: str license_info: str
unix_seconds: List[int] unix_seconds: List[int]
@@ -30,6 +53,21 @@ class EnergyChartsElecPrice(PydanticBaseModel):
deprecated: bool 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): class ElecPriceEnergyCharts(ElecPriceProvider):
"""Fetch and process electricity price forecast data from Energy-Charts. """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") 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) response = requests.get(url, timeout=30)
logger.debug(f"Response from {url}: {response}") logger.debug(f"Response from {url}: {response}")
response.raise_for_status() # Raise an error for bad responses response.raise_for_status() # Raise an error for bad responses

View File

@@ -9,7 +9,6 @@ format, enabling consistent access to forecasted and historical elecprice attrib
from pathlib import Path from pathlib import Path
from typing import Optional, Union from typing import Optional, Union
from loguru import logger
from pydantic import Field, field_validator from pydantic import Field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
@@ -65,16 +64,13 @@ class ElecPriceImport(ElecPriceProvider, PredictionImportProvider):
return "ElecPriceImport" return "ElecPriceImport"
def _update_data(self, force_update: Optional[bool] = False) -> None: def _update_data(self, force_update: Optional[bool] = False) -> None:
if self.config.elecprice.provider_settings.ElecPriceImport is None: if self.config.elecprice.elecpriceimport.import_file_path:
logger.debug(f"{self.provider_id()} data update without provider settings.")
return
if self.config.elecprice.provider_settings.ElecPriceImport.import_file_path:
self.import_from_file( self.import_from_file(
self.config.elecprice.provider_settings.ElecPriceImport.import_file_path, self.config.elecprice.elecpriceimport.import_file_path,
key_prefix="elecprice", key_prefix="elecprice",
) )
if self.config.elecprice.provider_settings.ElecPriceImport.import_json: if self.config.elecprice.elecpriceimport.import_json:
self.import_from_json( self.import_from_json(
self.config.elecprice.provider_settings.ElecPriceImport.import_json, self.config.elecprice.elecpriceimport.import_json,
key_prefix="elecprice", key_prefix="elecprice",
) )

View File

@@ -18,12 +18,10 @@ def provider(sample_import_1_json, config_eos):
settings = { settings = {
"elecprice": { "elecprice": {
"provider": "ElecPriceImport", "provider": "ElecPriceImport",
"provider_settings": { "elecpriceimport": {
"ElecPriceImport": {
"import_file_path": str(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON), "import_file_path": str(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON),
"import_json": json.dumps(sample_import_1_json), "import_json": json.dumps(sample_import_1_json),
}, },
},
} }
} }
config_eos.merge_settings_from_dict(settings) config_eos.merge_settings_from_dict(settings)
@@ -56,11 +54,9 @@ def test_invalid_provider(provider, config_eos):
settings = { settings = {
"elecprice": { "elecprice": {
"provider": "<invalid>", "provider": "<invalid>",
"provider_settings": { "elecpriceimport": {
"ElecPriceImport": {
"import_file_path": str(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON), "import_file_path": str(FILE_TESTDATA_ELECPRICEIMPORT_1_JSON),
}, },
},
} }
} }
with pytest.raises(ValueError, match="not a valid electricity price provider"): with pytest.raises(ValueError, match="not a valid electricity price provider"):
@@ -90,11 +86,11 @@ def test_import(provider, sample_import_1_json, start_datetime, from_file, confi
ems_eos = get_ems() ems_eos = get_ems()
ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin")) ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin"))
if from_file: if from_file:
config_eos.elecprice.provider_settings.ElecPriceImport.import_json = None config_eos.elecprice.elecpriceimport.import_json = None
assert config_eos.elecprice.provider_settings.ElecPriceImport.import_json is None assert config_eos.elecprice.elecpriceimport.import_json is None
else: else:
config_eos.elecprice.provider_settings.ElecPriceImport.import_file_path = None config_eos.elecprice.elecpriceimport.import_file_path = None
assert config_eos.elecprice.provider_settings.ElecPriceImport.import_file_path is None assert config_eos.elecprice.elecpriceimport.import_file_path is None
provider.clear() provider.clear()
# Call the method # Call the method