feat: add fixed electricity prediction with time window support (#930)
Some checks failed
Bump Version / Bump Version Workflow (push) Has been cancelled
docker-build / platform-excludes (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
Close stale pull requests/issues / Find Stale issues and PRs (push) Has been cancelled

Add a fixed electricity prediction that supports prices per time window.
The time windows may flexible be defined by day or date.

The prediction documentation is updated to also cover the ElecPriceFixed
provider.

The feature includes several changes that are not directly related to the
electricity price prediction implementation but are necessary to keep
EOS running properly and to test and document the changes.

* feat: add value time windows

    Add time windows with an associated float value.

* feat: harden eos measurements endpoints error detection and reporting

    Cover more errors that may be raised during endpoint access. Report the
    errors including trace information to ease debugging.

* feat: extend server configuration to cover all arguments

    Make the argument controlled options also available in server configuration.

* fix: eos config configuration by cli arguments

    Move the command line argument handling to config eos so that it is
    excuted whenever eos config is rebuild or reset.

* chore: extend measurement endpoint system test

* chore: refactor time windows

    Move time windows to configabc as they are only used in configurations.
    Also move all tests to test_configabc.

* chore: provide config update errors in eosdash with summarized error text

    If there is an update error provide the error text as a summary. On click
    provide the full error text.

* chore: force eosdash ip address and port in makefile dev run

    Ensure eosdash ip address and port are correctly set for development runs.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
Bobby Noelte
2026-03-11 17:18:45 +01:00
committed by GitHub
parent 850d6b7c74
commit cf477d91a3
35 changed files with 3778 additions and 1491 deletions

View File

@@ -8,6 +8,7 @@ from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider
from akkudoktoreos.prediction.elecpriceenergycharts import (
ElecPriceEnergyChartsCommonSettings,
)
from akkudoktoreos.prediction.elecpricefixed import ElecPriceFixedCommonSettings
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImportCommonSettings
@@ -37,6 +38,7 @@ class ElecPriceCommonSettings(SettingsBaseModel):
"examples": ["ElecPriceAkkudoktor"],
},
)
charges_kwh: Optional[float] = Field(
default=None,
ge=0,
@@ -45,6 +47,7 @@ class ElecPriceCommonSettings(SettingsBaseModel):
"examples": [0.21],
},
)
vat_rate: Optional[float] = Field(
default=1.19,
ge=0,
@@ -54,6 +57,11 @@ class ElecPriceCommonSettings(SettingsBaseModel):
},
)
elecpricefixed: ElecPriceFixedCommonSettings = Field(
default_factory=ElecPriceFixedCommonSettings,
json_schema_extra={"description": "Fixed electricity price provider settings."},
)
elecpriceimport: ElecPriceImportCommonSettings = Field(
default_factory=ElecPriceImportCommonSettings,
json_schema_extra={"description": "Import provider settings."},

View File

@@ -0,0 +1,111 @@
"""Provides fixed price electricity price data."""
from typing import Optional
from loguru import logger
from pydantic import Field
from akkudoktoreos.config.configabc import (
SettingsBaseModel,
ValueTimeWindowSequence,
)
from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider
from akkudoktoreos.utils.datetimeutil import to_duration
class ElecPriceFixedCommonSettings(SettingsBaseModel):
"""Common configuration settings for fixed electricity pricing.
This model defines a fixed electricity price schedule using a sequence
of time windows. Each window specifies a time interval and the electricity
price applicable during that interval.
"""
time_windows: ValueTimeWindowSequence = Field(
default_factory=ValueTimeWindowSequence,
json_schema_extra={
"description": (
"Sequence of time windows defining the fixed "
"price schedule. If not provided, no fixed pricing is applied."
),
"examples": [
{
"windows": [
{"start_time": "00:00", "duration": "8 hours", "value": 0.288},
{"start_time": "08:00", "duration": "16 hours", "value": 0.34},
],
}
],
},
)
class ElecPriceFixed(ElecPriceProvider):
"""Fixed price electricity price data.
ElecPriceFixed is a singleton-based class that retrieves electricity price data
from a fixed schedule defined by time windows.
The provider generates hourly electricity prices based on the configured time windows.
For each hour in the forecast period, it determines which time window applies and
assigns the corresponding price.
Attributes:
time_windows: Sequence of time windows with associated electricity prices.
"""
@classmethod
def provider_id(cls) -> str:
"""Return the unique identifier for the ElecPriceFixed provider."""
return "ElecPriceFixed"
def _update_data(self, force_update: Optional[bool] = False) -> None:
"""Update electricity price data from fixed schedule.
Generates electricity prices based on the configured time windows
at the optimization interval granularity. The price sequence starts
synchronized to the wall clock at the next full interval boundary.
Args:
force_update: If True, forces update even if data exists.
Raises:
ValueError: If no time windows are configured.
"""
time_windows_seq = self.config.elecprice.elecpricefixed.time_windows
if time_windows_seq is None or not time_windows_seq.windows:
error_msg = "No time windows configured for fixed electricity price"
logger.error(error_msg)
raise ValueError(error_msg)
start_datetime = self.ems_start_datetime
interval_seconds = self.config.optimization.interval
total_hours = self.config.prediction.hours
interval = to_duration(interval_seconds)
end_datetime = start_datetime.add(hours=total_hours)
logger.debug(
f"Generating fixed electricity prices for {total_hours} hours "
f"starting at {start_datetime}"
)
# Build the full price array in one call — kWh values aligned to the
# optimization grid. to_array mirrors the key_to_array signature so
# the grid is constructed identically to how prediction data is read.
prices_kwh = time_windows_seq.to_array(
start_datetime=start_datetime,
end_datetime=end_datetime,
interval=interval,
dropna=True,
boundary="context",
align_to_interval=True,
)
# Convert kWh → Wh and store one entry per interval step.
for idx, price_kwh in enumerate(prices_kwh):
current_dt = start_datetime.add(seconds=idx * interval_seconds)
self.update_value(current_dt, "elecprice_marketprice_wh", price_kwh / 1000.0)
logger.debug(f"Successfully generated {len(prices_kwh)} fixed electricity price entries")

View File

@@ -33,6 +33,7 @@ from pydantic import Field
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.prediction.elecpriceakkudoktor import ElecPriceAkkudoktor
from akkudoktoreos.prediction.elecpriceenergycharts import ElecPriceEnergyCharts
from akkudoktoreos.prediction.elecpricefixed import ElecPriceFixed
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImport
from akkudoktoreos.prediction.feedintarifffixed import FeedInTariffFixed
from akkudoktoreos.prediction.feedintariffimport import FeedInTariffImport
@@ -72,6 +73,7 @@ class PredictionCommonSettings(SettingsBaseModel):
# Initialize forecast providers, all are singletons.
elecprice_akkudoktor = ElecPriceAkkudoktor()
elecprice_energy_charts = ElecPriceEnergyCharts()
elecprice_fixed = ElecPriceFixed()
elecprice_import = ElecPriceImport()
feedintariff_fixed = FeedInTariffFixed()
feedintariff_import = FeedInTariffImport()
@@ -91,6 +93,7 @@ def prediction_providers() -> list[
Union[
ElecPriceAkkudoktor,
ElecPriceEnergyCharts,
ElecPriceFixed,
ElecPriceImport,
FeedInTariffFixed,
FeedInTariffImport,
@@ -110,6 +113,7 @@ def prediction_providers() -> list[
global \
elecprice_akkudoktor, \
elecprice_energy_charts, \
elecprice_fixed, \
elecprice_import, \
feedintariff_fixed, \
feedintariff_import, \
@@ -128,6 +132,7 @@ def prediction_providers() -> list[
return [
elecprice_akkudoktor,
elecprice_energy_charts,
elecprice_fixed,
elecprice_import,
feedintariff_fixed,
feedintariff_import,
@@ -151,6 +156,7 @@ class Prediction(PredictionContainer):
Union[
ElecPriceAkkudoktor,
ElecPriceEnergyCharts,
ElecPriceFixed,
ElecPriceImport,
FeedInTariffFixed,
FeedInTariffImport,