Add SMARD quarter-hour price provider

This commit is contained in:
Andreas
2026-08-01 12:21:06 +02:00
parent f7e2ac3619
commit 69ef57d9c9
18 changed files with 1126 additions and 72 deletions
+8
View File
@@ -7,6 +7,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased ## Unreleased
### Added ### Added
- Add direct `ElecPriceSMARD` and `FeedInTariffSMARD` providers for German/Luxembourg
day-ahead prices without relying on a supplier account or the rate-limited Energy-Charts API.
The providers retrieve SMARD's native quarter-hour prices, cache weekly chart-data chunks, and
extend missing horizon slots with the existing seasonal ETS forecast. Feed-in prices remain raw,
while import prices can include retail charges.
- Add named net electricity-price components and recurring time-window network fees. Constant
taxes, levies, supplier charges, and the matching dynamic grid fee are added to the underlying
market price before VAT; seasonal forecasting continues to model only the market-price component.
- Add `FeedInTariffAkkudoktor`, using raw hourly Akkudoktor/aWATTar day-ahead market prices as - Add `FeedInTariffAkkudoktor`, using raw hourly Akkudoktor/aWATTar day-ahead market prices as
feed-in tariff data without import charges or VAT. Quarter-hour optimization holds each hourly feed-in tariff data without import charges or VAT. Quarter-hour optimization holds each hourly
value constant for four slots. value constant for four slots.
+131
View File
@@ -7,12 +7,15 @@
| Name | Environment Variable | Type | Read-Only | Default | Description | | Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- | | ---- | -------------------- | ---- | --------- | ------- | ----------- |
| charge_components_kwh | `EOS_ELECPRICE__CHARGE_COMPONENTS_KWH` | `dict[str, Annotated[float, FieldInfo(annotation=NoneType, required=True, metadata=[Ge(ge=0)])]]` | `rw` | `required` | Named constant net charge components [€/kWh]. Their sum is added to charges_kwh, variable network fees, and the market price. |
| 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. |
| elecpricefixed | `EOS_ELECPRICE__ELECPRICEFIXED` | `ElecPriceFixedCommonSettings` | `rw` | `required` | Fixed electricity price provider settings. | | elecpricefixed | `EOS_ELECPRICE__ELECPRICEFIXED` | `ElecPriceFixedCommonSettings` | `rw` | `required` | Fixed electricity price provider settings. |
| elecpriceimport | `EOS_ELECPRICE__ELECPRICEIMPORT` | `ElecPriceImportCommonSettings` | `rw` | `required` | Import provider settings. | | elecpriceimport | `EOS_ELECPRICE__ELECPRICEIMPORT` | `ElecPriceImportCommonSettings` | `rw` | `required` | Import provider settings. |
| energycharts | `EOS_ELECPRICE__ENERGYCHARTS` | `ElecPriceEnergyChartsCommonSettings` | `rw` | `required` | Energy Charts provider settings. | | energycharts | `EOS_ELECPRICE__ENERGYCHARTS` | `ElecPriceEnergyChartsCommonSettings` | `rw` | `required` | Energy Charts provider settings. |
| network_fees_kwh | `EOS_ELECPRICE__NETWORK_FEES_KWH` | `ValueTimeWindowSequence` | `rw` | `required` | Recurring time windows for variable network fees [€/kWh, net]. The first matching window is added to charges_kwh and the market price. |
| 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. |
| providers | | `list[str]` | `ro` | `N/A` | Available electricity price provider ids. | | providers | | `list[str]` | `ro` | `N/A` | Available electricity price provider ids. |
| smard | `EOS_ELECPRICE__SMARD` | `ElecPriceSMARDCommonSettings` | `rw` | `required` | Direct SMARD electricity price provider settings. |
| tibber | `EOS_ELECPRICE__TIBBER` | `ElecPriceTibberCommonSettings` | `rw` | `required` | Tibber electricity price provider settings. | | tibber | `EOS_ELECPRICE__TIBBER` | `ElecPriceTibberCommonSettings` | `rw` | `required` | Tibber electricity price 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. |
::: :::
@@ -44,6 +47,54 @@
"tibber": { "tibber": {
"access_token": null, "access_token": null,
"home_id": null "home_id": null
},
"charge_components_kwh": {
"electricity_tax": 0.0205,
"concession_fee": 0.0132,
"kwkg_levy": 0.00446,
"section_19_levy": 0.01559,
"offshore_grid_levy": 0.00941,
"supplier_markup": 0.0
},
"smard": {
"filter_id": 4169,
"region": "DE"
},
"network_fees_kwh": {
"windows": [
{
"start_time": "00:00:00.000000",
"duration": "7 hours",
"day_of_week": null,
"date": null,
"locale": null,
"value": 0.0095
},
{
"start_time": "07:00:00.000000",
"duration": "8 hours",
"day_of_week": null,
"date": null,
"locale": null,
"value": 0.0953
},
{
"start_time": "15:00:00.000000",
"duration": "5 hours",
"day_of_week": null,
"date": null,
"locale": null,
"value": 0.1565
},
{
"start_time": "20:00:00.000000",
"duration": "4 hours",
"day_of_week": null,
"date": null,
"locale": null,
"value": 0.0953
}
]
} }
} }
} }
@@ -77,9 +128,58 @@
"access_token": null, "access_token": null,
"home_id": null "home_id": null
}, },
"charge_components_kwh": {
"electricity_tax": 0.0205,
"concession_fee": 0.0132,
"kwkg_levy": 0.00446,
"section_19_levy": 0.01559,
"offshore_grid_levy": 0.00941,
"supplier_markup": 0.0
},
"smard": {
"filter_id": 4169,
"region": "DE"
},
"network_fees_kwh": {
"windows": [
{
"start_time": "00:00:00.000000",
"duration": "7 hours",
"day_of_week": null,
"date": null,
"locale": null,
"value": 0.0095
},
{
"start_time": "07:00:00.000000",
"duration": "8 hours",
"day_of_week": null,
"date": null,
"locale": null,
"value": 0.0953
},
{
"start_time": "15:00:00.000000",
"duration": "5 hours",
"day_of_week": null,
"date": null,
"locale": null,
"value": 0.1565
},
{
"start_time": "20:00:00.000000",
"duration": "4 hours",
"day_of_week": null,
"date": null,
"locale": null,
"value": 0.0953
}
]
},
"providers": [ "providers": [
"ElecPriceAkkudoktor", "ElecPriceAkkudoktor",
"ElecPriceEnergyCharts", "ElecPriceEnergyCharts",
"ElecPriceSMARD",
"ElecPriceTibber", "ElecPriceTibber",
"ElecPriceFixed", "ElecPriceFixed",
"ElecPriceImport" "ElecPriceImport"
@@ -120,6 +220,37 @@
``` ```
<!-- pyml enable line-length --> <!-- pyml enable line-length -->
### Common settings for the direct SMARD electricity-price provider
<!-- pyml disable line-length -->
:::{table} elecprice::smard
:widths: 10 10 5 5 30
:align: left
| Name | Type | Read-Only | Default | Description |
| ---- | ---- | --------- | ------- | ----------- |
| filter_id | `int` | `rw` | `4169` | SMARD filter id for the German/Luxembourg day-ahead price. |
| region | `str` | `rw` | `DE` | SMARD market region used in the chart-data endpoint. |
:::
<!-- 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": {
"smard": {
"filter_id": 4169,
"region": "DE"
}
}
}
```
<!-- pyml enable line-length -->
### Common settings for Energy Charts electricity price provider ### Common settings for Energy Charts electricity price provider
<!-- pyml disable line-length --> <!-- pyml disable line-length -->
+53 -2
View File
@@ -114,6 +114,54 @@
"tibber": { "tibber": {
"access_token": null, "access_token": null,
"home_id": null "home_id": null
},
"charge_components_kwh": {
"electricity_tax": 0.0205,
"concession_fee": 0.0132,
"kwkg_levy": 0.00446,
"section_19_levy": 0.01559,
"offshore_grid_levy": 0.00941,
"supplier_markup": 0.0
},
"smard": {
"filter_id": 4169,
"region": "DE"
},
"network_fees_kwh": {
"windows": [
{
"start_time": "00:00:00.000000",
"duration": "7 hours",
"day_of_week": null,
"date": null,
"locale": null,
"value": 0.0095
},
{
"start_time": "07:00:00.000000",
"duration": "8 hours",
"day_of_week": null,
"date": null,
"locale": null,
"value": 0.0953
},
{
"start_time": "15:00:00.000000",
"duration": "5 hours",
"day_of_week": null,
"date": null,
"locale": null,
"value": 0.1565
},
{
"start_time": "20:00:00.000000",
"duration": "4 hours",
"day_of_week": null,
"date": null,
"locale": null,
"value": 0.0953
}
]
} }
}, },
"ems": { "ems": {
@@ -123,11 +171,14 @@
}, },
"feedintariff": { "feedintariff": {
"direct_marketing_enabled": false, "direct_marketing_enabled": false,
"provider": "FeedInTariffFixed", "provider": "FeedInTariffAkkudoktor",
"provider_settings": { "provider_settings": {
"FeedInTariffAkkudoktor": null,
"FeedInTariffFixed": null, "FeedInTariffFixed": null,
"FeedInTariffEnergyCharts": null, "FeedInTariffEnergyCharts": null,
"FeedInTariffImport": null "FeedInTariffImport": null,
"FeedInTariffTibber": null,
"FeedInTariffSMARD": null
} }
}, },
"general": { "general": {
+117 -11
View File
@@ -23,11 +23,14 @@
{ {
"feedintariff": { "feedintariff": {
"direct_marketing_enabled": false, "direct_marketing_enabled": false,
"provider": "FeedInTariffFixed", "provider": "FeedInTariffAkkudoktor",
"provider_settings": { "provider_settings": {
"FeedInTariffAkkudoktor": null,
"FeedInTariffFixed": null, "FeedInTariffFixed": null,
"FeedInTariffEnergyCharts": null, "FeedInTariffEnergyCharts": null,
"FeedInTariffImport": null "FeedInTariffImport": null,
"FeedInTariffTibber": null,
"FeedInTariffSMARD": null
} }
} }
} }
@@ -43,22 +46,87 @@
{ {
"feedintariff": { "feedintariff": {
"direct_marketing_enabled": false, "direct_marketing_enabled": false,
"provider": "FeedInTariffFixed", "provider": "FeedInTariffAkkudoktor",
"provider_settings": { "provider_settings": {
"FeedInTariffAkkudoktor": null,
"FeedInTariffFixed": null, "FeedInTariffFixed": null,
"FeedInTariffEnergyCharts": null, "FeedInTariffEnergyCharts": null,
"FeedInTariffImport": null "FeedInTariffImport": null,
"FeedInTariffTibber": null,
"FeedInTariffSMARD": null
}, },
"providers": [ "providers": [
"FeedInTariffEnergyCharts", "FeedInTariffEnergyCharts",
"FeedInTariffAkkudoktor",
"FeedInTariffFixed", "FeedInTariffFixed",
"FeedInTariffImport" "FeedInTariffImport",
"FeedInTariffSMARD",
"FeedInTariffTibber"
] ]
} }
} }
``` ```
<!-- pyml enable line-length --> <!-- pyml enable line-length -->
### Settings for SMARD feed-in prices shared with ``elecprice.smard``
<!-- pyml disable line-length -->
:::{table} feedintariff::provider_settings::FeedInTariffSMARD
:widths: 10 10 5 5 30
:align: left
| Name | Type | Read-Only | Default | Description |
| ---- | ---- | --------- | ------- | ----------- |
:::
<!-- 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": {
"provider_settings": {
"FeedInTariffSMARD": {}
}
}
}
```
<!-- pyml enable line-length -->
### 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.
<!-- pyml disable line-length -->
:::{table} feedintariff::provider_settings::FeedInTariffTibber
:widths: 10 10 5 5 30
:align: left
| Name | Type | Read-Only | Default | Description |
| ---- | ---- | --------- | ------- | ----------- |
:::
<!-- 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": {
"provider_settings": {
"FeedInTariffTibber": {}
}
}
}
```
<!-- pyml enable line-length -->
### Common settings for feed in tariff data import from file or JSON string ### Common settings for feed in tariff data import from file or JSON string
<!-- pyml disable line-length --> <!-- pyml disable line-length -->
@@ -154,18 +222,19 @@
``` ```
<!-- pyml enable line-length --> <!-- pyml enable line-length -->
### Feed In Tariff Prediction Provider Configuration ### 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.
<!-- pyml disable line-length --> <!-- pyml disable line-length -->
:::{table} feedintariff::provider_settings :::{table} feedintariff::provider_settings::FeedInTariffAkkudoktor
:widths: 10 10 5 5 30 :widths: 10 10 5 5 30
:align: left :align: left
| Name | Type | Read-Only | Default | Description | | Name | Type | Read-Only | Default | Description |
| ---- | ---- | --------- | ------- | ----------- | | ---- | ---- | --------- | ------- | ----------- |
| FeedInTariffEnergyCharts | `Optional[akkudoktoreos.prediction.feedintariffenergycharts.FeedInTariffEnergyChartsCommonSettings]` | `rw` | `None` | FeedInTariffEnergyCharts settings |
| FeedInTariffFixed | `Optional[akkudoktoreos.prediction.feedintarifffixed.FeedInTariffFixedCommonSettings]` | `rw` | `None` | FeedInTariffFixed settings |
| FeedInTariffImport | `Optional[akkudoktoreos.prediction.feedintariffimport.FeedInTariffImportCommonSettings]` | `rw` | `None` | FeedInTariffImport settings |
::: :::
<!-- pyml enable line-length --> <!-- pyml enable line-length -->
@@ -178,9 +247,46 @@
{ {
"feedintariff": { "feedintariff": {
"provider_settings": { "provider_settings": {
"FeedInTariffAkkudoktor": {}
}
}
}
```
<!-- pyml enable line-length -->
### Feed In Tariff Prediction Provider Configuration
<!-- pyml disable line-length -->
:::{table} feedintariff::provider_settings
:widths: 10 10 5 5 30
:align: left
| Name | Type | Read-Only | Default | Description |
| ---- | ---- | --------- | ------- | ----------- |
| FeedInTariffAkkudoktor | `Optional[akkudoktoreos.prediction.feedintariffakkudoktor.FeedInTariffAkkudoktorCommonSettings]` | `rw` | `None` | FeedInTariffAkkudoktor settings |
| FeedInTariffEnergyCharts | `Optional[akkudoktoreos.prediction.feedintariffenergycharts.FeedInTariffEnergyChartsCommonSettings]` | `rw` | `None` | FeedInTariffEnergyCharts settings |
| FeedInTariffFixed | `Optional[akkudoktoreos.prediction.feedintarifffixed.FeedInTariffFixedCommonSettings]` | `rw` | `None` | FeedInTariffFixed settings |
| FeedInTariffImport | `Optional[akkudoktoreos.prediction.feedintariffimport.FeedInTariffImportCommonSettings]` | `rw` | `None` | FeedInTariffImport settings |
| FeedInTariffSMARD | `Optional[akkudoktoreos.prediction.feedintariffsmard.FeedInTariffSMARDCommonSettings]` | `rw` | `None` | FeedInTariffSMARD settings |
| FeedInTariffTibber | `Optional[akkudoktoreos.prediction.feedintarifftibber.FeedInTariffTibberCommonSettings]` | `rw` | `None` | FeedInTariffTibber 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
{
"feedintariff": {
"provider_settings": {
"FeedInTariffAkkudoktor": null,
"FeedInTariffFixed": null, "FeedInTariffFixed": null,
"FeedInTariffEnergyCharts": null, "FeedInTariffEnergyCharts": null,
"FeedInTariffImport": null "FeedInTariffImport": null,
"FeedInTariffTibber": null,
"FeedInTariffSMARD": null
} }
} }
} }
+4 -2
View File
@@ -1,6 +1,6 @@
# Akkudoktor-EOS # Akkudoktor-EOS
**Version**: `v0.3.0.dev2607071966322885` **Version**: `v0.3.0.dev2608010951962828`
<!-- pyml disable line-length --> <!-- 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. **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.
@@ -122,7 +122,9 @@ Note:
- `start_hour` (query, optional): Defaults to current hour of the day. - `start_hour` (query, optional): Defaults to current hour of the day.
- `ngen` (query, optional): Number of indivuals to generate for genetic algorithm. - `ngen` (query, optional): Deprecated alias for the number of genetic generations. Defaults to optimization.genetic.generations.
- `individuals` (query, optional): Override optimization.genetic.individuals for this run.
**Request Body**: **Request Body**:
+26 -3
View File
@@ -136,10 +136,13 @@ Configuration options:
- `ElecPriceAkkudoktor`: Retrieves from Akkudoktor.net. - `ElecPriceAkkudoktor`: Retrieves from Akkudoktor.net.
- `ElecPriceEnergyCharts`: Retrieves from Energy-Charts.info. - `ElecPriceEnergyCharts`: Retrieves from Energy-Charts.info.
- `ElecPriceSMARD`: Retrieves German/Luxembourg day-ahead prices directly from SMARD.de.
- `ElecPriceFixed`: Caluclates from configured time window prices. - `ElecPriceFixed`: Caluclates from configured time window prices.
- `ElecPriceImport`: Imports from a file or JSON string or by endpoint data provision. - `ElecPriceImport`: Imports from a file or JSON string or by endpoint data provision.
- `charges_kwh`: Electricity price charges (€/kWh). - `charges_kwh`: Electricity price charges (€/kWh).
- `charge_components_kwh`: Named constant net-charge components (€/kWh), summed automatically.
- `network_fees_kwh.windows`: Recurring time windows with variable net-grid fees (€/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).
- `elecpricefixed.time_windows.windows`: The time windows with associated electricity prices. - `elecpricefixed.time_windows.windows`: The time windows with associated electricity prices.
- `elecpriceimport.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.
@@ -166,12 +169,32 @@ forecasting by combining real-time market data with historical price trends.
Charges and VAT Charges and VAT
- If `charges_kwh` configuration option is greater than 0, the electricity price is calculated as: - If constant charges or a matching `network_fees_kwh` window is greater than 0, the electricity
`(market price + charges_kwh) * vat_rate` where `vat_rate` is configurable (default: 1.19 for 19% VAT). price is calculated as: `(market price + charges_kwh + sum(charge components) + network fee)
- If `charges_kwh` is set to 0, the electricity price is simply: `market_price` (no VAT applied). * vat_rate` where
`vat_rate` is configurable (default: 1.19 for 19% VAT).
- If all constant charges and network-fee windows are empty or zero, the electricity price is
simply `market_price` (no VAT applied, preserving the existing raw-market-price behaviour).
`charges_kwh`, every `charge_components_kwh` value, and `network_fees_kwh` are net values. Time
windows repeat daily unless their optional `date` or `day_of_week` constraints are set. The first
matching window is used. Named components make statutory levies and the supplier markup auditable
instead of hiding them in one aggregate. The supplier-specific markup defaults to zero in the
example and must be taken from the electricity supply contract.
**Note:** For the most accurate forecasts, it is recommended to set the `historic_hours` parameter to 840. **Note:** For the most accurate forecasts, it is recommended to set the `historic_hours` parameter to 840.
### ElecPriceSMARD Provider
The `ElecPriceSMARD` provider retrieves quarter-hourly German/Luxembourg day-ahead prices directly from
the public SMARD chart-data endpoint. It requests the required weekly chunks only and caches the
combined response for one hour. Missing quarter-hour slots beyond the published day-ahead horizon are generated
with the same daily or weekly seasonal ETS forecast as the Energy-Charts provider. Constant charges,
time-variable network fees, and VAT are applied after forecasting the underlying market price.
The same raw SMARD series is also available as `FeedInTariffSMARD` for direct-marketing feed-in
revenue. Import-only charges, network fees, and VAT are deliberately not applied there.
### ElecPriceFixed Provider ### ElecPriceFixed Provider
The `ElecPriceFixed` provider calculates the day-ahead electricity market prices from the configuration The `ElecPriceFixed` provider calculates the day-ahead electricity market prices from the configuration
+230 -6
View File
@@ -8,7 +8,7 @@
"name": "Apache 2.0", "name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html" "url": "https://www.apache.org/licenses/LICENSE-2.0.html"
}, },
"version": "v0.3.0.dev2607101210858200" "version": "v0.3.0.dev2608010951962828"
}, },
"paths": { "paths": {
"/v1/admin/cache/clear": { "/v1/admin/cache/clear": {
@@ -2005,16 +2005,36 @@
"schema": { "schema": {
"anyOf": [ "anyOf": [
{ {
"type": "integer" "type": "integer",
"minimum": 1
}, },
{ {
"type": "null" "type": "null"
} }
], ],
"description": "Number of indivuals to generate for genetic algorithm.", "description": "Deprecated alias for the number of genetic generations. Defaults to optimization.genetic.generations.",
"title": "Ngen" "title": "Ngen"
}, },
"description": "Number of indivuals to generate for genetic algorithm." "description": "Deprecated alias for the number of genetic generations. Defaults to optimization.genetic.generations."
},
{
"name": "individuals",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "integer",
"minimum": 10
},
{
"type": "null"
}
],
"description": "Override optimization.genetic.individuals for this run.",
"title": "Individuals"
},
"description": "Override optimization.genetic.individuals for this run."
} }
], ],
"requestBody": { "requestBody": {
@@ -3405,6 +3425,59 @@
"tibber": { "tibber": {
"$ref": "#/components/schemas/ElecPriceTibberCommonSettings", "$ref": "#/components/schemas/ElecPriceTibberCommonSettings",
"description": "Tibber electricity price provider settings." "description": "Tibber electricity price provider settings."
},
"charge_components_kwh": {
"additionalProperties": {
"type": "number",
"minimum": 0.0
},
"type": "object",
"title": "Charge Components Kwh",
"description": "Named constant net charge components [\u20ac/kWh]. Their sum is added to charges_kwh, variable network fees, and the market price.",
"examples": [
{
"concession_fee": 0.0132,
"electricity_tax": 0.0205,
"kwkg_levy": 0.00446,
"offshore_grid_levy": 0.00941,
"section_19_levy": 0.01559,
"supplier_markup": 0.0
}
]
},
"smard": {
"$ref": "#/components/schemas/ElecPriceSMARDCommonSettings",
"description": "Direct SMARD electricity price provider settings."
},
"network_fees_kwh": {
"$ref": "#/components/schemas/ValueTimeWindowSequence-Input",
"description": "Recurring time windows for variable network fees [\u20ac/kWh, net]. The first matching window is added to charges_kwh and the market price.",
"examples": [
{
"windows": [
{
"duration": "7 hours",
"start_time": "00:00",
"value": 0.0095
},
{
"duration": "8 hours",
"start_time": "07:00",
"value": 0.0953
},
{
"duration": "5 hours",
"start_time": "15:00",
"value": 0.1565
},
{
"duration": "4 hours",
"start_time": "20:00",
"value": 0.0953
}
]
}
]
} }
}, },
"type": "object", "type": "object",
@@ -3477,6 +3550,59 @@
"$ref": "#/components/schemas/ElecPriceTibberCommonSettings", "$ref": "#/components/schemas/ElecPriceTibberCommonSettings",
"description": "Tibber electricity price provider settings." "description": "Tibber electricity price provider settings."
}, },
"charge_components_kwh": {
"additionalProperties": {
"type": "number",
"minimum": 0.0
},
"type": "object",
"title": "Charge Components Kwh",
"description": "Named constant net charge components [\u20ac/kWh]. Their sum is added to charges_kwh, variable network fees, and the market price.",
"examples": [
{
"concession_fee": 0.0132,
"electricity_tax": 0.0205,
"kwkg_levy": 0.00446,
"offshore_grid_levy": 0.00941,
"section_19_levy": 0.01559,
"supplier_markup": 0.0
}
]
},
"smard": {
"$ref": "#/components/schemas/ElecPriceSMARDCommonSettings",
"description": "Direct SMARD electricity price provider settings."
},
"network_fees_kwh": {
"$ref": "#/components/schemas/ValueTimeWindowSequence-Output",
"description": "Recurring time windows for variable network fees [\u20ac/kWh, net]. The first matching window is added to charges_kwh and the market price.",
"examples": [
{
"windows": [
{
"duration": "7 hours",
"start_time": "00:00",
"value": 0.0095
},
{
"duration": "8 hours",
"start_time": "07:00",
"value": 0.0953
},
{
"duration": "5 hours",
"start_time": "15:00",
"value": 0.1565
},
{
"duration": "4 hours",
"start_time": "20:00",
"value": 0.0953
}
]
}
]
},
"providers": { "providers": {
"items": { "items": {
"type": "string" "type": "string"
@@ -3605,6 +3731,33 @@
"title": "ElecPriceImportCommonSettings", "title": "ElecPriceImportCommonSettings",
"description": "Common settings for elecprice data import from file or JSON String." "description": "Common settings for elecprice data import from file or JSON String."
}, },
"ElecPriceSMARDCommonSettings": {
"properties": {
"filter_id": {
"type": "integer",
"exclusiveMinimum": 0.0,
"title": "Filter Id",
"description": "SMARD filter id for the German/Luxembourg day-ahead price.",
"default": 4169,
"examples": [
4169
]
},
"region": {
"type": "string",
"minLength": 2,
"title": "Region",
"description": "SMARD market region used in the chart-data endpoint.",
"default": "DE",
"examples": [
"DE"
]
}
},
"type": "object",
"title": "ElecPriceSMARDCommonSettings",
"description": "Common settings for the direct SMARD electricity-price provider."
},
"ElecPriceTibberCommonSettings": { "ElecPriceTibberCommonSettings": {
"properties": { "properties": {
"access_token": { "access_token": {
@@ -4278,8 +4431,28 @@
"title": "FRBCTimerStatus", "title": "FRBCTimerStatus",
"description": "Current status of an FRBC Timer.\n\nIndicates when the Timer will be finished." "description": "Current status of an FRBC Timer.\n\nIndicates when the Timer will be finished."
}, },
"FeedInTariffAkkudoktorCommonSettings": {
"properties": {},
"type": "object",
"title": "FeedInTariffAkkudoktorCommonSettings",
"description": "Settings for the Akkudoktor feed-in tariff provider.\n\nThe public Akkudoktor price endpoint only needs the timezone already\nconfigured in ``general.timezone``, so no provider-specific values are\ncurrently required."
},
"FeedInTariffCommonProviderSettings": { "FeedInTariffCommonProviderSettings": {
"properties": { "properties": {
"FeedInTariffAkkudoktor": {
"anyOf": [
{
"$ref": "#/components/schemas/FeedInTariffAkkudoktorCommonSettings"
},
{
"type": "null"
}
],
"description": "FeedInTariffAkkudoktor settings",
"examples": [
null
]
},
"FeedInTariffFixed": { "FeedInTariffFixed": {
"anyOf": [ "anyOf": [
{ {
@@ -4321,6 +4494,34 @@
"examples": [ "examples": [
null null
] ]
},
"FeedInTariffTibber": {
"anyOf": [
{
"$ref": "#/components/schemas/FeedInTariffTibberCommonSettings"
},
{
"type": "null"
}
],
"description": "FeedInTariffTibber settings",
"examples": [
null
]
},
"FeedInTariffSMARD": {
"anyOf": [
{
"$ref": "#/components/schemas/FeedInTariffSMARDCommonSettings"
},
{
"type": "null"
}
],
"description": "FeedInTariffSMARD settings",
"examples": [
null
]
} }
}, },
"type": "object", "type": "object",
@@ -4351,9 +4552,12 @@
"title": "Provider", "title": "Provider",
"description": "Feed in tariff provider id of provider to be used.", "description": "Feed in tariff provider id of provider to be used.",
"examples": [ "examples": [
"FeedInTariffAkkudoktor",
"FeedInTariffFixed", "FeedInTariffFixed",
"FeedInTariffEnergyCharts", "FeedInTariffEnergyCharts",
"FeedInTariffImport" "FeedInTariffImport",
"FeedInTariffSMARD",
"FeedInTariffTibber"
] ]
}, },
"provider_settings": { "provider_settings": {
@@ -4392,9 +4596,12 @@
"title": "Provider", "title": "Provider",
"description": "Feed in tariff provider id of provider to be used.", "description": "Feed in tariff provider id of provider to be used.",
"examples": [ "examples": [
"FeedInTariffAkkudoktor",
"FeedInTariffFixed", "FeedInTariffFixed",
"FeedInTariffEnergyCharts", "FeedInTariffEnergyCharts",
"FeedInTariffImport" "FeedInTariffImport",
"FeedInTariffSMARD",
"FeedInTariffTibber"
] ]
}, },
"provider_settings": { "provider_settings": {
@@ -4501,6 +4708,18 @@
"title": "FeedInTariffImportCommonSettings", "title": "FeedInTariffImportCommonSettings",
"description": "Common settings for feed in tariff data import from file or JSON string." "description": "Common settings for feed in tariff data import from file or JSON string."
}, },
"FeedInTariffSMARDCommonSettings": {
"properties": {},
"type": "object",
"title": "FeedInTariffSMARDCommonSettings",
"description": "Settings for SMARD feed-in prices shared with ``elecprice.smard``."
},
"FeedInTariffTibberCommonSettings": {
"properties": {},
"type": "object",
"title": "FeedInTariffTibberCommonSettings",
"description": "Settings for the Tibber feed-in tariff provider.\n\nAuthentication is shared with ``elecprice.tibber`` so the access token and\nhome id do not have to be configured twice."
},
"ForecastResponse": { "ForecastResponse": {
"properties": { "properties": {
"temperature": { "temperature": {
@@ -5313,6 +5532,11 @@
"hours": { "hours": {
"type": "integer", "type": "integer",
"title": "Hours" "title": "Hours"
},
"force_update": {
"type": "boolean",
"title": "Force Update",
"default": false
} }
}, },
"type": "object", "type": "object",
+49 -2
View File
@@ -1,8 +1,8 @@
from typing import Optional from typing import Annotated, Optional
from pydantic import Field, computed_field, field_validator from pydantic import Field, computed_field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel, ValueTimeWindowSequence
from akkudoktoreos.core.coreabc import get_prediction from akkudoktoreos.core.coreabc import get_prediction
from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider
from akkudoktoreos.prediction.elecpriceenergycharts import ( from akkudoktoreos.prediction.elecpriceenergycharts import (
@@ -10,6 +10,7 @@ from akkudoktoreos.prediction.elecpriceenergycharts import (
) )
from akkudoktoreos.prediction.elecpricefixed import ElecPriceFixedCommonSettings from akkudoktoreos.prediction.elecpricefixed import ElecPriceFixedCommonSettings
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImportCommonSettings from akkudoktoreos.prediction.elecpriceimport import ElecPriceImportCommonSettings
from akkudoktoreos.prediction.elecpricesmard import ElecPriceSMARDCommonSettings
from akkudoktoreos.prediction.elecpricetibber import ElecPriceTibberCommonSettings from akkudoktoreos.prediction.elecpricetibber import ElecPriceTibberCommonSettings
@@ -24,6 +25,7 @@ def elecprice_provider_ids() -> list[str]:
"ElecPriceEnergyCharts", "ElecPriceEnergyCharts",
"ElecPriceFixed", "ElecPriceFixed",
"ElecPriceImport", "ElecPriceImport",
"ElecPriceSMARD",
"ElecPriceTibber", "ElecPriceTibber",
] ]
@@ -83,6 +85,51 @@ class ElecPriceCommonSettings(SettingsBaseModel):
json_schema_extra={"description": "Tibber electricity price provider settings."}, json_schema_extra={"description": "Tibber electricity price provider settings."},
) )
charge_components_kwh: dict[str, Annotated[float, Field(ge=0)]] = Field(
default_factory=dict,
json_schema_extra={
"description": (
"Named constant net charge components [€/kWh]. Their sum is added to "
"charges_kwh, variable network fees, and the market price."
),
"examples": [
{
"electricity_tax": 0.0205,
"concession_fee": 0.0132,
"kwkg_levy": 0.00446,
"section_19_levy": 0.01559,
"offshore_grid_levy": 0.00941,
"supplier_markup": 0.0,
}
],
},
)
smard: ElecPriceSMARDCommonSettings = Field(
default_factory=ElecPriceSMARDCommonSettings,
json_schema_extra={"description": "Direct SMARD electricity price provider settings."},
)
network_fees_kwh: ValueTimeWindowSequence = Field(
default_factory=ValueTimeWindowSequence,
json_schema_extra={
"description": (
"Recurring time windows for variable network fees [€/kWh, net]. "
"The first matching window is added to charges_kwh and the market price."
),
"examples": [
{
"windows": [
{"start_time": "00:00", "duration": "7 hours", "value": 0.0095},
{"start_time": "07:00", "duration": "8 hours", "value": 0.0953},
{"start_time": "15:00", "duration": "5 hours", "value": 0.1565},
{"start_time": "20:00", "duration": "4 hours", "value": 0.0953},
]
}
],
},
)
@computed_field # type: ignore[prop-decorator] @computed_field # type: ignore[prop-decorator]
@property @property
def providers(self) -> list[str]: def providers(self) -> list[str]:
@@ -147,9 +147,6 @@ class ElecPriceEnergyCharts(ElecPriceProvider):
# Assumption that all lists are the same length and are ordered chronologically # Assumption that all lists are the same length and are ordered chronologically
# in ascending order and have the same timestamps. # in ascending order and have the same timestamps.
# Get charges_kwh in wh
charges_wh = (self.config.elecprice.charges_kwh or 0) / 1000
# Initialize # Initialize
highest_orig_datetime = None # newest datetime from the api after that we want to update. highest_orig_datetime = None # newest datetime from the api after that we want to update.
series_data = pd.Series(dtype=float) # Initialize an empty series series_data = pd.Series(dtype=float) # Initialize an empty series
@@ -164,18 +161,40 @@ class ElecPriceEnergyCharts(ElecPriceProvider):
if highest_orig_datetime is None or orig_datetime > highest_orig_datetime: if highest_orig_datetime is None or orig_datetime > highest_orig_datetime:
highest_orig_datetime = orig_datetime highest_orig_datetime = orig_datetime
# Convert EUR/MWh to EUR/Wh, apply charges and VAT if charges > 0 # Convert EUR/MWh to EUR/Wh and add the configured retail price components.
if charges_wh > 0: price_wh = self._price_with_charges(
vat_rate = self.config.elecprice.vat_rate or 1.19 price_eur_per_mwh / 1_000_000, orig_datetime
price_wh = ((price_eur_per_mwh / 1_000_000) + charges_wh) * vat_rate )
else:
price_wh = price_eur_per_mwh / 1_000_000
# Store in series # Store in series
series_data.at[orig_datetime] = price_wh series_data.at[orig_datetime] = price_wh
return series_data return series_data
def _charges_kwh_for_datetime(self, date_time: datetime) -> float:
"""Return constant charges plus the first matching variable network fee."""
constant_charges_kwh = self.config.elecprice.charges_kwh or 0.0
component_charges_kwh = sum(self.config.elecprice.charge_components_kwh.values())
network_fees = self.config.elecprice.network_fees_kwh
network_fee_kwh = network_fees.get_value_for_datetime(to_datetime(date_time))
return constant_charges_kwh + component_charges_kwh + network_fee_kwh
def _price_with_charges(self, market_price_wh: float, date_time: datetime) -> float:
"""Build the gross retail price from a net market price in EUR/Wh."""
charges_kwh = self._charges_kwh_for_datetime(date_time)
if charges_kwh <= 0:
return market_price_wh
vat_rate = self.config.elecprice.vat_rate or 1.19
return (market_price_wh + charges_kwh / 1000.0) * vat_rate
def _price_without_charges(self, retail_price_wh: float, date_time: datetime) -> float:
"""Reverse configured charges so ETS forecasts only the underlying market price."""
charges_kwh = self._charges_kwh_for_datetime(date_time)
if charges_kwh <= 0:
return retail_price_wh
vat_rate = self.config.elecprice.vat_rate or 1.19
return retail_price_wh / vat_rate - charges_kwh / 1000.0
@staticmethod @staticmethod
def _resolution_seconds(series: pd.Series) -> int: def _resolution_seconds(series: pd.Series) -> int:
"""Infer the current native market interval from recent timestamps.""" """Infer the current native market interval from recent timestamps."""
@@ -243,7 +262,9 @@ class ElecPriceEnergyCharts(ElecPriceProvider):
if needs_update: if needs_update:
logger.info( logger.info(
f"Update ElecPriceEnergyCharts is needed, last in history: {self.highest_orig_datetime}" "Update {} is needed, last in history: {}",
self.provider_id(),
self.highest_orig_datetime,
) )
# 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(
@@ -260,7 +281,9 @@ class ElecPriceEnergyCharts(ElecPriceProvider):
self.key_from_series("elecprice_marketprice_wh", series_data) self.key_from_series("elecprice_marketprice_wh", series_data)
else: else:
logger.info( logger.info(
f"No Update ElecPriceEnergyCharts is needed, last in history: {self.highest_orig_datetime}" "No update {} is needed, last in history: {}",
self.provider_id(),
self.highest_orig_datetime,
) )
if not self.highest_orig_datetime: # mypy fix if not self.highest_orig_datetime: # mypy fix
@@ -274,12 +297,25 @@ class ElecPriceEnergyCharts(ElecPriceProvider):
) )
resolution_seconds = self._resolution_seconds(raw_series) resolution_seconds = self._resolution_seconds(raw_series)
slots_per_hour = 3600 // resolution_seconds slots_per_hour = 3600 // resolution_seconds
history = self.key_to_array( priced_history = self.key_to_array(
key="elecprice_marketprice_wh", key="elecprice_marketprice_wh",
end_datetime=self.highest_orig_datetime, end_datetime=to_datetime(self.highest_orig_datetime),
interval=to_duration(f"{resolution_seconds} seconds"), interval=to_duration(f"{resolution_seconds} seconds"),
fill_method="linear", fill_method="linear",
) )
history_start = self.highest_orig_datetime - to_duration(
f"{max(len(priced_history) - 1, 0) * resolution_seconds} seconds"
)
history = np.array(
[
self._price_without_charges(
float(price_wh),
history_start + to_duration(f"{i * resolution_seconds} seconds"),
)
for i, price_wh in enumerate(priced_history)
],
dtype=float,
)
# 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 # 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 covered_slots = 0
@@ -326,11 +362,15 @@ class ElecPriceEnergyCharts(ElecPriceProvider):
raise ValueError("No data available") raise ValueError("No data available")
# write predictions into the records, update if exist. # write predictions into the records, update if exist.
prediction_index = [
self.highest_orig_datetime + to_duration(f"{(i + 1) * resolution_seconds} seconds")
for i in range(len(prediction))
]
prediction_series = pd.Series( prediction_series = pd.Series(
data=prediction, data=[
index=[ self._price_with_charges(float(price_wh), timestamp)
self.highest_orig_datetime + to_duration(f"{(i + 1) * resolution_seconds} seconds") for timestamp, price_wh in zip(prediction_index, prediction)
for i in range(len(prediction))
], ],
index=prediction_index,
) )
self.key_from_series("elecprice_marketprice_wh", prediction_series) self.key_from_series("elecprice_marketprice_wh", prediction_series)
@@ -0,0 +1,185 @@
"""Retrieve German day-ahead electricity prices directly from SMARD."""
import time
from datetime import datetime
from typing import List, Optional
import requests
from loguru import logger
from pydantic import Field, ValidationError
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.cache import cache_in_file
from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.prediction.elecpriceenergycharts import (
ElecPriceEnergyCharts,
EnergyChartsElecPrice,
)
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration
SMARD_BASE_URL = "https://www.smard.de/app/chart_data"
class SmardIndex(PydanticBaseModel):
"""Available SMARD data-chunk timestamps."""
timestamps: List[int]
class SmardChunkMetadata(PydanticBaseModel):
"""Metadata included in a SMARD data chunk."""
version: int
created: int
class SmardChunk(PydanticBaseModel):
"""SMARD data chunk with millisecond timestamps and EUR/MWh values."""
meta_data: SmardChunkMetadata
series: List[tuple[int, Optional[float]]]
class ElecPriceSMARDCommonSettings(SettingsBaseModel):
"""Common settings for the direct SMARD electricity-price provider."""
filter_id: int = Field(
default=4169,
gt=0,
json_schema_extra={
"description": "SMARD filter id for the German/Luxembourg day-ahead price.",
"examples": [4169],
},
)
region: str = Field(
default="DE",
min_length=2,
json_schema_extra={
"description": "SMARD market region used in the chart-data endpoint.",
"examples": ["DE"],
},
)
class ElecPriceSMARD(ElecPriceEnergyCharts):
"""Fetch SMARD day-ahead prices and extend them with the seasonal EOS forecast.
The provider uses the public SMARD chart-data endpoint directly. It reuses the
Energy-Charts parsing and ETS pipeline after normalizing the response because both
sources expose the same EUR/MWh day-ahead market-price concept.
"""
@classmethod
def provider_id(cls) -> str:
"""Return the unique identifier for the direct SMARD provider."""
return "ElecPriceSMARD"
@classmethod
def _validate_index(cls, json_data: bytes) -> SmardIndex:
"""Validate a SMARD chunk index response."""
try:
return SmardIndex.model_validate_json(json_data)
except ValidationError as exc:
logger.error("SMARD index schema change: {}", exc)
raise ValueError(f"SMARD index schema change: {exc}") from exc
@classmethod
def _validate_chunk(cls, json_data: bytes) -> SmardChunk:
"""Validate a SMARD price chunk response."""
try:
return SmardChunk.model_validate_json(json_data)
except ValidationError as exc:
logger.error("SMARD price schema change: {}", exc)
raise ValueError(f"SMARD price schema change: {exc}") from exc
@staticmethod
def _get(url: str) -> bytes:
"""Request a SMARD JSON resource with bounded retries."""
last_exc: Optional[Exception] = None
for attempt in range(1, 4):
try:
response = requests.get(
url,
headers={"User-Agent": "Akkudoktor-EOS/SMARD price provider"},
timeout=(5, 30),
)
response.raise_for_status()
return response.content
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as exc:
last_exc = exc
logger.warning("SMARD request attempt {}/3 failed for {}: {}", attempt, url, exc)
if attempt < 3:
time.sleep(2 * attempt)
if last_exc is not None:
raise last_exc
raise RuntimeError(f"SMARD request failed without an exception: {url}")
def _chunk_timestamps(
self, index: SmardIndex, start_datetime: datetime, end_datetime: datetime
) -> list[int]:
"""Select all weekly chunks overlapping the requested datetime range."""
start_ms = int(to_datetime(start_datetime).timestamp() * 1000)
end_ms = int(to_datetime(end_datetime).timestamp() * 1000)
timestamps = sorted(set(index.timestamps))
selected: list[int] = []
for position, chunk_start in enumerate(timestamps):
next_start = timestamps[position + 1] if position + 1 < len(timestamps) else None
overlaps_start = next_start is None or next_start > start_ms
if chunk_start <= end_ms and overlaps_start:
selected.append(chunk_start)
return selected
@cache_in_file(with_ttl="1 hour")
def _request_forecast(self, start_date: Optional[str] = None) -> EnergyChartsElecPrice:
"""Fetch and normalize quarter-hourly German/Luxembourg day-ahead prices from SMARD."""
if not self.ems_start_datetime:
raise ValueError(f"Start DateTime not set: {self.ems_start_datetime}")
if start_date is None:
start_datetime = self.ems_start_datetime - to_duration("35 days")
else:
start_datetime = to_datetime(
start_date, in_timezone=self.config.general.timezone
).start_of("day")
end_datetime = to_datetime(self.end_datetime).end_of("day")
settings = self.config.elecprice.smard
filter_id = settings.filter_id
region = settings.region
resolution = "quarterhour"
index_url = f"{SMARD_BASE_URL}/{filter_id}/{region}/index_{resolution}.json"
index = self._validate_index(self._get(index_url))
chunk_timestamps = self._chunk_timestamps(index, start_datetime, end_datetime)
if not chunk_timestamps:
raise ValueError("SMARD index contains no price chunks for the requested period")
values_by_timestamp: dict[int, float] = {}
latest_created = 0
for chunk_timestamp in chunk_timestamps:
chunk_url = (
f"{SMARD_BASE_URL}/{filter_id}/{region}/"
f"{filter_id}_{region}_{resolution}_{chunk_timestamp}.json"
)
chunk = self._validate_chunk(self._get(chunk_url))
latest_created = max(latest_created, chunk.meta_data.created)
for timestamp_ms, price_eur_mwh in chunk.series:
if price_eur_mwh is None:
continue
if int(start_datetime.timestamp() * 1000) <= timestamp_ms <= int(
end_datetime.timestamp() * 1000
):
values_by_timestamp[timestamp_ms] = price_eur_mwh
if not values_by_timestamp:
raise ValueError("SMARD response contains no usable day-ahead prices")
ordered_values = sorted(values_by_timestamp.items())
self.update_datetime = to_datetime(
latest_created / 1000, in_timezone=self.config.general.timezone
)
return EnergyChartsElecPrice(
license_info="CC BY 4.0 Bundesnetzagentur | SMARD.de",
unix_seconds=[timestamp_ms // 1000 for timestamp_ms, _ in ordered_values],
price=[price for _, price in ordered_values],
unit="EUR/MWh",
deprecated=False,
)
@@ -13,6 +13,7 @@ from akkudoktoreos.prediction.feedintariffenergycharts import (
) )
from akkudoktoreos.prediction.feedintarifffixed import FeedInTariffFixedCommonSettings from akkudoktoreos.prediction.feedintarifffixed import FeedInTariffFixedCommonSettings
from akkudoktoreos.prediction.feedintariffimport import FeedInTariffImportCommonSettings from akkudoktoreos.prediction.feedintariffimport import FeedInTariffImportCommonSettings
from akkudoktoreos.prediction.feedintariffsmard import FeedInTariffSMARDCommonSettings
from akkudoktoreos.prediction.feedintarifftibber import FeedInTariffTibberCommonSettings from akkudoktoreos.prediction.feedintarifftibber import FeedInTariffTibberCommonSettings
@@ -28,6 +29,7 @@ def elecprice_provider_ids() -> list[str]:
"FeedInTariffFixed", "FeedInTariffFixed",
"FeedInTariffEnergyCharts", "FeedInTariffEnergyCharts",
"FeedInTariffImport", "FeedInTariffImport",
"FeedInTariffSMARD",
"FeedInTariffTibber", "FeedInTariffTibber",
] ]
@@ -61,6 +63,10 @@ class FeedInTariffCommonProviderSettings(SettingsBaseModel):
default=None, default=None,
json_schema_extra={"description": "FeedInTariffTibber settings", "examples": [None]}, json_schema_extra={"description": "FeedInTariffTibber settings", "examples": [None]},
) )
FeedInTariffSMARD: Optional[FeedInTariffSMARDCommonSettings] = Field(
default=None,
json_schema_extra={"description": "FeedInTariffSMARD settings", "examples": [None]},
)
class FeedInTariffCommonSettings(SettingsBaseModel): class FeedInTariffCommonSettings(SettingsBaseModel):
@@ -83,6 +89,7 @@ class FeedInTariffCommonSettings(SettingsBaseModel):
"FeedInTariffFixed", "FeedInTariffFixed",
"FeedInTariffEnergyCharts", "FeedInTariffEnergyCharts",
"FeedInTariffImport", "FeedInTariffImport",
"FeedInTariffSMARD",
"FeedInTariffTibber", "FeedInTariffTibber",
], ],
}, },
@@ -99,6 +106,7 @@ class FeedInTariffCommonSettings(SettingsBaseModel):
"FeedInTariffFixed": None, "FeedInTariffFixed": None,
"FeedInTariffEnergyCharts": None, "FeedInTariffEnergyCharts": None,
"FeedInTariffImport": None, "FeedInTariffImport": None,
"FeedInTariffSMARD": None,
"FeedInTariffTibber": None, "FeedInTariffTibber": None,
}, },
], ],
@@ -116,8 +116,9 @@ class FeedInTariffEnergyCharts(FeedInTariffProvider):
energycharts = ElecPriceEnergyCharts() energycharts = ElecPriceEnergyCharts()
if len(history) > 800 * slots_per_hour: if len(history) > 800 * slots_per_hour:
logger.info( logger.info(
"Using weekly seasonal ETS forecast for Energy-Charts feed-in tariff " "Using weekly seasonal ETS forecast for {} "
"with {} historical values.", "with {} historical values.",
self.provider_id(),
len(history), len(history),
) )
return energycharts._predict_ets( return energycharts._predict_ets(
@@ -125,8 +126,9 @@ class FeedInTariffEnergyCharts(FeedInTariffProvider):
) )
if len(history) > 168 * slots_per_hour: if len(history) > 168 * slots_per_hour:
logger.info( logger.info(
"Using daily seasonal ETS forecast for Energy-Charts feed-in tariff " "Using daily seasonal ETS forecast for {} "
"with {} historical values.", "with {} historical values.",
self.provider_id(),
len(history), len(history),
) )
return energycharts._predict_ets( return energycharts._predict_ets(
@@ -134,12 +136,13 @@ class FeedInTariffEnergyCharts(FeedInTariffProvider):
) )
if len(history) > 0: if len(history) > 0:
logger.warning( logger.warning(
"Using constant median fallback for Energy-Charts feed-in tariff " "Using constant median fallback for {} "
"with only {} historical values.", "with only {} historical values.",
self.provider_id(),
len(history), len(history),
) )
return energycharts._predict_median(history, hours=slots) return energycharts._predict_median(history, hours=slots)
logger.error("No feed-in tariff data available for Energy-Charts prediction") logger.error("No feed-in tariff data available for {} prediction", self.provider_id())
raise ValueError("No data available") raise ValueError("No data available")
def _update_data(self, force_update: Optional[bool] = False) -> None: def _update_data(self, force_update: Optional[bool] = False) -> None:
@@ -181,8 +184,9 @@ class FeedInTariffEnergyCharts(FeedInTariffProvider):
if needs_update: if needs_update:
logger.info( logger.info(
"Update FeedInTariffEnergyCharts is needed, last in history: {}, " "Update {} is needed, last in history: {}, "
"force_update={}, history_refresh={}", "force_update={}, history_refresh={}",
self.provider_id(),
self.highest_orig_datetime, self.highest_orig_datetime,
bool(force_update), bool(force_update),
needs_history_refresh, needs_history_refresh,
@@ -211,15 +215,17 @@ class FeedInTariffEnergyCharts(FeedInTariffProvider):
# slots, so downstream (e.g. /gesamtlast, optimization) still # slots, so downstream (e.g. /gesamtlast, optimization) still
# gets a usable feed-in tariff series. # gets a usable feed-in tariff series.
logger.warning( logger.warning(
"Energy-Charts feed-in tariff update failed ({}); keeping " "{} update failed ({}); keeping "
"existing history until {} and extrapolating the remaining " "existing history until {} and extrapolating the remaining "
"slots via ETS.", "slots via ETS.",
self.provider_id(),
exc, exc,
self.highest_orig_datetime, self.highest_orig_datetime,
) )
else: else:
logger.info( logger.info(
"No update FeedInTariffEnergyCharts is needed, last in history: {}", "No update {} is needed, last in history: {}",
self.provider_id(),
self.highest_orig_datetime, self.highest_orig_datetime,
) )
@@ -0,0 +1,29 @@
"""Provide direct-marketing feed-in prices from SMARD day-ahead data."""
from typing import Optional
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.prediction.elecpriceenergycharts import EnergyChartsElecPrice
from akkudoktoreos.prediction.elecpricesmard import ElecPriceSMARD
from akkudoktoreos.prediction.feedintariffenergycharts import FeedInTariffEnergyCharts
class FeedInTariffSMARDCommonSettings(SettingsBaseModel):
"""Settings for SMARD feed-in prices shared with ``elecprice.smard``."""
class FeedInTariffSMARD(FeedInTariffEnergyCharts):
"""Use raw SMARD day-ahead market prices for direct-marketing feed-in revenue."""
@classmethod
def provider_id(cls) -> str:
"""Return the unique identifier for the direct SMARD feed-in provider."""
return "FeedInTariffSMARD"
def _request_forecast(
self, start_date: Optional[str] = None, force_update: Optional[bool] = False
) -> EnergyChartsElecPrice:
"""Reuse the cached direct SMARD request without import-price components."""
return ElecPriceSMARD()._request_forecast( # type: ignore[call-arg]
start_date=start_date, force_update=force_update
)
@@ -35,11 +35,13 @@ from akkudoktoreos.prediction.elecpriceakkudoktor import ElecPriceAkkudoktor
from akkudoktoreos.prediction.elecpriceenergycharts import ElecPriceEnergyCharts from akkudoktoreos.prediction.elecpriceenergycharts import ElecPriceEnergyCharts
from akkudoktoreos.prediction.elecpricefixed import ElecPriceFixed from akkudoktoreos.prediction.elecpricefixed import ElecPriceFixed
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImport from akkudoktoreos.prediction.elecpriceimport import ElecPriceImport
from akkudoktoreos.prediction.elecpricesmard import ElecPriceSMARD
from akkudoktoreos.prediction.elecpricetibber import ElecPriceTibber from akkudoktoreos.prediction.elecpricetibber import ElecPriceTibber
from akkudoktoreos.prediction.feedintariffakkudoktor import FeedInTariffAkkudoktor from akkudoktoreos.prediction.feedintariffakkudoktor import FeedInTariffAkkudoktor
from akkudoktoreos.prediction.feedintariffenergycharts import FeedInTariffEnergyCharts from akkudoktoreos.prediction.feedintariffenergycharts import FeedInTariffEnergyCharts
from akkudoktoreos.prediction.feedintarifffixed import FeedInTariffFixed from akkudoktoreos.prediction.feedintarifffixed import FeedInTariffFixed
from akkudoktoreos.prediction.feedintariffimport import FeedInTariffImport from akkudoktoreos.prediction.feedintariffimport import FeedInTariffImport
from akkudoktoreos.prediction.feedintariffsmard import FeedInTariffSMARD
from akkudoktoreos.prediction.feedintarifftibber import FeedInTariffTibber from akkudoktoreos.prediction.feedintarifftibber import FeedInTariffTibber
from akkudoktoreos.prediction.loadakkudoktor import ( from akkudoktoreos.prediction.loadakkudoktor import (
LoadAkkudoktor, LoadAkkudoktor,
@@ -81,6 +83,7 @@ class PredictionCommonSettings(SettingsBaseModel):
# Initialize forecast providers, all are singletons. # Initialize forecast providers, all are singletons.
elecprice_akkudoktor = ElecPriceAkkudoktor() elecprice_akkudoktor = ElecPriceAkkudoktor()
elecprice_energy_charts = ElecPriceEnergyCharts() elecprice_energy_charts = ElecPriceEnergyCharts()
elecprice_smard = ElecPriceSMARD()
elecprice_tibber = ElecPriceTibber() elecprice_tibber = ElecPriceTibber()
elecprice_fixed = ElecPriceFixed() elecprice_fixed = ElecPriceFixed()
elecprice_import = ElecPriceImport() elecprice_import = ElecPriceImport()
@@ -88,6 +91,7 @@ feedintariff_energy_charts = FeedInTariffEnergyCharts()
feedintariff_akkudoktor = FeedInTariffAkkudoktor() feedintariff_akkudoktor = FeedInTariffAkkudoktor()
feedintariff_fixed = FeedInTariffFixed() feedintariff_fixed = FeedInTariffFixed()
feedintariff_import = FeedInTariffImport() feedintariff_import = FeedInTariffImport()
feedintariff_smard = FeedInTariffSMARD()
feedintariff_tibber = FeedInTariffTibber() feedintariff_tibber = FeedInTariffTibber()
loadforecast_akkudoktor = LoadAkkudoktor() loadforecast_akkudoktor = LoadAkkudoktor()
loadforecast_akkudoktor_adjusted = LoadAkkudoktorAdjusted() loadforecast_akkudoktor_adjusted = LoadAkkudoktorAdjusted()
@@ -110,6 +114,7 @@ def prediction_providers() -> (
Union[ Union[
ElecPriceAkkudoktor, ElecPriceAkkudoktor,
ElecPriceEnergyCharts, ElecPriceEnergyCharts,
ElecPriceSMARD,
ElecPriceTibber, ElecPriceTibber,
ElecPriceFixed, ElecPriceFixed,
ElecPriceImport, ElecPriceImport,
@@ -117,6 +122,7 @@ def prediction_providers() -> (
FeedInTariffAkkudoktor, FeedInTariffAkkudoktor,
FeedInTariffFixed, FeedInTariffFixed,
FeedInTariffImport, FeedInTariffImport,
FeedInTariffSMARD,
FeedInTariffTibber, FeedInTariffTibber,
LoadAkkudoktor, LoadAkkudoktor,
LoadAkkudoktorAdjusted, LoadAkkudoktorAdjusted,
@@ -142,6 +148,7 @@ def prediction_providers() -> (
global \ global \
elecprice_akkudoktor, \ elecprice_akkudoktor, \
elecprice_energy_charts, \ elecprice_energy_charts, \
elecprice_smard, \
elecprice_tibber, \ elecprice_tibber, \
elecprice_fixed, \ elecprice_fixed, \
elecprice_import, \ elecprice_import, \
@@ -149,6 +156,7 @@ def prediction_providers() -> (
feedintariff_akkudoktor, \ feedintariff_akkudoktor, \
feedintariff_fixed, \ feedintariff_fixed, \
feedintariff_import, \ feedintariff_import, \
feedintariff_smard, \
feedintariff_tibber, \ feedintariff_tibber, \
loadforecast_akkudoktor, \ loadforecast_akkudoktor, \
loadforecast_akkudoktor_adjusted, \ loadforecast_akkudoktor_adjusted, \
@@ -169,6 +177,7 @@ def prediction_providers() -> (
return [ return [
elecprice_akkudoktor, elecprice_akkudoktor,
elecprice_energy_charts, elecprice_energy_charts,
elecprice_smard,
elecprice_tibber, elecprice_tibber,
elecprice_fixed, elecprice_fixed,
elecprice_import, elecprice_import,
@@ -176,6 +185,7 @@ def prediction_providers() -> (
feedintariff_akkudoktor, feedintariff_akkudoktor,
feedintariff_fixed, feedintariff_fixed,
feedintariff_import, feedintariff_import,
feedintariff_smard,
feedintariff_tibber, feedintariff_tibber,
loadforecast_akkudoktor, loadforecast_akkudoktor,
loadforecast_akkudoktor_adjusted, loadforecast_akkudoktor_adjusted,
@@ -201,6 +211,7 @@ class Prediction(PredictionContainer):
Union[ Union[
ElecPriceAkkudoktor, ElecPriceAkkudoktor,
ElecPriceEnergyCharts, ElecPriceEnergyCharts,
ElecPriceSMARD,
ElecPriceTibber, ElecPriceTibber,
ElecPriceFixed, ElecPriceFixed,
ElecPriceImport, ElecPriceImport,
@@ -208,6 +219,7 @@ class Prediction(PredictionContainer):
FeedInTariffAkkudoktor, FeedInTariffAkkudoktor,
FeedInTariffFixed, FeedInTariffFixed,
FeedInTariffImport, FeedInTariffImport,
FeedInTariffSMARD,
FeedInTariffTibber, FeedInTariffTibber,
LoadAkkudoktor, LoadAkkudoktor,
LoadAkkudoktorAdjusted, LoadAkkudoktorAdjusted,
+56
View File
@@ -8,6 +8,7 @@ import requests
from loguru import logger from loguru import logger
from akkudoktoreos.core.cache import CacheFileStore from akkudoktoreos.core.cache import CacheFileStore
from akkudoktoreos.config.configabc import ValueTimeWindowSequence
from akkudoktoreos.core.coreabc import get_ems from akkudoktoreos.core.coreabc import get_ems
from akkudoktoreos.prediction.elecpriceakkudoktor import ( from akkudoktoreos.prediction.elecpriceakkudoktor import (
AkkudoktorElecPrice, AkkudoktorElecPrice,
@@ -158,6 +159,61 @@ 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_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."""
provider.config.elecprice.charges_kwh = None
provider.config.elecprice.charge_components_kwh = {
"electricity_tax": 0.0205,
"concession_fee": 0.0132,
"kwkg_levy": 0.00446,
"section_19_levy": 0.01559,
"offshore_grid_levy": 0.00941,
}
provider.config.elecprice.vat_rate = 1.19
provider.config.elecprice.network_fees_kwh = ValueTimeWindowSequence(
windows=[
{"start_time": "00:00", "duration": "7 hours", "value": 0.0095},
{"start_time": "07:00", "duration": "8 hours", "value": 0.0953},
{"start_time": "15:00", "duration": "5 hours", "value": 0.1565},
{"start_time": "20:00", "duration": "4 hours", "value": 0.0953},
]
)
start = to_datetime("2026-01-15 00:00:00", in_timezone="Europe/Berlin")
timestamps = [start, start.add(hours=7), start.add(hours=15), start.add(hours=20)]
data = EnergyChartsElecPrice(
license_info="",
unix_seconds=[int(timestamp.timestamp()) for timestamp in timestamps],
price=[100.0] * len(timestamps),
unit="EUR/MWh",
deprecated=False,
)
result_kwh = provider._parse_data(data) * 1000
assert result_kwh.iloc[0] == pytest.approx((0.1 + 0.06316 + 0.0095) * 1.19)
assert result_kwh.iloc[1] == pytest.approx((0.1 + 0.06316 + 0.0953) * 1.19)
assert result_kwh.iloc[2] == pytest.approx((0.1 + 0.06316 + 0.1565) * 1.19)
assert result_kwh.iloc[3] == pytest.approx((0.1 + 0.06316 + 0.0953) * 1.19)
def test_market_price_charge_round_trip(provider):
"""Seasonal forecasting can remove and reapply timestamp-dependent retail charges."""
provider.config.elecprice.charges_kwh = None
provider.config.elecprice.charge_components_kwh = {"statutory_charges": 0.06316}
provider.config.elecprice.vat_rate = 1.19
provider.config.elecprice.network_fees_kwh = ValueTimeWindowSequence(
windows=[{"start_time": "15:00", "duration": "5 hours", "value": 0.1565}]
)
timestamp = to_datetime("2026-01-15 16:30:00", in_timezone="Europe/Berlin")
market_price_wh = -0.00002
retail_price_wh = provider._price_with_charges(market_price_wh, timestamp)
assert provider._price_without_charges(retail_price_wh, timestamp) == pytest.approx(
market_price_wh
)
@patch("requests.get") @patch("requests.get")
def test_update_data_with_incomplete_forecast(mock_get, provider): def test_update_data_with_incomplete_forecast(mock_get, provider):
"""Test `_update_data` with incomplete or missing forecast data.""" """Test `_update_data` with incomplete or missing forecast data."""
+77
View File
@@ -0,0 +1,77 @@
# ruff: noqa: S101
import json
from unittest.mock import Mock, patch
import pytest
from akkudoktoreos.core.coreabc import get_ems
from akkudoktoreos.prediction.elecprice import ElecPriceCommonSettings
from akkudoktoreos.prediction.elecpricesmard import ElecPriceSMARD
from akkudoktoreos.utils.datetimeutil import to_datetime
@pytest.fixture
def provider(config_eos):
"""Configure and return the direct SMARD singleton provider."""
config_eos.elecprice = ElecPriceCommonSettings(provider="ElecPriceSMARD")
provider = ElecPriceSMARD()
provider.highest_orig_datetime = None
get_ems().set_start_datetime(
to_datetime("2026-07-27 00:00:00", in_timezone="Europe/Berlin")
)
return provider
def _response(payload):
response = Mock()
response.content = json.dumps(payload).encode()
response.raise_for_status.return_value = None
return response
@patch("akkudoktoreos.prediction.elecpricesmard.requests.get")
def test_request_forecast_fetches_index_and_overlapping_chunks(mock_get, provider):
"""SMARD index and weekly chunks are combined, sorted, and stripped of null values."""
chunk_start = 1785103200000
mock_get.side_effect = [
_response({"timestamps": [chunk_start]}),
_response(
{
"meta_data": {"version": 1, "created": 1785500527370},
"series": [
[1785103200000, 86.04],
[1785106800000, None],
[1785110400000, -1.25],
],
}
),
]
result = provider._request_forecast(
start_date="2026-07-27", force_update=True
)
assert result.unix_seconds == [1785103200, 1785110400]
assert result.price == [86.04, -1.25]
assert result.license_info == "CC BY 4.0 Bundesnetzagentur | SMARD.de"
assert mock_get.call_count == 2
assert mock_get.call_args_list[0].args[0].endswith("/4169/DE/index_quarterhour.json")
assert mock_get.call_args_list[1].args[0].endswith(
"/4169/DE/4169_DE_quarterhour_1785103200000.json"
)
def test_chunk_selection_includes_preceding_overlapping_chunk(provider):
"""A range beginning mid-week includes the chunk that started before it."""
index = provider._validate_index(
json.dumps({"timestamps": [1000, 2000, 3000]}).encode()
)
start = to_datetime(2.5, in_timezone="UTC")
end = to_datetime(3.5, in_timezone="UTC")
assert provider._chunk_timestamps(index, start, end) == [2000, 3000]
def test_smard_provider_is_enabled(provider):
assert provider.enabled()
+41
View File
@@ -0,0 +1,41 @@
# ruff: noqa: S101
from unittest.mock import patch
from akkudoktoreos.core.coreabc import get_ems
from akkudoktoreos.prediction.elecpriceenergycharts import EnergyChartsElecPrice
from akkudoktoreos.prediction.elecpricesmard import ElecPriceSMARD
from akkudoktoreos.prediction.feedintariffsmard import FeedInTariffSMARD
from akkudoktoreos.utils.datetimeutil import to_datetime
def test_feed_in_tariff_smard_reuses_raw_smard_market_prices(config_eos):
"""The feed-in provider delegates to SMARD and stores no import-price components."""
config_eos.merge_settings_from_dict(
{
"elecprice": {"provider": "ElecPriceSMARD"},
"feedintariff": {
"direct_marketing_enabled": True,
"provider": "FeedInTariffSMARD",
},
}
)
get_ems().set_start_datetime(
to_datetime("2026-08-01 00:00:00", in_timezone="Europe/Berlin")
)
provider = FeedInTariffSMARD()
data = EnergyChartsElecPrice(
license_info="CC BY 4.0 Bundesnetzagentur | SMARD.de",
unix_seconds=[1785535200],
price=[169.44],
unit="EUR/MWh",
deprecated=False,
)
with patch.object(ElecPriceSMARD, "_request_forecast", return_value=data) as request:
result = provider._request_forecast(start_date="2026-08-01", force_update=True)
assert provider.enabled()
assert result is data
assert provider._parse_data(result).iloc[0] == 169.44 / 1_000_000
request.assert_called_once_with(start_date="2026-08-01", force_update=True)
+30 -22
View File
@@ -6,11 +6,13 @@ from akkudoktoreos.prediction.elecpriceakkudoktor import ElecPriceAkkudoktor
from akkudoktoreos.prediction.elecpriceenergycharts import ElecPriceEnergyCharts from akkudoktoreos.prediction.elecpriceenergycharts import ElecPriceEnergyCharts
from akkudoktoreos.prediction.elecpricefixed import ElecPriceFixed from akkudoktoreos.prediction.elecpricefixed import ElecPriceFixed
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImport from akkudoktoreos.prediction.elecpriceimport import ElecPriceImport
from akkudoktoreos.prediction.elecpricesmard import ElecPriceSMARD
from akkudoktoreos.prediction.elecpricetibber import ElecPriceTibber from akkudoktoreos.prediction.elecpricetibber import ElecPriceTibber
from akkudoktoreos.prediction.feedintariffakkudoktor import FeedInTariffAkkudoktor from akkudoktoreos.prediction.feedintariffakkudoktor import FeedInTariffAkkudoktor
from akkudoktoreos.prediction.feedintariffenergycharts import FeedInTariffEnergyCharts from akkudoktoreos.prediction.feedintariffenergycharts import FeedInTariffEnergyCharts
from akkudoktoreos.prediction.feedintarifffixed import FeedInTariffFixed from akkudoktoreos.prediction.feedintarifffixed import FeedInTariffFixed
from akkudoktoreos.prediction.feedintariffimport import FeedInTariffImport from akkudoktoreos.prediction.feedintariffimport import FeedInTariffImport
from akkudoktoreos.prediction.feedintariffsmard import FeedInTariffSMARD
from akkudoktoreos.prediction.feedintarifftibber import FeedInTariffTibber from akkudoktoreos.prediction.feedintarifftibber import FeedInTariffTibber
from akkudoktoreos.prediction.loadakkudoktor import ( from akkudoktoreos.prediction.loadakkudoktor import (
LoadAkkudoktor, LoadAkkudoktor,
@@ -46,6 +48,7 @@ def forecast_providers():
return [ return [
ElecPriceAkkudoktor(), ElecPriceAkkudoktor(),
ElecPriceEnergyCharts(), ElecPriceEnergyCharts(),
ElecPriceSMARD(),
ElecPriceTibber(), ElecPriceTibber(),
ElecPriceFixed(), ElecPriceFixed(),
ElecPriceImport(), ElecPriceImport(),
@@ -53,6 +56,7 @@ def forecast_providers():
FeedInTariffAkkudoktor(), FeedInTariffAkkudoktor(),
FeedInTariffFixed(), FeedInTariffFixed(),
FeedInTariffImport(), FeedInTariffImport(),
FeedInTariffSMARD(),
FeedInTariffTibber(), FeedInTariffTibber(),
LoadAkkudoktor(), LoadAkkudoktor(),
LoadAkkudoktorAdjusted(), LoadAkkudoktorAdjusted(),
@@ -102,28 +106,30 @@ def test_provider_sequence(prediction):
"""Test the provider sequence is maintained in the Prediction instance.""" """Test the provider sequence is maintained in the Prediction instance."""
assert isinstance(prediction.providers[0], ElecPriceAkkudoktor) assert isinstance(prediction.providers[0], ElecPriceAkkudoktor)
assert isinstance(prediction.providers[1], ElecPriceEnergyCharts) assert isinstance(prediction.providers[1], ElecPriceEnergyCharts)
assert isinstance(prediction.providers[2], ElecPriceTibber) assert isinstance(prediction.providers[2], ElecPriceSMARD)
assert isinstance(prediction.providers[3], ElecPriceFixed) assert isinstance(prediction.providers[3], ElecPriceTibber)
assert isinstance(prediction.providers[4], ElecPriceImport) assert isinstance(prediction.providers[4], ElecPriceFixed)
assert isinstance(prediction.providers[5], FeedInTariffEnergyCharts) assert isinstance(prediction.providers[5], ElecPriceImport)
assert isinstance(prediction.providers[6], FeedInTariffAkkudoktor) assert isinstance(prediction.providers[6], FeedInTariffEnergyCharts)
assert isinstance(prediction.providers[7], FeedInTariffFixed) assert isinstance(prediction.providers[7], FeedInTariffAkkudoktor)
assert isinstance(prediction.providers[8], FeedInTariffImport) assert isinstance(prediction.providers[8], FeedInTariffFixed)
assert isinstance(prediction.providers[9], FeedInTariffTibber) assert isinstance(prediction.providers[9], FeedInTariffImport)
assert isinstance(prediction.providers[10], LoadAkkudoktor) assert isinstance(prediction.providers[10], FeedInTariffSMARD)
assert isinstance(prediction.providers[11], LoadAkkudoktorAdjusted) assert isinstance(prediction.providers[11], FeedInTariffTibber)
assert isinstance(prediction.providers[12], LoadVrm) assert isinstance(prediction.providers[12], LoadAkkudoktor)
assert isinstance(prediction.providers[13], LoadImport) assert isinstance(prediction.providers[13], LoadAkkudoktorAdjusted)
assert isinstance(prediction.providers[14], PVForecastAkkudoktor) assert isinstance(prediction.providers[14], LoadVrm)
assert isinstance(prediction.providers[15], PVForecastVrm) assert isinstance(prediction.providers[15], LoadImport)
assert isinstance(prediction.providers[16], PVForecastPVNode) assert isinstance(prediction.providers[16], PVForecastAkkudoktor)
assert isinstance(prediction.providers[17], PVForecastForecastSolar) assert isinstance(prediction.providers[17], PVForecastVrm)
assert isinstance(prediction.providers[18], PVForecastSolcast) assert isinstance(prediction.providers[18], PVForecastPVNode)
assert isinstance(prediction.providers[19], PVForecastImport) assert isinstance(prediction.providers[19], PVForecastForecastSolar)
assert isinstance(prediction.providers[20], WeatherBrightSky) assert isinstance(prediction.providers[20], PVForecastSolcast)
assert isinstance(prediction.providers[21], WeatherClearOutside) assert isinstance(prediction.providers[21], PVForecastImport)
assert isinstance(prediction.providers[22], WeatherOpenMeteo) assert isinstance(prediction.providers[22], WeatherBrightSky)
assert isinstance(prediction.providers[23], WeatherImport) assert isinstance(prediction.providers[23], WeatherClearOutside)
assert isinstance(prediction.providers[24], WeatherOpenMeteo)
assert isinstance(prediction.providers[25], WeatherImport)
def test_provider_by_id(prediction, forecast_providers): def test_provider_by_id(prediction, forecast_providers):
@@ -141,12 +147,14 @@ def test_prediction_repr(prediction):
assert "Prediction([" in result assert "Prediction([" in result
assert "ElecPriceAkkudoktor" in result assert "ElecPriceAkkudoktor" in result
assert "ElecPriceEnergyCharts" in result assert "ElecPriceEnergyCharts" in result
assert "ElecPriceSMARD" in result
assert "ElecPriceTibber" in result assert "ElecPriceTibber" in result
assert "ElecPriceFixed" in result assert "ElecPriceFixed" in result
assert "ElecPriceImport" in result assert "ElecPriceImport" in result
assert "FeedInTariffFixed" in result assert "FeedInTariffFixed" in result
assert "FeedInTariffAkkudoktor" in result assert "FeedInTariffAkkudoktor" in result
assert "FeedInTariffImport" in result assert "FeedInTariffImport" in result
assert "FeedInTariffSMARD" in result
assert "FeedInTariffTibber" in result assert "FeedInTariffTibber" in result
assert "LoadAkkudoktor" in result assert "LoadAkkudoktor" in result
assert "LoadVrm" in result assert "LoadVrm" in result