mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-08-31 12:46:38 +00:00
Fix seasonal SMARD retail price forecast
This commit is contained in:
@@ -91,6 +91,10 @@ class ElecPriceEnergyCharts(ElecPriceProvider):
|
|||||||
|
|
||||||
highest_orig_datetime: Optional[datetime] = None
|
highest_orig_datetime: Optional[datetime] = None
|
||||||
|
|
||||||
|
def historic_hours_min(self) -> int:
|
||||||
|
"""Keep enough market-price history for weekly seasonal extrapolation."""
|
||||||
|
return 24 * 35
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def provider_id(cls) -> str:
|
def provider_id(cls) -> str:
|
||||||
"""Return the unique identifier for the Energy-Charts provider."""
|
"""Return the unique identifier for the Energy-Charts provider."""
|
||||||
@@ -246,25 +250,41 @@ class ElecPriceEnergyCharts(ElecPriceProvider):
|
|||||||
if not self.ems_start_datetime:
|
if not self.ems_start_datetime:
|
||||||
raise ValueError(f"Start DateTime not set: {self.ems_start_datetime}")
|
raise ValueError(f"Start DateTime not set: {self.ems_start_datetime}")
|
||||||
|
|
||||||
# Determine if update is needed and how many days
|
# Determine if an update or history repair is needed and how many days to request.
|
||||||
past_days = 35
|
past_days = 35
|
||||||
|
needs_history_refresh = False
|
||||||
if self.highest_orig_datetime:
|
if self.highest_orig_datetime:
|
||||||
history_series = self.key_to_series(
|
raw_history = self.key_to_series(
|
||||||
key="elecprice_marketprice_wh", start_datetime=self.ems_start_datetime
|
key="elecprice_marketprice_wh",
|
||||||
|
end_datetime=to_datetime(self.highest_orig_datetime).add(seconds=1),
|
||||||
)
|
)
|
||||||
# If history lower, then start_datetime
|
|
||||||
if history_series.index.min() <= self.ems_start_datetime:
|
# Do not mistake the current forecast window for sufficient ETS history. This also
|
||||||
|
# repairs installations that retained only the previous 48-hour default history.
|
||||||
|
if not raw_history.empty:
|
||||||
|
resolution_seconds = self._resolution_seconds(raw_history)
|
||||||
|
slots_per_hour = 3600 // resolution_seconds
|
||||||
|
needs_history_refresh = len(raw_history) <= 800 * slots_per_hour
|
||||||
|
else:
|
||||||
|
needs_history_refresh = True
|
||||||
|
|
||||||
|
if not needs_history_refresh and not force_update:
|
||||||
past_days = 0
|
past_days = 0
|
||||||
|
|
||||||
needs_update = end > self.highest_orig_datetime
|
needs_update = (
|
||||||
|
bool(force_update) or end > self.highest_orig_datetime or needs_history_refresh
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
needs_update = True
|
needs_update = True
|
||||||
|
|
||||||
if needs_update:
|
if needs_update:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Update {} is needed, last in history: {}",
|
"Update {} is needed, last in history: {}, "
|
||||||
|
"force_update={}, history_refresh={}",
|
||||||
self.provider_id(),
|
self.provider_id(),
|
||||||
self.highest_orig_datetime,
|
self.highest_orig_datetime,
|
||||||
|
bool(force_update),
|
||||||
|
needs_history_refresh,
|
||||||
)
|
)
|
||||||
# Set start_date try to take data from 5 weeks back for prediction
|
# Set start_date try to take data from 5 weeks back for prediction
|
||||||
start_date = to_datetime(
|
start_date = to_datetime(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from pathlib import Path
|
|||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -63,6 +64,11 @@ def test_singleton_instance(provider):
|
|||||||
assert provider is another_instance
|
assert provider is another_instance
|
||||||
|
|
||||||
|
|
||||||
|
def test_keeps_weekly_price_history(provider):
|
||||||
|
"""Retain enough native-resolution values for the weekly ETS forecast."""
|
||||||
|
assert provider.historic_hours_min() == 24 * 35
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_provider(provider, monkeypatch):
|
def test_invalid_provider(provider, monkeypatch):
|
||||||
"""Test requesting an unsupported provider."""
|
"""Test requesting an unsupported provider."""
|
||||||
monkeypatch.setenv("EOS_ELECPRICE__ELECPRICE_PROVIDER", "<invalid>")
|
monkeypatch.setenv("EOS_ELECPRICE__ELECPRICE_PROVIDER", "<invalid>")
|
||||||
@@ -159,6 +165,53 @@ def test_update_data_keeps_quarter_hour_resolution(provider):
|
|||||||
assert result.index.to_series().diff().dropna().dt.total_seconds().unique().tolist() == [900.0]
|
assert result.index.to_series().diff().dropna().dt.total_seconds().unique().tolist() == [900.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_data_repairs_short_quarter_hour_history(provider):
|
||||||
|
"""A previously retained 48-hour series is replaced with the full ETS history."""
|
||||||
|
start = to_datetime("2026-08-01 00:00:00", in_timezone="Europe/Berlin")
|
||||||
|
get_ems().set_start_datetime(start)
|
||||||
|
provider.highest_orig_datetime = start.add(hours=24)
|
||||||
|
short_history = pd.Series(
|
||||||
|
0.0001,
|
||||||
|
index=pd.date_range(start=start.subtract(hours=48), periods=192, freq="15min"),
|
||||||
|
)
|
||||||
|
weekly_history = pd.Series(
|
||||||
|
0.0001,
|
||||||
|
index=pd.date_range(start=start.subtract(days=35), periods=3204, freq="15min"),
|
||||||
|
)
|
||||||
|
refreshed_data = EnergyChartsElecPrice(
|
||||||
|
license_info="",
|
||||||
|
unix_seconds=[int(provider.highest_orig_datetime.timestamp())],
|
||||||
|
price=[100.0],
|
||||||
|
unit="EUR/MWh",
|
||||||
|
deprecated=False,
|
||||||
|
)
|
||||||
|
predicted_slots = provider.config.prediction.hours * 4 - 97
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
ElecPriceEnergyCharts,
|
||||||
|
"key_to_series",
|
||||||
|
side_effect=[short_history, weekly_history],
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
ElecPriceEnergyCharts, "key_to_array", return_value=weekly_history.to_numpy()
|
||||||
|
),
|
||||||
|
patch.object(ElecPriceEnergyCharts, "key_from_series"),
|
||||||
|
patch.object(
|
||||||
|
ElecPriceEnergyCharts, "_request_forecast", return_value=refreshed_data
|
||||||
|
) as request,
|
||||||
|
patch.object(
|
||||||
|
ElecPriceEnergyCharts,
|
||||||
|
"_predict_ets",
|
||||||
|
return_value=np.full(predicted_slots, 0.0001),
|
||||||
|
) as predict,
|
||||||
|
):
|
||||||
|
provider._update_data()
|
||||||
|
|
||||||
|
assert request.call_args.kwargs["start_date"] == "2026-06-27"
|
||||||
|
assert predict.call_args.kwargs["seasonal_periods"] == 168 * 4
|
||||||
|
|
||||||
|
|
||||||
def test_parse_data_adds_constant_charges_variable_network_fees_and_vat(provider):
|
def test_parse_data_adds_constant_charges_variable_network_fees_and_vat(provider):
|
||||||
"""Build the gross retail price from market price and the matching Module 3 fee."""
|
"""Build the gross retail price from market price and the matching Module 3 fee."""
|
||||||
provider.config.elecprice.charges_kwh = None
|
provider.config.elecprice.charges_kwh = None
|
||||||
|
|||||||
Reference in New Issue
Block a user