feat: add Akkudoktor feed-in provider (#1187)
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 `FeedInTariffAkkudoktor` provider uses raw day-ahead market prices from
`https://api.akkudoktor.net/prices` as `feed_in_tariff_wh`. It does not add electricity import
charges or VAT. Published prices are extended to the configured prediction horizon with the same
seasonal ETS or median fallback used by the Akkudoktor electricity-price provider.

The Akkudoktor endpoint currently forwards hourly market prices from aWATTar. With a 15-minute
optimization interval, EOS holds each hourly price constant for its four quarter-hour slots. This
keeps the slot grid consistent but does not create genuine quarter-hour market prices.

Signed-off-by: Andreas Schmitz <akkudoktor.net>
Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
Bobby Noelte
2026-07-23 22:37:38 +02:00
committed by GitHub
parent ed61918fe0
commit 4c8a8e40f5
14 changed files with 327 additions and 31 deletions

View File

@@ -0,0 +1,100 @@
import asyncio
import json
from unittest.mock import Mock, patch
import pytest
import pytest_asyncio
from akkudoktoreos.core.coreabc import get_ems
from akkudoktoreos.prediction.elecpriceakkudoktor import AkkudoktorElecPrice
from akkudoktoreos.prediction.feedintariffakkudoktor import FeedInTariffAkkudoktor
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration
@pytest.fixture
def provider(config_eos):
config_eos.merge_settings_from_dict(
{
"elecprice": {"charges_kwh": 0.30},
"feedintariff": {"provider": "FeedInTariffAkkudoktor"},
}
)
value = FeedInTariffAkkudoktor()
value.highest_orig_datetime = None
value.records.clear()
assert value.enabled()
return value
@pytest.fixture
def response_data():
return {
"meta": {
"start_timestamp": "1733871600",
"end_timestamp": "1733958000",
"start": "2024-12-11T00:00:00+01:00",
"end": "2024-12-12T00:00:00+01:00",
},
"values": [
{
"start_timestamp": 1733871600,
"end_timestamp": 1733875200,
"start": "2024-12-11T00:00:00+01:00",
"end": "2024-12-11T01:00:00+01:00",
"marketprice": 100.0,
"unit": "Eur/MWh",
"marketpriceEurocentPerKWh": 10.0,
},
{
"start_timestamp": 1733875200,
"end_timestamp": 1733878800,
"start": "2024-12-11T01:00:00+01:00",
"end": "2024-12-11T02:00:00+01:00",
"marketprice": 200.0,
"unit": "Eur/MWh",
"marketpriceEurocentPerKWh": 20.0,
},
],
}
class TestFeedInTariffAkkudokor:
def test_provider_is_available(self, config_eos):
assert "FeedInTariffAkkudoktor" in config_eos.feedintariff.providers
def test_parse_data_uses_raw_market_price_without_import_charges(self, provider, response_data):
data = AkkudoktorElecPrice.model_validate(response_data)
series = provider._parse_data(data)
assert series.iloc[0] == pytest.approx(0.0001)
@pytest.mark.asyncio
async def test_hourly_prices_are_held_constant_on_quarter_hour_grid(self, provider, response_data):
data = AkkudoktorElecPrice.model_validate(response_data)
await provider.key_from_series("feed_in_tariff_wh", provider._parse_data(data))
start = to_datetime("2024-12-11 00:00:00", in_timezone="Europe/Berlin")
values = await provider.key_to_array(
key="feed_in_tariff_wh",
start_datetime=start,
end_datetime=start + to_duration("2 hours"),
interval=to_duration("15 minutes"),
fill_method="ffill",
)
assert values.tolist() == pytest.approx([0.0001] * 4 + [0.0002] * 4)
@patch("requests.get")
def test_request_uses_akkudoktor_prices_endpoint(self, mock_get, provider, response_data):
response = Mock()
response.content = json.dumps(response_data)
response.raise_for_status = Mock()
mock_get.return_value = response
get_ems().set_start_datetime(to_datetime("2024-12-11 00:00:00", in_timezone="Europe/Berlin"))
provider._request_forecast(force_update=True)
url = mock_get.call_args[0][0]
assert url.startswith("https://api.akkudoktor.net/prices?")
assert "tz=Europe/Berlin" in url
assert mock_get.call_args.kwargs["timeout"] == (5, 20)