mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-07-24 02:38:11 +00:00
feat: add EnergyCharts feed-in tariff provider (#1165)
Some checks failed
Bump Version / Bump Version Workflow (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (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
Some checks failed
Bump Version / Bump Version Workflow (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (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
The `FeedInTariffEnergyCharts` provider uses the raw Energy-Charts day-ahead market price as the feed-in tariff. It stores prices in `feed_in_tariff_wh` without adding electricity import charges or VAT. The data is loaded from the Energy-Charts `/price` endpoint for the configured bidding zone. The native Energy-Charts resolution, including quarter-hour data, is retained. Energy-Charts usually supplies prices only for the published day-ahead period. If that data does not cover the complete configured prediction horizon, the provider extends it as follows: - With more than 800 hours of history, an ETS (Holt-Winters exponential smoothing) forecast with weekly seasonality is used. - With more than 168 hours of history, an ETS forecast with daily seasonality is used. - With less history, the median of the available values is used as a constant fallback. The seasonal periods are adjusted to the source resolution. For example, quarter-hour data uses four values per hour. Values already supplied by Energy-Charts are kept unchanged; only missing future slots after the last published price are forecast. Consequently, a 15-minute optimization uses four forecast values per hour without converting them to hourly averages. Signed-off-by: Andreas Schmitz <akkudoktor.net> Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
2
.env
2
.env
@@ -11,7 +11,7 @@ DOCKER_COMPOSE_DATA_DIR=${HOME}/.local/share/net.akkudoktor.eos
|
||||
# -----------------------------------------------------------------------------
|
||||
# Image / build
|
||||
# -----------------------------------------------------------------------------
|
||||
VERSION=0.3.0.dev2607231185985809
|
||||
VERSION=0.3.0.dev2607231331447302
|
||||
PYTHON_VERSION=3.13.9
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# the root directory (no add-on folder as usual).
|
||||
|
||||
name: "Akkudoktor-EOS"
|
||||
version: "0.3.0.dev2607231185985809"
|
||||
version: "0.3.0.dev2607231331447302"
|
||||
slug: "eos"
|
||||
description: "Akkudoktor-EOS add-on"
|
||||
url: "https://github.com/Akkudoktor-EOS/EOS"
|
||||
|
||||
@@ -129,6 +129,9 @@
|
||||
"feedintariffimport": {
|
||||
"import_file_path": null,
|
||||
"import_json": null
|
||||
},
|
||||
"energycharts": {
|
||||
"bidding_zone": "DE-LU"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
| Name | Environment Variable | Type | Read-Only | Default | Description |
|
||||
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
|
||||
| energycharts | `EOS_FEEDINTARIFF__ENERGYCHARTS` | `FeedInTariffEnergyChartsCommonSettings` | `rw` | `required` | EnergyCharts feed in tariff provider settings. |
|
||||
| feedintarifffixed | `EOS_FEEDINTARIFF__FEEDINTARIFFFIXED` | `FeedInTariffFixedCommonSettings` | `rw` | `required` | Fixed feed in tariff provider settings. |
|
||||
| feedintariffimport | `EOS_FEEDINTARIFF__FEEDINTARIFFIMPORT` | `FeedInTariffImportCommonSettings` | `rw` | `required` | Feed in tarif import provider settings. |
|
||||
| provider | `EOS_FEEDINTARIFF__PROVIDER` | `Optional[str]` | `rw` | `None` | Feed in tariff provider id of provider to be used. |
|
||||
@@ -29,6 +30,9 @@
|
||||
"feedintariffimport": {
|
||||
"import_file_path": null,
|
||||
"import_json": null
|
||||
},
|
||||
"energycharts": {
|
||||
"bidding_zone": "DE-LU"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +55,11 @@
|
||||
"import_file_path": null,
|
||||
"import_json": null
|
||||
},
|
||||
"energycharts": {
|
||||
"bidding_zone": "DE-LU"
|
||||
},
|
||||
"providers": [
|
||||
"FeedInTariffEnergyCharts",
|
||||
"FeedInTariffFixed",
|
||||
"FeedInTariffImport"
|
||||
]
|
||||
@@ -119,3 +127,32 @@
|
||||
}
|
||||
```
|
||||
<!-- pyml enable line-length -->
|
||||
|
||||
### Common settings for Energy-Charts feed-in tariff provider
|
||||
|
||||
<!-- pyml disable line-length -->
|
||||
:::{table} feedintariff::energycharts
|
||||
:widths: 10 10 5 5 30
|
||||
:align: left
|
||||
|
||||
| Name | Type | Read-Only | Default | Description |
|
||||
| ---- | ---- | --------- | ------- | ----------- |
|
||||
| bidding_zone | `<enum 'EnergyChartsBiddingZones'>` | `rw` | `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
|
||||
{
|
||||
"feedintariff": {
|
||||
"energycharts": {
|
||||
"bidding_zone": "DE-LU"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
<!-- pyml enable line-length -->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Akkudoktor-EOS
|
||||
|
||||
**Version**: `v0.3.0.dev2607231185985809`
|
||||
**Version**: `v0.3.0.dev2607231331447302`
|
||||
|
||||
<!-- pyml disable line-length -->
|
||||
**Description**: This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period.
|
||||
|
||||
@@ -217,13 +217,49 @@ Configuration options:
|
||||
|
||||
- `provider`: Feed in tariff provider id of provider to be used.
|
||||
|
||||
- `FeedInTariffEnergyCharts`: Retrieves Energy-Charts day-ahead market prices and extends
|
||||
them to the configured prediction horizon when necessary.
|
||||
- `FeedInTariffFixed`: Provides fixed feed in tariff values.
|
||||
- `FeedInTariffImport`: Imports from a file or JSON string or by endpoint data provision.
|
||||
|
||||
- `energycharts.bidding_zone`: Bidding zone Energy Charts shall provide feed-in tariff for.
|
||||
- `feedintarifffixed.feed_in_tariff_kwh`: Fixed feed in tariff (€/kWh).
|
||||
- `feedintariffimport.import_file_path`: Path to the file to import feed in tariff forecast data from.
|
||||
- `feedintariffimport.import_json`: JSON string, dictionary of feed in tariff value lists.
|
||||
|
||||
### FeedInTariffEnergyCharts Provider
|
||||
|
||||
The `FeedInTariffEnergyCharts` provider uses the raw Energy-Charts day-ahead market price as the
|
||||
feed-in tariff. It stores prices in `feed_in_tariff_wh` without adding electricity import charges
|
||||
or VAT. The data is loaded from the Energy-Charts `/price` endpoint for the configured bidding
|
||||
zone. The native Energy-Charts resolution, including quarter-hour data, is retained.
|
||||
|
||||
Energy-Charts usually supplies prices only for the published day-ahead period. If that data does
|
||||
not cover the complete configured prediction horizon, the provider extends it as follows:
|
||||
|
||||
- With more than 800 hours of history, an ETS (Holt-Winters exponential smoothing) forecast with
|
||||
weekly seasonality is used.
|
||||
- With more than 168 hours of history, an ETS forecast with daily seasonality is used.
|
||||
- With less history, the median of the available values is used as a constant fallback.
|
||||
|
||||
The seasonal periods are adjusted to the source resolution. For example, quarter-hour data uses
|
||||
four values per hour. Values already supplied by Energy-Charts are kept unchanged; only missing
|
||||
future slots after the last published price are forecast. Consequently, a 15-minute optimization
|
||||
uses four forecast values per hour without converting them to hourly averages.
|
||||
|
||||
Example configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"feedintariff": {
|
||||
"provider": "FeedInTariffEnergyCharts",
|
||||
"energycharts": {
|
||||
"bidding_zone": "DE-LU"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### FeedInTariffImport Provider
|
||||
|
||||
The `FeedInTariffImport` provider is designed to import feed in tariff prices from:
|
||||
|
||||
25
openapi.json
25
openapi.json
@@ -8,7 +8,7 @@
|
||||
"name": "Apache 2.0",
|
||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
},
|
||||
"version": "v0.3.0.dev2607231185985809"
|
||||
"version": "v0.3.0.dev2607231331447302"
|
||||
},
|
||||
"paths": {
|
||||
"/v1/admin/cache/clear": {
|
||||
@@ -4400,6 +4400,10 @@
|
||||
"feedintariffimport": {
|
||||
"$ref": "#/components/schemas/FeedInTariffImportCommonSettings",
|
||||
"description": "Feed in tarif import provider settings."
|
||||
},
|
||||
"energycharts": {
|
||||
"$ref": "#/components/schemas/FeedInTariffEnergyChartsCommonSettings",
|
||||
"description": "EnergyCharts feed in tariff provider settings."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -4432,6 +4436,10 @@
|
||||
"$ref": "#/components/schemas/FeedInTariffImportCommonSettings",
|
||||
"description": "Feed in tarif import provider settings."
|
||||
},
|
||||
"energycharts": {
|
||||
"$ref": "#/components/schemas/FeedInTariffEnergyChartsCommonSettings",
|
||||
"description": "EnergyCharts feed in tariff provider settings."
|
||||
},
|
||||
"providers": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
@@ -4449,6 +4457,21 @@
|
||||
"title": "FeedInTariffCommonSettings",
|
||||
"description": "Feed In Tariff Prediction Configuration."
|
||||
},
|
||||
"FeedInTariffEnergyChartsCommonSettings": {
|
||||
"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": [
|
||||
"DE-LU"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"title": "FeedInTariffEnergyChartsCommonSettings",
|
||||
"description": "Common settings for Energy-Charts feed-in tariff provider."
|
||||
},
|
||||
"FeedInTariffFixedCommonSettings": {
|
||||
"properties": {
|
||||
"feed_in_tariff_kwh": {
|
||||
|
||||
@@ -5,6 +5,9 @@ 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.feedintariffenergycharts import (
|
||||
FeedInTariffEnergyChartsCommonSettings,
|
||||
)
|
||||
from akkudoktoreos.prediction.feedintarifffixed import FeedInTariffFixedCommonSettings
|
||||
from akkudoktoreos.prediction.feedintariffimport import FeedInTariffImportCommonSettings
|
||||
|
||||
@@ -16,7 +19,11 @@ def feedintariff_provider_ids() -> list[str]:
|
||||
except Exception:
|
||||
# Prediction may not be initialized
|
||||
# Return at least provider used in example
|
||||
return ["FeedInTariffFixed", "FeedInTarifImport"]
|
||||
return [
|
||||
"FeedInTariffEnergyCharts",
|
||||
"FeedInTariffFixed",
|
||||
"FeedInTarifImport",
|
||||
]
|
||||
|
||||
return [
|
||||
provider.provider_id()
|
||||
@@ -46,6 +53,11 @@ class FeedInTariffCommonSettings(SettingsBaseModel):
|
||||
json_schema_extra={"description": "Feed in tarif import provider settings."},
|
||||
)
|
||||
|
||||
energycharts: FeedInTariffEnergyChartsCommonSettings = Field(
|
||||
default_factory=FeedInTariffEnergyChartsCommonSettings,
|
||||
json_schema_extra={"description": "EnergyCharts feed in tariff provider settings."},
|
||||
)
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def providers(self) -> list[str]:
|
||||
|
||||
292
src/akkudoktoreos/prediction/feedintariffenergycharts.py
Normal file
292
src/akkudoktoreos/prediction/feedintariffenergycharts.py
Normal file
@@ -0,0 +1,292 @@
|
||||
"""Provides feed-in tariff data from Energy-Charts 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 pydantic import Field
|
||||
|
||||
from akkudoktoreos.config.configabc import SettingsBaseModel
|
||||
from akkudoktoreos.core.cache import cache_in_file
|
||||
from akkudoktoreos.prediction.elecpriceenergycharts import (
|
||||
ElecPriceEnergyCharts,
|
||||
EnergyChartsBiddingZones,
|
||||
EnergyChartsElecPrice,
|
||||
)
|
||||
from akkudoktoreos.prediction.feedintariffabc import FeedInTariffProvider
|
||||
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration
|
||||
|
||||
|
||||
class FeedInTariffEnergyChartsCommonSettings(SettingsBaseModel):
|
||||
"""Common settings for Energy-Charts feed-in tariff 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": ["DE-LU"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class FeedInTariffEnergyCharts(FeedInTariffProvider):
|
||||
"""Fetch Energy-Charts market prices as feed-in tariff data.
|
||||
|
||||
This provider stores the raw Energy-Charts day-ahead market price as
|
||||
``feed_in_tariff_wh``. Unlike ``ElecPriceEnergyCharts`` it intentionally
|
||||
does not add electricity import charges or VAT.
|
||||
"""
|
||||
|
||||
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."""
|
||||
return "FeedInTariffEnergyCharts"
|
||||
|
||||
def _bidding_zone(self) -> str:
|
||||
settings = self.config.feedintariff.energycharts
|
||||
if settings is None:
|
||||
return EnergyChartsBiddingZones.DE_LU.value
|
||||
bidding_zone = settings.bidding_zone
|
||||
if isinstance(bidding_zone, EnergyChartsBiddingZones):
|
||||
return bidding_zone.value
|
||||
return str(bidding_zone)
|
||||
|
||||
@cache_in_file(with_ttl="1 hour")
|
||||
def _request_forecast(self, start_date: Optional[str] = None) -> EnergyChartsElecPrice:
|
||||
"""Fetch market price forecast data from Energy-Charts."""
|
||||
source = "https://api.energy-charts.info"
|
||||
if start_date is None:
|
||||
start_date = to_datetime(
|
||||
self.ems_start_datetime - to_duration("35 days"), as_string="YYYY-MM-DD"
|
||||
)
|
||||
|
||||
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}"
|
||||
|
||||
# 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)
|
||||
for unix_sec, price_eur_per_mwh in zip(
|
||||
energy_charts_data.unix_seconds, energy_charts_data.price, strict=False
|
||||
):
|
||||
orig_datetime = to_datetime(unix_sec, in_timezone=self.config.general.timezone)
|
||||
series_data.at[orig_datetime] = price_eur_per_mwh / 1_000_000
|
||||
return series_data
|
||||
|
||||
def _resolution_seconds(self, series: pd.Series) -> int:
|
||||
"""Infer the current native market interval from recent timestamps."""
|
||||
if len(series) < 2:
|
||||
return 3600
|
||||
index = pd.DatetimeIndex(series.sort_index().index).drop_duplicates()
|
||||
deltas = index.to_series().diff().dropna().dt.total_seconds()
|
||||
deltas = deltas[deltas > 0].tail(96)
|
||||
if deltas.empty:
|
||||
return 3600
|
||||
resolution = int(round(float(deltas.median())))
|
||||
return resolution if resolution > 0 and 3600 % resolution == 0 else 3600
|
||||
|
||||
def _predict_prices(self, history: np.ndarray, slots: int, slots_per_hour: int) -> np.ndarray:
|
||||
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")
|
||||
|
||||
async def _update_data(self, force_update: Optional[bool] = False) -> None:
|
||||
"""Update feed-in tariff forecast data from Energy-Charts."""
|
||||
# New prices are available every day at 14:00
|
||||
now = pd.Timestamp.now(tz=self.config.general.timezone)
|
||||
midnight = now.normalize()
|
||||
hours_ahead = 23 if now.time() < pd.Timestamp("14:00").time() else 47
|
||||
end = midnight + pd.Timedelta(hours=hours_ahead)
|
||||
|
||||
if not self.ems_start_datetime:
|
||||
raise ValueError(f"Start DateTime not set: {self.ems_start_datetime}")
|
||||
|
||||
# Determine if update is needed and how many days
|
||||
past_days = 35
|
||||
needs_history_refresh = False
|
||||
if self.highest_orig_datetime:
|
||||
raw_history = await self.key_to_series(
|
||||
key="feed_in_tariff_wh",
|
||||
end_datetime=to_datetime(self.highest_orig_datetime).add(seconds=1),
|
||||
)
|
||||
|
||||
# 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 = 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
|
||||
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: {}, "
|
||||
"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",
|
||||
)
|
||||
try:
|
||||
energy_charts_data = self._request_forecast(
|
||||
start_date=start_date, force_update=force_update
|
||||
) # type: ignore
|
||||
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()
|
||||
await 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: {}",
|
||||
self.highest_orig_datetime,
|
||||
)
|
||||
|
||||
if not self.highest_orig_datetime:
|
||||
error_msg = f"Highest original datetime not available: {self.highest_orig_datetime}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
raw_series = await self.key_to_series(
|
||||
key="feed_in_tariff_wh",
|
||||
end_datetime=to_datetime(self.highest_orig_datetime).add(seconds=1),
|
||||
)
|
||||
resolution_seconds = self._resolution_seconds(raw_series)
|
||||
slots_per_hour = 3600 // resolution_seconds
|
||||
history = await self.key_to_array(
|
||||
key="feed_in_tariff_wh",
|
||||
end_datetime=self.highest_orig_datetime,
|
||||
interval=to_duration(f"{resolution_seconds} seconds"),
|
||||
fill_method="linear",
|
||||
)
|
||||
|
||||
# some of our data is already in the future, so we need to predict less.
|
||||
# If we got less data we increase the prediction hours
|
||||
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()
|
||||
// resolution_seconds
|
||||
)
|
||||
+ 1
|
||||
)
|
||||
needed_slots = self.config.prediction.hours * slots_per_hour - covered_slots
|
||||
|
||||
if needed_slots <= 0:
|
||||
logger.warning(
|
||||
"No feed-in tariff prediction needed. needed_slots={}, hours={}, "
|
||||
"resolution_seconds={}, highest_orig_datetime={}, start_datetime={}",
|
||||
needed_slots,
|
||||
self.config.prediction.hours,
|
||||
resolution_seconds,
|
||||
self.highest_orig_datetime,
|
||||
self.ems_start_datetime,
|
||||
)
|
||||
return
|
||||
|
||||
prediction = self._predict_prices(history, needed_slots, slots_per_hour)
|
||||
prediction_series = pd.Series(
|
||||
data=prediction,
|
||||
index=[
|
||||
self.highest_orig_datetime + to_duration(f"{(i + 1) * resolution_seconds} seconds")
|
||||
for i in range(len(prediction))
|
||||
],
|
||||
)
|
||||
await self.key_from_series("feed_in_tariff_wh", prediction_series)
|
||||
@@ -36,6 +36,7 @@ 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.feedintariffenergycharts import FeedInTariffEnergyCharts
|
||||
from akkudoktoreos.prediction.feedintarifffixed import FeedInTariffFixed
|
||||
from akkudoktoreos.prediction.feedintariffimport import FeedInTariffImport
|
||||
from akkudoktoreos.prediction.loadakkudoktor import (
|
||||
@@ -81,6 +82,7 @@ elecprice_energy_charts = ElecPriceEnergyCharts()
|
||||
elecprice_fixed = ElecPriceFixed()
|
||||
elecprice_import = ElecPriceImport()
|
||||
elecprice_tibber = ElecPriceTibber()
|
||||
feedintariff_energy_charts = FeedInTariffEnergyCharts()
|
||||
feedintariff_fixed = FeedInTariffFixed()
|
||||
feedintariff_import = FeedInTariffImport()
|
||||
loadforecast_akkudoktor = LoadAkkudoktor()
|
||||
@@ -106,6 +108,7 @@ def prediction_providers() -> list[
|
||||
ElecPriceFixed,
|
||||
ElecPriceImport,
|
||||
ElecPriceTibber,
|
||||
FeedInTariffEnergyCharts,
|
||||
FeedInTariffFixed,
|
||||
FeedInTariffImport,
|
||||
LoadAkkudoktor,
|
||||
@@ -134,6 +137,7 @@ def prediction_providers() -> list[
|
||||
elecprice_fixed, \
|
||||
elecprice_import, \
|
||||
elecprice_tibber, \
|
||||
feedintariff_energy_charts, \
|
||||
feedintariff_fixed, \
|
||||
feedintariff_import, \
|
||||
loadforecast_akkudoktor, \
|
||||
@@ -158,6 +162,7 @@ def prediction_providers() -> list[
|
||||
elecprice_fixed,
|
||||
elecprice_import,
|
||||
elecprice_tibber,
|
||||
feedintariff_energy_charts,
|
||||
feedintariff_fixed,
|
||||
feedintariff_import,
|
||||
loadforecast_akkudoktor,
|
||||
@@ -187,6 +192,7 @@ class Prediction(PredictionContainer):
|
||||
ElecPriceFixed,
|
||||
ElecPriceImport,
|
||||
ElecPriceTibber,
|
||||
FeedInTariffEnergyCharts,
|
||||
FeedInTariffFixed,
|
||||
FeedInTariffImport,
|
||||
LoadAkkudoktor,
|
||||
|
||||
220
tests/test_feedintariffenergycharts.py
Normal file
220
tests/test_feedintariffenergycharts.py
Normal file
@@ -0,0 +1,220 @@
|
||||
# ruff: noqa: S101
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from akkudoktoreos.core.coreabc import get_ems
|
||||
from akkudoktoreos.prediction.elecpriceenergycharts import (
|
||||
ElecPriceEnergyCharts,
|
||||
EnergyChartsElecPrice,
|
||||
)
|
||||
from akkudoktoreos.prediction.feedintariffenergycharts import FeedInTariffEnergyCharts
|
||||
from akkudoktoreos.utils.datetimeutil import to_datetime
|
||||
|
||||
DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
||||
|
||||
FILE_TESTDATA_ELECPRICE_ENERGYCHARTS_JSON = DIR_TESTDATA.joinpath(
|
||||
"elecpriceforecast_energycharts.json"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def provider(config_eos):
|
||||
config_eos.merge_settings_from_dict(
|
||||
{
|
||||
"feedintariff": {
|
||||
"provider": "FeedInTariffEnergyCharts",
|
||||
"energycharts": {"bidding_zone": "AT"},
|
||||
},
|
||||
}
|
||||
)
|
||||
provider = FeedInTariffEnergyCharts()
|
||||
provider.highest_orig_datetime = None
|
||||
provider.records.clear()
|
||||
assert provider.enabled()
|
||||
return provider
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_energycharts_json():
|
||||
with FILE_TESTDATA_ELECPRICE_ENERGYCHARTS_JSON.open(
|
||||
"r", encoding="utf-8", newline=None
|
||||
) as f_res:
|
||||
return json.load(f_res)
|
||||
|
||||
|
||||
class TestFeedInTariffEnergyCharts:
|
||||
|
||||
def test_provider_is_available(self, config_eos):
|
||||
assert "FeedInTariffEnergyCharts" in config_eos.feedintariff.providers
|
||||
|
||||
|
||||
def test_parse_data_uses_raw_market_price(self, provider, sample_energycharts_json):
|
||||
energy_charts_data = EnergyChartsElecPrice.model_validate(sample_energycharts_json)
|
||||
|
||||
series = provider._parse_data(energy_charts_data)
|
||||
|
||||
assert series.iloc[0] == pytest.approx(sample_energycharts_json["price"][0] / 1_000_000)
|
||||
|
||||
|
||||
@patch("requests.get")
|
||||
def test_request_forecast_uses_feedintariff_bidding_zone(
|
||||
self, mock_get, provider, sample_energycharts_json
|
||||
):
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = json.dumps(sample_energycharts_json)
|
||||
mock_get.return_value = mock_response
|
||||
get_ems().set_start_datetime(to_datetime("2024-12-11 00:00:00", in_timezone="Europe/Berlin"))
|
||||
|
||||
provider._request_forecast(start_date="2024-12-10", force_update=True)
|
||||
|
||||
actual_url = mock_get.call_args[0][0]
|
||||
assert "bzn=AT" in actual_url
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_data_keeps_quarter_hour_resolution(self, provider):
|
||||
start = to_datetime("2025-01-15 00:00:00", in_timezone="Europe/Berlin")
|
||||
get_ems().set_start_datetime(start)
|
||||
raw_slots = provider.config.prediction.hours * 2
|
||||
energy_charts_data = EnergyChartsElecPrice(
|
||||
license_info="",
|
||||
unix_seconds=[int(start.add(minutes=15 * i).timestamp()) for i in range(raw_slots)],
|
||||
price=[100.0] * raw_slots,
|
||||
unit="EUR/MWh",
|
||||
deprecated=False,
|
||||
)
|
||||
|
||||
with patch.object(provider, "_request_forecast", return_value=energy_charts_data):
|
||||
await provider._update_data(force_update=True)
|
||||
|
||||
result = await provider.key_to_series(
|
||||
key="feed_in_tariff_wh",
|
||||
start_datetime=start,
|
||||
end_datetime=start.add(hours=provider.config.prediction.hours),
|
||||
)
|
||||
assert len(result) == provider.config.prediction.hours * 4
|
||||
assert result.index.to_series().diff().dropna().dt.total_seconds().unique().tolist() == [900.0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_repeated_updates_keep_ets_history_and_honor_force_update(self, provider):
|
||||
"""A later update must retain ETS history and a forced update must fetch again."""
|
||||
start = to_datetime(in_timezone="Europe/Berlin").start_of("day")
|
||||
get_ems().set_start_datetime(start)
|
||||
provider.config.prediction.hours = 72
|
||||
|
||||
raw_start = start.subtract(days=35)
|
||||
raw_end = start.add(days=2)
|
||||
raw_slots = int((raw_end - raw_start).total_seconds() // 900) + 1
|
||||
energy_charts_data = EnergyChartsElecPrice(
|
||||
license_info="",
|
||||
unix_seconds=[int(raw_start.add(minutes=15 * i).timestamp()) for i in range(raw_slots)],
|
||||
price=[50.0 + float(i % 96) for i in range(raw_slots)],
|
||||
unit="EUR/MWh",
|
||||
deprecated=False,
|
||||
)
|
||||
|
||||
ets_history_lengths = []
|
||||
|
||||
def fake_ets(history, seasonal_periods, hours):
|
||||
ets_history_lengths.append((len(history), seasonal_periods))
|
||||
return np.full(hours, 0.00005)
|
||||
|
||||
with (
|
||||
patch.object(provider, "_request_forecast", return_value=energy_charts_data) as request,
|
||||
patch.object(ElecPriceEnergyCharts, "_predict_ets", side_effect=fake_ets),
|
||||
patch.object(
|
||||
ElecPriceEnergyCharts,
|
||||
"_predict_median",
|
||||
side_effect=AssertionError("median fallback must not be used"),
|
||||
),
|
||||
):
|
||||
await provider.update_data(force_enable=True, force_update=True)
|
||||
await provider.update_data(force_enable=True, force_update=False)
|
||||
|
||||
# Raw prices already cover the Energy-Charts publication window, so the
|
||||
# second update reuses the retained 35-day history without another request.
|
||||
assert request.call_count == 1
|
||||
assert len(ets_history_lengths) == 2
|
||||
assert all(length > 800 * 4 for length, _ in ets_history_lengths)
|
||||
assert all(seasonal_periods == 168 * 4 for _, seasonal_periods in ets_history_lengths)
|
||||
|
||||
await provider.update_data(force_enable=True, force_update=True)
|
||||
|
||||
# force_update must bypass the provider's own "no update needed" decision.
|
||||
assert request.call_count == 2
|
||||
assert provider.historic_hours_min() == 24 * 35
|
||||
|
||||
|
||||
def test_request_forecast_retries_transient_errors(self, provider, sample_energycharts_json):
|
||||
"""A transient timeout is retried; a later success is returned (Fix D)."""
|
||||
get_ems().set_start_datetime(to_datetime("2024-12-11 00:00:00", in_timezone="Europe/Berlin"))
|
||||
|
||||
ok_response = Mock()
|
||||
ok_response.status_code = 200
|
||||
ok_response.content = json.dumps(sample_energycharts_json)
|
||||
ok_response.raise_for_status = Mock()
|
||||
|
||||
with (
|
||||
patch("requests.get", side_effect=[requests.exceptions.ReadTimeout("t1"), ok_response]) as get_mock,
|
||||
patch("akkudoktoreos.prediction.feedintariffenergycharts.time.sleep", return_value=None),
|
||||
):
|
||||
provider._request_forecast(start_date="2024-12-10", force_update=True)
|
||||
|
||||
assert get_mock.call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_data_falls_back_to_history_on_fetch_error(self, provider):
|
||||
"""A transient fetch error must not abort the update when history exists (Fix A)."""
|
||||
start = to_datetime(in_timezone="Europe/Berlin").start_of("day")
|
||||
get_ems().set_start_datetime(start)
|
||||
provider.config.prediction.hours = 48
|
||||
|
||||
raw_start = start.subtract(days=35)
|
||||
raw_slots = int((start.add(days=2) - raw_start).total_seconds() // 900) + 1
|
||||
energy_charts_data = EnergyChartsElecPrice(
|
||||
license_info="",
|
||||
unix_seconds=[int(raw_start.add(minutes=15 * i).timestamp()) for i in range(raw_slots)],
|
||||
price=[50.0 + float(i % 96) for i in range(raw_slots)],
|
||||
unit="EUR/MWh",
|
||||
deprecated=False,
|
||||
)
|
||||
|
||||
def fake_predict(history, slots, slots_per_hour):
|
||||
return np.full(slots, 0.00005)
|
||||
|
||||
with patch.object(provider, "_predict_prices", side_effect=fake_predict):
|
||||
# First: successful update seeds history and highest_orig_datetime.
|
||||
with patch.object(provider, "_request_forecast", return_value=energy_charts_data):
|
||||
await provider.update_data(force_enable=True, force_update=True)
|
||||
assert provider.highest_orig_datetime is not None
|
||||
last_good = provider.highest_orig_datetime
|
||||
|
||||
# Second: API times out. With existing history the update must NOT raise
|
||||
# and the retained history must be kept.
|
||||
with patch.object(
|
||||
provider, "_request_forecast", side_effect=requests.exceptions.ReadTimeout("boom")
|
||||
):
|
||||
await provider.update_data(force_enable=True, force_update=True)
|
||||
|
||||
# Fix A: the update did not abort (we got here) and the retained history is
|
||||
# unchanged, so downstream consumers still receive a feed-in tariff series.
|
||||
assert provider.highest_orig_datetime == last_good
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_data_cold_start_fetch_error_raises(self, provider):
|
||||
"""Without any history a fetch error stays fatal (cold start)."""
|
||||
start = to_datetime(in_timezone="Europe/Berlin").start_of("day")
|
||||
get_ems().set_start_datetime(start)
|
||||
assert provider.highest_orig_datetime is None
|
||||
|
||||
with patch.object(
|
||||
provider, "_request_forecast", side_effect=requests.exceptions.ReadTimeout("boom")
|
||||
):
|
||||
with pytest.raises(requests.exceptions.ReadTimeout):
|
||||
await provider.update_data(force_enable=True, force_update=True)
|
||||
@@ -7,6 +7,7 @@ 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.feedintariffenergycharts import FeedInTariffEnergyCharts
|
||||
from akkudoktoreos.prediction.feedintarifffixed import FeedInTariffFixed
|
||||
from akkudoktoreos.prediction.feedintariffimport import FeedInTariffImport
|
||||
from akkudoktoreos.prediction.loadakkudoktor import (
|
||||
@@ -46,6 +47,7 @@ def forecast_providers():
|
||||
ElecPriceFixed(),
|
||||
ElecPriceImport(),
|
||||
ElecPriceTibber(),
|
||||
FeedInTariffEnergyCharts(),
|
||||
FeedInTariffFixed(),
|
||||
FeedInTariffImport(),
|
||||
LoadAkkudoktor(),
|
||||
@@ -99,22 +101,23 @@ def test_provider_sequence(prediction):
|
||||
assert isinstance(prediction.providers[2], ElecPriceFixed)
|
||||
assert isinstance(prediction.providers[3], ElecPriceImport)
|
||||
assert isinstance(prediction.providers[4], ElecPriceTibber)
|
||||
assert isinstance(prediction.providers[5], FeedInTariffFixed)
|
||||
assert isinstance(prediction.providers[6], FeedInTariffImport)
|
||||
assert isinstance(prediction.providers[7], LoadAkkudoktor)
|
||||
assert isinstance(prediction.providers[8], LoadAkkudoktorAdjusted)
|
||||
assert isinstance(prediction.providers[9], LoadVrm)
|
||||
assert isinstance(prediction.providers[10], LoadImport)
|
||||
assert isinstance(prediction.providers[11], PVForecastAkkudoktor)
|
||||
assert isinstance(prediction.providers[12], PVForecastVrm)
|
||||
assert isinstance(prediction.providers[13], PVForecastPVNode)
|
||||
assert isinstance(prediction.providers[14], PVForecastForecastSolar)
|
||||
assert isinstance(prediction.providers[15], PVForecastSolcast)
|
||||
assert isinstance(prediction.providers[16], PVForecastImport)
|
||||
assert isinstance(prediction.providers[17], WeatherBrightSky)
|
||||
assert isinstance(prediction.providers[18], WeatherClearOutside)
|
||||
assert isinstance(prediction.providers[19], WeatherOpenMeteo)
|
||||
assert isinstance(prediction.providers[20], WeatherImport)
|
||||
assert isinstance(prediction.providers[5], FeedInTariffEnergyCharts)
|
||||
assert isinstance(prediction.providers[6], FeedInTariffFixed)
|
||||
assert isinstance(prediction.providers[7], FeedInTariffImport)
|
||||
assert isinstance(prediction.providers[8], LoadAkkudoktor)
|
||||
assert isinstance(prediction.providers[9], LoadAkkudoktorAdjusted)
|
||||
assert isinstance(prediction.providers[10], LoadVrm)
|
||||
assert isinstance(prediction.providers[11], LoadImport)
|
||||
assert isinstance(prediction.providers[12], PVForecastAkkudoktor)
|
||||
assert isinstance(prediction.providers[13], PVForecastVrm)
|
||||
assert isinstance(prediction.providers[14], PVForecastPVNode)
|
||||
assert isinstance(prediction.providers[15], PVForecastForecastSolar)
|
||||
assert isinstance(prediction.providers[16], PVForecastSolcast)
|
||||
assert isinstance(prediction.providers[17], PVForecastImport)
|
||||
assert isinstance(prediction.providers[18], WeatherBrightSky)
|
||||
assert isinstance(prediction.providers[19], WeatherClearOutside)
|
||||
assert isinstance(prediction.providers[20], WeatherOpenMeteo)
|
||||
assert isinstance(prediction.providers[21], WeatherImport)
|
||||
|
||||
|
||||
def test_provider_by_id(prediction, forecast_providers):
|
||||
@@ -132,6 +135,7 @@ def test_prediction_repr(prediction):
|
||||
assert "ElecPriceFixed" in result
|
||||
assert "ElecPriceImport" in result
|
||||
assert "ElecPriceTibber" in result
|
||||
assert "FeedInTariffEnergyCharts" in result
|
||||
assert "FeedInTariffFixed" in result
|
||||
assert "FeedInTariffImport" in result
|
||||
assert "LoadAkkudoktor" in result
|
||||
|
||||
@@ -129,6 +129,9 @@
|
||||
"feedintariffimport": {
|
||||
"import_file_path": null,
|
||||
"import_json": null
|
||||
},
|
||||
"energycharts": {
|
||||
"bidding_zone": "DE-LU"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
| Name | Environment Variable | Type | Read-Only | Default | Description |
|
||||
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
|
||||
| energycharts | `EOS_FEEDINTARIFF__ENERGYCHARTS` | `FeedInTariffEnergyChartsCommonSettings` | `rw` | `required` | EnergyCharts feed in tariff provider settings. |
|
||||
| feedintarifffixed | `EOS_FEEDINTARIFF__FEEDINTARIFFFIXED` | `FeedInTariffFixedCommonSettings` | `rw` | `required` | Fixed feed in tariff provider settings. |
|
||||
| feedintariffimport | `EOS_FEEDINTARIFF__FEEDINTARIFFIMPORT` | `FeedInTariffImportCommonSettings` | `rw` | `required` | Feed in tarif import provider settings. |
|
||||
| provider | `EOS_FEEDINTARIFF__PROVIDER` | `Optional[str]` | `rw` | `None` | Feed in tariff provider id of provider to be used. |
|
||||
@@ -29,6 +30,9 @@
|
||||
"feedintariffimport": {
|
||||
"import_file_path": null,
|
||||
"import_json": null
|
||||
},
|
||||
"energycharts": {
|
||||
"bidding_zone": "DE-LU"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +55,11 @@
|
||||
"import_file_path": null,
|
||||
"import_json": null
|
||||
},
|
||||
"energycharts": {
|
||||
"bidding_zone": "DE-LU"
|
||||
},
|
||||
"providers": [
|
||||
"FeedInTariffEnergyCharts",
|
||||
"FeedInTariffFixed",
|
||||
"FeedInTariffImport"
|
||||
]
|
||||
@@ -119,3 +127,32 @@
|
||||
}
|
||||
```
|
||||
<!-- pyml enable line-length -->
|
||||
|
||||
### Common settings for Energy-Charts feed-in tariff provider
|
||||
|
||||
<!-- pyml disable line-length -->
|
||||
:::{table} feedintariff::energycharts
|
||||
:widths: 10 10 5 5 30
|
||||
:align: left
|
||||
|
||||
| Name | Type | Read-Only | Default | Description |
|
||||
| ---- | ---- | --------- | ------- | ----------- |
|
||||
| bidding_zone | `<enum 'EnergyChartsBiddingZones'>` | `rw` | `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
|
||||
{
|
||||
"feedintariff": {
|
||||
"energycharts": {
|
||||
"bidding_zone": "DE-LU"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
<!-- pyml enable line-length -->
|
||||
|
||||
6
uv.lock
generated
6
uv.lock
generated
@@ -1010,11 +1010,11 @@ standard-no-fastapi-cloud-cli = [
|
||||
|
||||
[[package]]
|
||||
name = "fastcore"
|
||||
version = "2.1.4"
|
||||
version = "2.1.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/cd/1c7fe06bd4b42d199b6c70b82f09f177eb22b8b520fd4861d64514c4dc70/fastcore-2.1.4.tar.gz", hash = "sha256:2c6ef14b4f986a1e1792906c0f4b8d9ada85e014b110ce4ffb46ca18eed7cb5b", size = 113686, upload-time = "2026-07-20T13:29:22.152Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2c/83/81349cd1cb156b1737431ef6f3fc3065b4c2f94729bf2e10620c29c36699/fastcore-2.1.5.tar.gz", hash = "sha256:e029a2231e826c38bc02be4a179ab85737d8eeabe9c54d4f54a7ee77e721f1eb", size = 113991, upload-time = "2026-07-22T05:42:32.075Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/55/1d/56a84d96e89505ddcb4f3017f60e47192bf74164f38c0ae2bb54279bf048/fastcore-2.1.4-py3-none-any.whl", hash = "sha256:25bd4b9e941454619b2eaa5726bb1e6a3fd531e5ea7ab7e1e7f1d9dcf12b9705", size = 118789, upload-time = "2026-07-20T13:29:20.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/d6/a660226b0770c5d4a39d5964a5e00745dc5ca25fa9d9e7948e911c30c3c3/fastcore-2.1.5-py3-none-any.whl", hash = "sha256:612a1084f039a4f65695713f62d411dfa5b9f5f4f8d767ff999b7f6c65a29bd7", size = 119006, upload-time = "2026-07-22T05:42:30.619Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user