2024-12-15 14:40:03 +01:00
|
|
|
import json
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from unittest.mock import Mock, patch
|
|
|
|
|
|
2024-12-29 18:42:49 +01:00
|
|
|
import numpy as np
|
2024-12-15 14:40:03 +01:00
|
|
|
import pytest
|
2024-12-29 18:42:49 +01:00
|
|
|
import requests
|
2025-06-10 22:00:28 +02:00
|
|
|
from loguru import logger
|
2024-12-15 14:40:03 +01:00
|
|
|
|
2025-02-12 21:35:51 +01:00
|
|
|
from akkudoktoreos.core.cache import CacheFileStore
|
2026-02-22 14:12:42 +01:00
|
|
|
from akkudoktoreos.core.coreabc import get_ems
|
2024-12-15 14:40:03 +01:00
|
|
|
from akkudoktoreos.prediction.elecpriceakkudoktor import (
|
|
|
|
|
AkkudoktorElecPrice,
|
|
|
|
|
AkkudoktorElecPriceValue,
|
|
|
|
|
ElecPriceAkkudoktor,
|
|
|
|
|
)
|
|
|
|
|
from akkudoktoreos.utils.datetimeutil import to_datetime
|
|
|
|
|
|
|
|
|
|
DIR_TESTDATA = Path(__file__).absolute().parent.joinpath("testdata")
|
|
|
|
|
|
|
|
|
|
FILE_TESTDATA_ELECPRICEAKKUDOKTOR_1_JSON = DIR_TESTDATA.joinpath(
|
|
|
|
|
"elecpriceforecast_akkudoktor_1.json"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2025-01-18 14:26:34 +01:00
|
|
|
def provider(monkeypatch, config_eos):
|
2024-12-15 14:40:03 +01:00
|
|
|
"""Fixture to create a ElecPriceProvider instance."""
|
2025-01-12 05:19:37 +01:00
|
|
|
monkeypatch.setenv("EOS_ELECPRICE__ELECPRICE_PROVIDER", "ElecPriceAkkudoktor")
|
|
|
|
|
config_eos.reset_settings()
|
2024-12-15 14:40:03 +01:00
|
|
|
return ElecPriceAkkudoktor()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def sample_akkudoktor_1_json():
|
|
|
|
|
"""Fixture that returns sample forecast data report."""
|
2025-02-12 21:35:51 +01:00
|
|
|
with FILE_TESTDATA_ELECPRICEAKKUDOKTOR_1_JSON.open(
|
|
|
|
|
"r", encoding="utf-8", newline=None
|
|
|
|
|
) as f_res:
|
2024-12-15 14:40:03 +01:00
|
|
|
input_data = json.load(f_res)
|
|
|
|
|
return input_data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def cache_store():
|
|
|
|
|
"""A pytest fixture that creates a new CacheFileStore instance for testing."""
|
|
|
|
|
return CacheFileStore()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------
|
|
|
|
|
# General forecast
|
|
|
|
|
# ------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
2025-01-18 14:26:34 +01:00
|
|
|
def test_singleton_instance(provider):
|
2024-12-15 14:40:03 +01:00
|
|
|
"""Test that ElecPriceForecast behaves as a singleton."""
|
|
|
|
|
another_instance = ElecPriceAkkudoktor()
|
2025-01-18 14:26:34 +01:00
|
|
|
assert provider is another_instance
|
2024-12-15 14:40:03 +01:00
|
|
|
|
|
|
|
|
|
2025-01-18 14:26:34 +01:00
|
|
|
def test_invalid_provider(provider, monkeypatch):
|
|
|
|
|
"""Test requesting an unsupported provider."""
|
2025-01-12 05:19:37 +01:00
|
|
|
monkeypatch.setenv("EOS_ELECPRICE__ELECPRICE_PROVIDER", "<invalid>")
|
2025-01-18 14:26:34 +01:00
|
|
|
provider.config.reset_settings()
|
|
|
|
|
assert not provider.enabled()
|
2024-12-15 14:40:03 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------
|
|
|
|
|
# Akkudoktor
|
|
|
|
|
# ------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
2024-12-29 18:42:49 +01:00
|
|
|
@patch("akkudoktoreos.prediction.elecpriceakkudoktor.logger.error")
|
2025-01-18 14:26:34 +01:00
|
|
|
def test_validate_data_invalid_format(mock_logger, provider):
|
2024-12-29 18:42:49 +01:00
|
|
|
"""Test validation for invalid Akkudoktor data."""
|
|
|
|
|
invalid_data = '{"invalid": "data"}'
|
|
|
|
|
with pytest.raises(ValueError):
|
2025-01-18 14:26:34 +01:00
|
|
|
provider._validate_data(invalid_data)
|
2024-12-29 18:42:49 +01:00
|
|
|
mock_logger.assert_called_once_with(mock_logger.call_args[0][0])
|
|
|
|
|
|
|
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
@patch("requests.get")
|
2025-01-18 14:26:34 +01:00
|
|
|
def test_request_forecast(mock_get, provider, sample_akkudoktor_1_json):
|
2024-12-15 14:40:03 +01:00
|
|
|
"""Test requesting forecast from Akkudoktor."""
|
|
|
|
|
# Mock response object
|
|
|
|
|
mock_response = Mock()
|
|
|
|
|
mock_response.status_code = 200
|
|
|
|
|
mock_response.content = json.dumps(sample_akkudoktor_1_json)
|
|
|
|
|
mock_get.return_value = mock_response
|
|
|
|
|
|
|
|
|
|
# Test function
|
2025-01-18 14:26:34 +01:00
|
|
|
akkudoktor_data = provider._request_forecast()
|
2024-12-15 14:40:03 +01:00
|
|
|
|
|
|
|
|
assert isinstance(akkudoktor_data, AkkudoktorElecPrice)
|
2025-06-10 22:00:28 +02:00
|
|
|
assert akkudoktor_data.values[0].model_dump() == AkkudoktorElecPriceValue(
|
2025-01-02 15:22:18 +01:00
|
|
|
start_timestamp=1733785200000,
|
|
|
|
|
end_timestamp=1733788800000,
|
|
|
|
|
start="2024-12-09T23:00:00.000Z",
|
|
|
|
|
end="2024-12-10T00:00:00.000Z",
|
|
|
|
|
marketprice=92.85,
|
2024-12-15 14:40:03 +01:00
|
|
|
unit="Eur/MWh",
|
2025-01-02 15:22:18 +01:00
|
|
|
marketpriceEurocentPerKWh=9.29,
|
2025-06-10 22:00:28 +02:00
|
|
|
).model_dump()
|
2024-12-15 14:40:03 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@patch("requests.get")
|
2025-01-18 14:26:34 +01:00
|
|
|
def test_update_data(mock_get, provider, sample_akkudoktor_1_json, cache_store):
|
2024-12-15 14:40:03 +01:00
|
|
|
"""Test fetching forecast from Akkudoktor."""
|
|
|
|
|
# Mock response object
|
|
|
|
|
mock_response = Mock()
|
|
|
|
|
mock_response.status_code = 200
|
|
|
|
|
mock_response.content = json.dumps(sample_akkudoktor_1_json)
|
|
|
|
|
mock_get.return_value = mock_response
|
|
|
|
|
|
|
|
|
|
cache_store.clear(clear_all=True)
|
|
|
|
|
|
|
|
|
|
# Call the method
|
2024-12-30 13:41:39 +01:00
|
|
|
ems_eos = get_ems()
|
2024-12-15 14:40:03 +01:00
|
|
|
ems_eos.set_start_datetime(to_datetime("2024-12-11 00:00:00", in_timezone="Europe/Berlin"))
|
2025-01-18 14:26:34 +01:00
|
|
|
provider.update_data(force_enable=True, force_update=True)
|
2024-12-15 14:40:03 +01:00
|
|
|
|
|
|
|
|
# Assert: Verify the result is as expected
|
|
|
|
|
mock_get.assert_called_once()
|
2026-09-09 07:56:38 +02:00
|
|
|
# The API response holds 48 datasets, 23 h of which already reach into the
|
|
|
|
|
# future, so the provider forecasts the remaining hours of the horizon itself.
|
|
|
|
|
assert len(provider) == 48 + provider.config.prediction.hours - 23
|
2024-12-15 14:40:03 +01:00
|
|
|
|
2025-01-18 14:26:34 +01:00
|
|
|
# Assert we get hours prioce values by resampling
|
|
|
|
|
np_price_array = provider.key_to_array(
|
2024-12-30 21:29:50 +01:00
|
|
|
key="elecprice_marketprice_wh",
|
2025-10-28 02:50:31 +01:00
|
|
|
start_datetime=provider.ems_start_datetime,
|
2025-01-18 14:26:34 +01:00
|
|
|
end_datetime=provider.end_datetime,
|
2024-12-15 14:40:03 +01:00
|
|
|
)
|
2025-01-18 14:26:34 +01:00
|
|
|
assert len(np_price_array) == provider.total_hours
|
2024-12-15 14:40:03 +01:00
|
|
|
|
|
|
|
|
# with open(FILE_TESTDATA_ELECPRICEAKKUDOKTOR_2_JSON, "w") as f_out:
|
2025-01-18 14:26:34 +01:00
|
|
|
# f_out.write(provider.to_json())
|
2024-12-15 14:40:03 +01:00
|
|
|
|
|
|
|
|
|
2024-12-29 18:42:49 +01:00
|
|
|
@patch("requests.get")
|
2025-01-18 14:26:34 +01:00
|
|
|
def test_update_data_with_incomplete_forecast(mock_get, provider):
|
2024-12-29 18:42:49 +01:00
|
|
|
"""Test `_update_data` with incomplete or missing forecast data."""
|
|
|
|
|
incomplete_data: dict = {"meta": {}, "values": []}
|
|
|
|
|
mock_response = Mock()
|
|
|
|
|
mock_response.status_code = 200
|
|
|
|
|
mock_response.content = json.dumps(incomplete_data)
|
|
|
|
|
mock_get.return_value = mock_response
|
2025-01-26 22:42:54 +01:00
|
|
|
logger.info("The following errors are intentional and part of the test.")
|
2024-12-29 18:42:49 +01:00
|
|
|
with pytest.raises(ValueError):
|
2025-01-18 14:26:34 +01:00
|
|
|
provider._update_data(force_update=True)
|
2024-12-29 18:42:49 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"status_code, exception",
|
|
|
|
|
[(400, requests.exceptions.HTTPError), (500, requests.exceptions.HTTPError), (200, None)],
|
|
|
|
|
)
|
|
|
|
|
@patch("requests.get")
|
|
|
|
|
def test_request_forecast_status_codes(
|
2025-01-18 14:26:34 +01:00
|
|
|
mock_get, provider, sample_akkudoktor_1_json, status_code, exception
|
2024-12-29 18:42:49 +01:00
|
|
|
):
|
|
|
|
|
"""Test handling of various API status codes."""
|
|
|
|
|
mock_response = Mock()
|
|
|
|
|
mock_response.status_code = status_code
|
|
|
|
|
mock_response.content = json.dumps(sample_akkudoktor_1_json)
|
|
|
|
|
mock_response.raise_for_status.side_effect = (
|
|
|
|
|
requests.exceptions.HTTPError if exception else None
|
|
|
|
|
)
|
|
|
|
|
mock_get.return_value = mock_response
|
|
|
|
|
if exception:
|
|
|
|
|
with pytest.raises(exception):
|
2025-01-18 14:26:34 +01:00
|
|
|
provider._request_forecast()
|
2024-12-29 18:42:49 +01:00
|
|
|
else:
|
2025-01-18 14:26:34 +01:00
|
|
|
provider._request_forecast()
|
2024-12-29 18:42:49 +01:00
|
|
|
|
|
|
|
|
|
2025-11-13 15:13:37 +01:00
|
|
|
@patch("requests.get")
|
2025-02-12 21:35:51 +01:00
|
|
|
@patch("akkudoktoreos.core.cache.CacheFileStore")
|
2025-11-13 15:13:37 +01:00
|
|
|
def test_cache_integration(mock_cache, mock_get, provider, sample_akkudoktor_1_json):
|
2024-12-29 18:42:49 +01:00
|
|
|
"""Test caching of 8-day electricity price data."""
|
2025-11-13 15:13:37 +01:00
|
|
|
# Mock response object
|
|
|
|
|
mock_response = Mock()
|
|
|
|
|
mock_response.status_code = 200
|
|
|
|
|
mock_response.content = json.dumps(sample_akkudoktor_1_json)
|
|
|
|
|
mock_get.return_value = mock_response
|
|
|
|
|
|
|
|
|
|
# Mock cache object
|
2024-12-29 18:42:49 +01:00
|
|
|
mock_cache_instance = mock_cache.return_value
|
|
|
|
|
mock_cache_instance.get.return_value = None # Simulate no cache
|
2025-11-13 15:13:37 +01:00
|
|
|
|
2025-01-18 14:26:34 +01:00
|
|
|
provider._update_data(force_update=True)
|
2024-12-29 18:42:49 +01:00
|
|
|
mock_cache_instance.create.assert_called_once()
|
|
|
|
|
mock_cache_instance.get.assert_called_once()
|
|
|
|
|
|
|
|
|
|
|
2025-01-18 14:26:34 +01:00
|
|
|
def test_key_to_array_resampling(provider):
|
2024-12-29 18:42:49 +01:00
|
|
|
"""Test resampling of forecast data to NumPy array."""
|
2025-01-18 14:26:34 +01:00
|
|
|
provider.update_data(force_update=True)
|
|
|
|
|
array = provider.key_to_array(
|
2024-12-30 21:29:50 +01:00
|
|
|
key="elecprice_marketprice_wh",
|
2025-10-28 02:50:31 +01:00
|
|
|
start_datetime=provider.ems_start_datetime,
|
2025-01-18 14:26:34 +01:00
|
|
|
end_datetime=provider.end_datetime,
|
2024-12-29 18:42:49 +01:00
|
|
|
)
|
|
|
|
|
assert isinstance(array, np.ndarray)
|
2025-01-18 14:26:34 +01:00
|
|
|
assert len(array) == provider.total_hours
|
2024-12-29 18:42:49 +01:00
|
|
|
|
|
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
# ------------------------------------------------
|
|
|
|
|
# Development Akkudoktor
|
|
|
|
|
# ------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skip(reason="For development only")
|
2025-01-18 14:26:34 +01:00
|
|
|
def test_akkudoktor_development_forecast_data(provider):
|
2024-12-15 14:40:03 +01:00
|
|
|
"""Fetch data from real Akkudoktor server."""
|
|
|
|
|
# Preset, as this is usually done by update_data()
|
2025-10-28 02:50:31 +01:00
|
|
|
provider.ems_start_datetime = to_datetime("2024-10-26 00:00:00")
|
2024-12-15 14:40:03 +01:00
|
|
|
|
2025-01-18 14:26:34 +01:00
|
|
|
akkudoktor_data = provider._request_forecast()
|
2024-12-15 14:40:03 +01:00
|
|
|
|
2025-02-12 21:35:51 +01:00
|
|
|
with FILE_TESTDATA_ELECPRICEAKKUDOKTOR_1_JSON.open(
|
|
|
|
|
"w", encoding="utf-8", newline="\n"
|
|
|
|
|
) as f_out:
|
2024-12-15 14:40:03 +01:00
|
|
|
json.dump(akkudoktor_data, f_out, indent=4)
|