Add resilient market feed-in tariff providers

This commit is contained in:
Andreas
2026-07-15 16:10:34 +02:00
parent 67cf6f7d8a
commit d4056af0f6
15 changed files with 928 additions and 45 deletions
@@ -29,6 +29,10 @@ from akkudoktoreos.optimization.genetic.geneticdevices import (
)
from akkudoktoreos.utils.datetimeutil import to_duration
MARKET_PRICE_FEED_IN_TARIFF_PROVIDERS = frozenset(
{"FeedInTariffAkkudoktor", "FeedInTariffEnergyCharts", "FeedInTariffTibber"}
)
# Do not import directly from akkudoktoreos.core.coreabc
# EnergyManagementSystemMixin - Creates circular dependency with ems.py
# StartMixin - Creates circular dependency with ems.py
@@ -161,8 +165,7 @@ class GeneticOptimizationParameters(
dishwasher = self.__dict__.get("dishwasher")
if dishwasher is not None and self.home_appliances is not None:
raise ValueError(
"Provide either 'home_appliances' or the deprecated 'dishwasher', "
"not both."
"Provide either 'home_appliances' or the deprecated 'dishwasher', " "not both."
)
appliances = self.home_appliances or []
device_ids = [appliance.device_id for appliance in appliances]
@@ -405,7 +408,7 @@ class GeneticOptimizationParameters(
# Retry
continue
if cls.config.feedintariff.direct_marketing_enabled:
if cls.config.feedintariff.provider == "FeedInTariffEnergyCharts":
if cls.config.feedintariff.provider in MARKET_PRICE_FEED_IN_TARIFF_PROVIDERS:
try:
feed_in_tariff_wh = cls.prediction.key_to_array(
key="feed_in_tariff_wh",
@@ -28,16 +28,19 @@ query TibberPriceInfo {
today {
startsAt
total
energy
}
tomorrow {
startsAt
total
energy
}
}
priceInfoRange(resolution: QUARTER_HOURLY, last: 672) {
nodes {
startsAt
total
energy
}
}
}
@@ -61,16 +64,19 @@ query TibberPriceInfo {
today {
startsAt
total
energy
}
tomorrow {
startsAt
total
energy
}
}
priceInfoRange(resolution: QUARTER_HOURLY, last: 672) {
nodes {
startsAt
total
energy
}
}
}
@@ -107,6 +113,7 @@ class TibberPricePoint(PydanticBaseModel):
startsAt: str
total: float
energy: Optional[float] = None
class TibberPriceConnection(PydanticBaseModel):
+23 -1
View File
@@ -5,11 +5,15 @@ from pydantic import Field, computed_field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.coreabc import get_prediction
from akkudoktoreos.prediction.feedintariffabc import FeedInTariffProvider
from akkudoktoreos.prediction.feedintariffakkudoktor import (
FeedInTariffAkkudoktorCommonSettings,
)
from akkudoktoreos.prediction.feedintariffenergycharts import (
FeedInTariffEnergyChartsCommonSettings,
)
from akkudoktoreos.prediction.feedintarifffixed import FeedInTariffFixedCommonSettings
from akkudoktoreos.prediction.feedintariffimport import FeedInTariffImportCommonSettings
from akkudoktoreos.prediction.feedintarifftibber import FeedInTariffTibberCommonSettings
def elecprice_provider_ids() -> list[str]:
@@ -19,7 +23,13 @@ def elecprice_provider_ids() -> list[str]:
except:
# Prediction may not be initialized
# Return at least provider used in example
return ["FeedInTariffFixed", "FeedInTariffEnergyCharts", "FeedInTariffImport"]
return [
"FeedInTariffAkkudoktor",
"FeedInTariffFixed",
"FeedInTariffEnergyCharts",
"FeedInTariffImport",
"FeedInTariffTibber",
]
return [
provider.provider_id()
@@ -31,6 +41,10 @@ def elecprice_provider_ids() -> list[str]:
class FeedInTariffCommonProviderSettings(SettingsBaseModel):
"""Feed In Tariff Prediction Provider Configuration."""
FeedInTariffAkkudoktor: Optional[FeedInTariffAkkudoktorCommonSettings] = Field(
default=None,
json_schema_extra={"description": "FeedInTariffAkkudoktor settings", "examples": [None]},
)
FeedInTariffFixed: Optional[FeedInTariffFixedCommonSettings] = Field(
default=None,
json_schema_extra={"description": "FeedInTariffFixed settings", "examples": [None]},
@@ -43,6 +57,10 @@ class FeedInTariffCommonProviderSettings(SettingsBaseModel):
default=None,
json_schema_extra={"description": "FeedInTariffImport settings", "examples": [None]},
)
FeedInTariffTibber: Optional[FeedInTariffTibberCommonSettings] = Field(
default=None,
json_schema_extra={"description": "FeedInTariffTibber settings", "examples": [None]},
)
class FeedInTariffCommonSettings(SettingsBaseModel):
@@ -61,9 +79,11 @@ class FeedInTariffCommonSettings(SettingsBaseModel):
json_schema_extra={
"description": "Feed in tariff provider id of provider to be used.",
"examples": [
"FeedInTariffAkkudoktor",
"FeedInTariffFixed",
"FeedInTariffEnergyCharts",
"FeedInTariffImport",
"FeedInTariffTibber",
],
},
)
@@ -75,9 +95,11 @@ class FeedInTariffCommonSettings(SettingsBaseModel):
"examples": [
# Example 1: Empty/default settings (all providers None)
{
"FeedInTariffAkkudoktor": None,
"FeedInTariffFixed": None,
"FeedInTariffEnergyCharts": None,
"FeedInTariffImport": None,
"FeedInTariffTibber": None,
},
],
},
@@ -0,0 +1,163 @@
"""Provide feed-in tariff data from Akkudoktor market prices."""
import time
from datetime import datetime
from typing import Optional
import numpy as np
import pandas as pd
import requests
from loguru import logger
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.cache import cache_in_file
from akkudoktoreos.prediction.elecpriceakkudoktor import (
AkkudoktorElecPrice,
ElecPriceAkkudoktor,
)
from akkudoktoreos.prediction.feedintariffabc import FeedInTariffProvider
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration
class FeedInTariffAkkudoktorCommonSettings(SettingsBaseModel):
"""Settings for the Akkudoktor feed-in tariff provider.
The public Akkudoktor price endpoint only needs the timezone already
configured in ``general.timezone``, so no provider-specific values are
currently required.
"""
class FeedInTariffAkkudoktor(FeedInTariffProvider):
"""Use raw Akkudoktor day-ahead market prices as feed-in tariff data.
The upstream aWATTar endpoint currently supplies hourly values. EOS stores
those source values unchanged; consumers requesting a shorter interval can
forward-fill them onto the optimization grid.
Electricity import charges and VAT are intentionally not added. Prices
returned in EUR/MWh are converted to EUR/Wh and stored under
``feed_in_tariff_wh``.
"""
highest_orig_datetime: Optional[datetime] = None
def historic_hours_min(self) -> int:
"""Keep enough history for weekly seasonal price extrapolation."""
return 24 * 35
@classmethod
def provider_id(cls) -> str:
"""Return the unique provider identifier."""
return "FeedInTariffAkkudoktor"
@cache_in_file(with_ttl="1 hour")
def _request_forecast(self) -> AkkudoktorElecPrice:
"""Fetch market prices from the public Akkudoktor API."""
if not self.ems_start_datetime:
raise ValueError(f"Start DateTime not set: {self.ems_start_datetime}")
start_date = to_datetime(
self.ems_start_datetime - to_duration("35 days"), as_string="YYYY-MM-DD"
)
end_date = to_datetime(self.end_datetime, as_string="YYYY-MM-DD")
timezone = self.config.general.timezone
url = (
"https://api.akkudoktor.net/prices" f"?start={start_date}&end={end_date}&tz={timezone}"
)
max_attempts = 3
last_exc: Optional[Exception] = None
for attempt in range(1, max_attempts + 1):
try:
response = requests.get(url, timeout=(5, 20))
logger.debug("Response from {}: {}", url, response)
response.raise_for_status()
data = ElecPriceAkkudoktor._validate_data(response.content)
self.update_datetime = to_datetime(in_timezone=timezone)
return data
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as exc:
last_exc = exc
logger.warning(
"Akkudoktor feed-in tariff request attempt {}/{} failed: {}",
attempt,
max_attempts,
exc,
)
if attempt < max_attempts:
time.sleep(2 * attempt)
raise last_exc # type: ignore[misc]
def _parse_data(self, data: AkkudoktorElecPrice) -> pd.Series:
"""Convert raw EUR/MWh values to a timezone-aware EUR/Wh series."""
series = pd.Series(dtype=float)
for value in data.values:
timestamp = to_datetime(value.start, in_timezone=self.config.general.timezone)
series.at[timestamp] = value.marketprice / 1_000_000
return series
def _predict_prices(self, history: np.ndarray, hours: int) -> np.ndarray:
"""Extend published prices to the configured prediction horizon."""
predictor = ElecPriceAkkudoktor()
if len(history) > 800:
return predictor._predict_ets(history, seasonal_periods=168, hours=hours)
if len(history) > 168:
return predictor._predict_ets(history, seasonal_periods=24, hours=hours)
if len(history) > 0:
logger.warning(
"Using median fallback for Akkudoktor feed-in tariff with only {} values.",
len(history),
)
return predictor._predict_median(history, hours=hours)
raise ValueError("No Akkudoktor feed-in tariff data available")
def _update_data(self, force_update: Optional[bool] = False) -> None:
"""Update raw prices and extrapolate any missing horizon values."""
if not self.ems_start_datetime:
raise ValueError(f"Start DateTime not set: {self.ems_start_datetime}")
try:
data = self._request_forecast(force_update=force_update) # type: ignore[call-arg]
series = self._parse_data(data)
if series.empty:
raise ValueError("No Akkudoktor feed-in tariff data available")
self.highest_orig_datetime = to_datetime(
series.index.max(), in_timezone=self.config.general.timezone
)
self.key_from_series("feed_in_tariff_wh", series)
except Exception as exc:
if self.highest_orig_datetime is None:
raise
logger.warning(
"Akkudoktor feed-in tariff update failed ({}); retaining existing data.",
exc,
)
if self.highest_orig_datetime is None:
raise ValueError("Highest original datetime not available")
history = np.asarray(
self.key_to_array(
key="feed_in_tariff_wh",
end_datetime=self.highest_orig_datetime,
fill_method="linear",
),
dtype=float,
)
covered_hours = (
int((self.highest_orig_datetime - self.ems_start_datetime).total_seconds() // 3600) + 1
)
needed_hours = self.config.prediction.hours - max(covered_hours, 0)
if needed_hours <= 0:
return
prediction = self._predict_prices(history, needed_hours)
prediction_series = pd.Series(
data=prediction,
index=[
self.highest_orig_datetime + to_duration(f"{i + 1} hours")
for i in range(len(prediction))
],
)
self.key_from_series("feed_in_tariff_wh", prediction_series)
@@ -1,5 +1,6 @@
"""Provides feed-in tariff data from Energy-Charts market prices."""
import time
from datetime import datetime
from typing import Optional
@@ -44,6 +45,10 @@ class FeedInTariffEnergyCharts(FeedInTariffProvider):
highest_orig_datetime: Optional[datetime] = None
def historic_hours_min(self) -> int:
"""Keep enough history for weekly seasonal price extrapolation."""
return 24 * 35
@classmethod
def provider_id(cls) -> str:
"""Return the unique identifier for the Energy-Charts feed-in tariff provider."""
@@ -69,12 +74,34 @@ class FeedInTariffEnergyCharts(FeedInTariffProvider):
last_date = to_datetime(self.end_datetime, as_string="YYYY-MM-DD")
url = f"{source}/price?bzn={self._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()
energy_charts_data = ElecPriceEnergyCharts._validate_data(response.content)
self.update_datetime = to_datetime(in_timezone=self.config.general.timezone)
return energy_charts_data
# Retry transient network problems (timeouts / connection resets) a few
# times with a short backoff. Uses a (connect, read) timeout tuple so a
# slow-to-respond API does not block forever but also is not aborted
# after a too-short single read window.
max_attempts = 3
last_exc: Optional[Exception] = None
for attempt in range(1, max_attempts + 1):
try:
response = requests.get(url, timeout=(5, 60))
logger.debug(f"Response from {url}: {response}")
response.raise_for_status()
energy_charts_data = ElecPriceEnergyCharts._validate_data(response.content)
self.update_datetime = to_datetime(in_timezone=self.config.general.timezone)
return energy_charts_data
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as exc:
last_exc = exc
logger.warning(
"Energy-Charts request attempt {}/{} failed: {}",
attempt,
max_attempts,
exc,
)
if attempt < max_attempts:
time.sleep(2 * attempt)
# All attempts exhausted - re-raise the last transient error so the
# caller (_update_data) can decide whether to fall back to history.
raise last_exc # type: ignore[misc]
def _parse_data(self, energy_charts_data: EnergyChartsElecPrice) -> pd.Series:
series_data = pd.Series(dtype=float)
@@ -88,14 +115,29 @@ class FeedInTariffEnergyCharts(FeedInTariffProvider):
def _predict_prices(self, history, slots: int, slots_per_hour: int):
energycharts = ElecPriceEnergyCharts()
if len(history) > 800 * slots_per_hour:
logger.info(
"Using weekly seasonal ETS forecast for Energy-Charts feed-in tariff "
"with {} historical values.",
len(history),
)
return energycharts._predict_ets(
history, seasonal_periods=168 * slots_per_hour, hours=slots
)
if len(history) > 168 * slots_per_hour:
logger.info(
"Using daily seasonal ETS forecast for Energy-Charts feed-in tariff "
"with {} historical values.",
len(history),
)
return energycharts._predict_ets(
history, seasonal_periods=24 * slots_per_hour, hours=slots
)
if len(history) > 0:
logger.warning(
"Using constant median fallback for Energy-Charts feed-in tariff "
"with only {} historical values.",
len(history),
)
return energycharts._predict_median(history, hours=slots)
logger.error("No feed-in tariff data available for Energy-Charts prediction")
raise ValueError("No data available")
@@ -111,33 +153,70 @@ class FeedInTariffEnergyCharts(FeedInTariffProvider):
raise ValueError(f"Start DateTime not set: {self.ems_start_datetime}")
past_days = 35
needs_history_refresh = False
if self.highest_orig_datetime:
history_series = self.key_to_series(
key="feed_in_tariff_wh", start_datetime=self.ems_start_datetime
raw_history = self.key_to_series(
key="feed_in_tariff_wh",
end_datetime=to_datetime(self.highest_orig_datetime).add(seconds=1),
)
if not history_series.empty and history_series.index.min() <= self.ems_start_datetime:
# A later update must not mistake the current forecast window for
# sufficient ETS history. Require the same amount of data that the
# weekly prediction branch below needs; otherwise fetch 35 days
# again and repair an already-truncated in-memory history.
if not raw_history.empty:
resolution_seconds = ElecPriceEnergyCharts._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
needs_update = end > self.highest_orig_datetime
needs_update = (
bool(force_update) or end > self.highest_orig_datetime or needs_history_refresh
)
else:
needs_update = True
if needs_update:
logger.info(
"Update FeedInTariffEnergyCharts is needed, last in history: {}",
"Update FeedInTariffEnergyCharts is needed, last in history: {}, "
"force_update={}, history_refresh={}",
self.highest_orig_datetime,
bool(force_update),
needs_history_refresh,
)
start_date = to_datetime(
self.ems_start_datetime - to_duration(f"{past_days} days"),
as_string="YYYY-MM-DD",
)
energy_charts_data = self._request_forecast(
start_date=start_date, force_update=force_update
)
series_data = self._parse_data(energy_charts_data)
if series_data.empty:
raise ValueError("No Energy-Charts feed-in tariff data available")
self.highest_orig_datetime = series_data.index.max()
self.key_from_series("feed_in_tariff_wh", series_data)
try:
energy_charts_data = self._request_forecast(
start_date=start_date, force_update=force_update
)
series_data = self._parse_data(energy_charts_data)
if series_data.empty:
raise ValueError("No Energy-Charts feed-in tariff data available")
self.highest_orig_datetime = series_data.index.max()
self.key_from_series("feed_in_tariff_wh", series_data)
except Exception as exc:
if self.highest_orig_datetime is None:
# Cold start: no cached/historical data to fall back to, so a
# failed fetch is fatal.
raise
# Transient API outage with existing history available: do not
# abort the whole prediction update. Keep the existing history
# and let the ETS/median branch below extrapolate the remaining
# slots, so downstream (e.g. /gesamtlast, optimization) still
# gets a usable feed-in tariff series.
logger.warning(
"Energy-Charts feed-in tariff update failed ({}); keeping "
"existing history until {} and extrapolating the remaining "
"slots via ETS.",
exc,
self.highest_orig_datetime,
)
else:
logger.info(
"No update FeedInTariffEnergyCharts is needed, last in history: {}",
@@ -0,0 +1,169 @@
"""Provide native quarter-hour feed-in prices from the Tibber API."""
from datetime import datetime
from typing import Optional
import numpy as np
import pandas as pd
import requests
from loguru import logger
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.cache import cache_in_file
from akkudoktoreos.prediction.elecpricetibber import (
TIBBER_GRAPHQL_URL,
TIBBER_PRICE_QUERY_QUARTER_HOURLY,
ElecPriceTibber,
TibberGraphQLResponse,
TibberPricePoint,
)
from akkudoktoreos.prediction.feedintariffabc import FeedInTariffProvider
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration
class FeedInTariffTibberCommonSettings(SettingsBaseModel):
"""Settings for the Tibber feed-in tariff provider.
Authentication is shared with ``elecprice.tibber`` so the access token and
home id do not have to be configured twice.
"""
class FeedInTariffTibber(FeedInTariffProvider):
"""Use Tibber's native quarter-hour energy component as feed-in price.
Tibber documents ``energy`` as the spot-price component. Unlike the
end-customer ``total`` component it excludes taxes.
"""
highest_orig_datetime: Optional[datetime] = None
@classmethod
def provider_id(cls) -> str:
"""Return the unique provider identifier."""
return "FeedInTariffTibber"
def historic_hours_min(self) -> int:
"""Keep enough history for seasonal price extrapolation."""
return 24 * 35
@cache_in_file(with_ttl="1 hour")
def _request_forecast(self) -> TibberGraphQLResponse:
"""Request strictly quarter-hourly Tibber prices.
Unlike the electricity-price provider, this provider deliberately has
no hourly fallback because it promises a native 15-minute signal.
"""
access_token = self.config.elecprice.tibber.access_token
if not access_token:
raise ValueError("Tibber access_token is required")
response = requests.post(
TIBBER_GRAPHQL_URL,
json={"query": TIBBER_PRICE_QUERY_QUARTER_HOURLY},
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
timeout=30,
)
logger.debug("Response from Tibber GraphQL API for feed-in tariff: {}", response)
response.raise_for_status()
tibber_data = ElecPriceTibber._validate_data(response.content)
self.update_datetime = to_datetime(in_timezone=self.config.general.timezone)
return tibber_data
def _price_points(self, response: TibberGraphQLResponse) -> list[TibberPricePoint]:
"""Collect historical, today, and tomorrow price points."""
tibber = ElecPriceTibber()
home = tibber._select_home(response)
subscription = home.currentSubscription
if subscription is None:
raise ValueError("Tibber home has no current subscription")
points: list[TibberPricePoint] = []
if subscription.priceInfoRange is not None:
points.extend(subscription.priceInfoRange.nodes)
if subscription.priceInfo is not None:
points.extend(subscription.priceInfo.today)
points.extend(subscription.priceInfo.tomorrow)
if not subscription.priceInfo.tomorrow:
logger.warning("Tibber tomorrow prices not available yet")
return points
def _parse_data(self, response: TibberGraphQLResponse) -> pd.Series:
"""Convert Tibber's EUR/kWh spot-price component to EUR/Wh."""
series = pd.Series(dtype=float)
for point in self._price_points(response):
if point.energy is None:
raise ValueError("Tibber response does not contain the energy price component")
timestamp = to_datetime(point.startsAt, in_timezone=self.config.general.timezone)
series.at[timestamp] = point.energy / 1000.0
if series.empty:
raise ValueError("Tibber response contains no feed-in price points")
return ElecPriceTibber()._normalize_series(series)
def _update_data(self, force_update: Optional[bool] = False) -> None:
"""Store native 15-minute values and forecast missing horizon slots."""
if not self.ems_start_datetime:
raise ValueError(f"Start DateTime not set: {self.ems_start_datetime}")
try:
data = self._request_forecast(force_update=force_update) # type: ignore[call-arg]
series = self._parse_data(data)
resolution_seconds = ElecPriceTibber()._resolution_seconds(series)
if resolution_seconds != 900:
raise ValueError(
"FeedInTariffTibber requires native 15-minute prices; "
f"received {resolution_seconds}-second intervals"
)
self.highest_orig_datetime = to_datetime(
series.index.max(), in_timezone=self.config.general.timezone
)
self.key_from_series("feed_in_tariff_wh", series)
except Exception as exc:
if self.highest_orig_datetime is None:
raise
logger.warning(
"Tibber feed-in tariff update failed ({}); retaining existing 15-minute data.",
exc,
)
if self.highest_orig_datetime is None:
raise ValueError("Highest original datetime not available")
interval_seconds = 900
history = np.asarray(
self.key_to_array(
key="feed_in_tariff_wh",
end_datetime=self.highest_orig_datetime,
interval=to_duration(f"{interval_seconds} seconds"),
fill_method="linear",
),
dtype=float,
)
covered_slots = 0
if self.highest_orig_datetime >= self.ems_start_datetime:
covered_slots = (
int(
(self.highest_orig_datetime - self.ems_start_datetime).total_seconds()
// interval_seconds
)
+ 1
)
needed_slots = self.config.prediction.hours * 4 - covered_slots
if needed_slots <= 0:
return
prediction = ElecPriceTibber()._predict_missing_prices(
history, slots=needed_slots, slots_per_hour=4
)
prediction_series = pd.Series(
data=prediction,
index=[
self.highest_orig_datetime + to_duration(f"{(i + 1) * interval_seconds} seconds")
for i in range(len(prediction))
],
)
self.key_from_series("feed_in_tariff_wh", prediction_series)
@@ -36,9 +36,11 @@ from akkudoktoreos.prediction.elecpriceenergycharts import ElecPriceEnergyCharts
from akkudoktoreos.prediction.elecpricefixed import ElecPriceFixed
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImport
from akkudoktoreos.prediction.elecpricetibber import ElecPriceTibber
from akkudoktoreos.prediction.feedintariffakkudoktor import FeedInTariffAkkudoktor
from akkudoktoreos.prediction.feedintariffenergycharts import FeedInTariffEnergyCharts
from akkudoktoreos.prediction.feedintarifffixed import FeedInTariffFixed
from akkudoktoreos.prediction.feedintariffimport import FeedInTariffImport
from akkudoktoreos.prediction.feedintarifftibber import FeedInTariffTibber
from akkudoktoreos.prediction.loadakkudoktor import (
LoadAkkudoktor,
LoadAkkudoktorAdjusted,
@@ -83,8 +85,10 @@ elecprice_tibber = ElecPriceTibber()
elecprice_fixed = ElecPriceFixed()
elecprice_import = ElecPriceImport()
feedintariff_energy_charts = FeedInTariffEnergyCharts()
feedintariff_akkudoktor = FeedInTariffAkkudoktor()
feedintariff_fixed = FeedInTariffFixed()
feedintariff_import = FeedInTariffImport()
feedintariff_tibber = FeedInTariffTibber()
loadforecast_akkudoktor = LoadAkkudoktor()
loadforecast_akkudoktor_adjusted = LoadAkkudoktorAdjusted()
loadforecast_vrm = LoadVrm()
@@ -110,8 +114,10 @@ def prediction_providers() -> (
ElecPriceFixed,
ElecPriceImport,
FeedInTariffEnergyCharts,
FeedInTariffAkkudoktor,
FeedInTariffFixed,
FeedInTariffImport,
FeedInTariffTibber,
LoadAkkudoktor,
LoadAkkudoktorAdjusted,
LoadVrm,
@@ -140,8 +146,10 @@ def prediction_providers() -> (
elecprice_fixed, \
elecprice_import, \
feedintariff_energy_charts, \
feedintariff_akkudoktor, \
feedintariff_fixed, \
feedintariff_import, \
feedintariff_tibber, \
loadforecast_akkudoktor, \
loadforecast_akkudoktor_adjusted, \
loadforecast_vrm, \
@@ -165,8 +173,10 @@ def prediction_providers() -> (
elecprice_fixed,
elecprice_import,
feedintariff_energy_charts,
feedintariff_akkudoktor,
feedintariff_fixed,
feedintariff_import,
feedintariff_tibber,
loadforecast_akkudoktor,
loadforecast_akkudoktor_adjusted,
loadforecast_vrm,
@@ -195,8 +205,10 @@ class Prediction(PredictionContainer):
ElecPriceFixed,
ElecPriceImport,
FeedInTariffEnergyCharts,
FeedInTariffAkkudoktor,
FeedInTariffFixed,
FeedInTariffImport,
FeedInTariffTibber,
LoadAkkudoktor,
LoadAkkudoktorAdjusted,
LoadVrm,
+7 -2
View File
@@ -1162,6 +1162,7 @@ class GesamtlastRequest(PydanticBaseModel):
year_energy: float
measured_data: List[Dict[str, Any]]
hours: int
force_update: bool = False
@app.post("/gesamtlast", tags=["prediction"])
@@ -1230,11 +1231,15 @@ async def fastapi_gesamtlast(request: GesamtlastRequest) -> list[float]:
energy_mr_values.append(energy_mr)
get_measurement().key_from_lists(measurement_key, energy_mr_dates, energy_mr_values)
# Ensure there is only one optimization/ energy management run at a time
# Ensure there is only one optimization/ energy management run at a time.
# Do not force a full provider refresh by default (see request.force_update):
# forcing bypasses the provider caches and hammers external APIs on every
# call, which made a single flaky provider (e.g. Energy-Charts) abort the
# whole load prediction.
try:
await get_ems().run(
mode=EnergyManagementMode.PREDICTION,
force_update=True,
force_update=request.force_update,
)
except Exception as e:
raise HTTPException(