Files
EOS/tests/test_prediction.py
T
Bobby NoelteandClaude Opus 5.5 a0e57a18b7 fix(config): validate prediction providers before the prediction exists
The provider settings (weather, PV forecast, load, electricity price, feed-in
tariff, electricity fee) validate their provider id against the providers of
the prediction instance. While the configuration is created, that instance
does not exist yet, and the validation falls back to fixed lists. These were
incomplete: weather knew only 'WeatherImport', load lacked
'LoadAkkudoktorAdjusted'. A configuration file with e.g. weather provider
'OpenMeteo' then fails to load:

  Provider 'OpenMeteo' is not a valid weather provider: ['WeatherImport']

This is hidden today because ConfigEOS.documentation_mode() is always true
(_force_documentation_mode is a pydantic private attribute, whose class
attribute is a truthy descriptor), and get_prediction() then creates the
prediction instance on access. Once documentation mode is enabled only
explicitly, the configuration does not load.

Derive the provider ids from the provider classes instead
(prediction_provider_ids). They no longer depend on whether the prediction
instance exists, so the validation is the same before and after
initialization.

Additional changes
- Add the test fixture uncreated_config, which lets get_config(init=True)
  create a new configuration from the config file and restores the old one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 12:40:15 +01:00

243 lines
10 KiB
Python

import pytest
from pydantic import ValidationError
from akkudoktoreos.core.coreabc import get_prediction
from akkudoktoreos.prediction.elecfeefixed import ElecFeeFixed
from akkudoktoreos.prediction.elecfeeimport import ElecFeeImport
from akkudoktoreos.prediction.elecpriceakkudoktor import ElecPriceAkkudoktor
from akkudoktoreos.prediction.elecpriceenergycharts import ElecPriceEnergyCharts
from akkudoktoreos.prediction.elecpricefixed import ElecPriceFixed
from akkudoktoreos.prediction.elecpriceimport import ElecPriceImport
from akkudoktoreos.prediction.elecpricesmard import ElecPriceSMARD
from akkudoktoreos.prediction.elecpricetibber import ElecPriceTibber
from akkudoktoreos.prediction.feedintariffakkudoktor import FeedInTariffAkkudoktor
from akkudoktoreos.prediction.feedintariffdvhubonline import FeedInTariffDvhubOnline
from akkudoktoreos.prediction.feedintariffenergycharts import FeedInTariffEnergyCharts
from akkudoktoreos.prediction.feedintarifffixed import FeedInTariffFixed
from akkudoktoreos.prediction.feedintariffimport import FeedInTariffImport
from akkudoktoreos.prediction.feedintariffsmard import FeedInTariffSMARD
from akkudoktoreos.prediction.feedintarifftibber import FeedInTariffTibber
from akkudoktoreos.prediction.loadakkudoktor import (
LoadAkkudoktor,
LoadAkkudoktorAdjusted,
)
from akkudoktoreos.prediction.loadimport import LoadImport
from akkudoktoreos.prediction.loadvrm import LoadVrm
from akkudoktoreos.prediction.prediction import (
Prediction,
PredictionCommonSettings,
)
from akkudoktoreos.prediction.pvakkuadapt import (
PVForecastAdaptiveAkkudoktor,
)
from akkudoktoreos.prediction.pvforecastakkudoktor import PVForecastAkkudoktor
from akkudoktoreos.prediction.pvforecastforecastsolar import PVForecastForecastSolar
from akkudoktoreos.prediction.pvforecasthomeassistant import PVForecastHomeAssistant
from akkudoktoreos.prediction.pvforecastimport import PVForecastImport
from akkudoktoreos.prediction.pvforecastpvlib import PVForecastPVLib
from akkudoktoreos.prediction.pvforecastpvnode import PVForecastPVNode
from akkudoktoreos.prediction.pvforecastsolcast import PVForecastSolcast
from akkudoktoreos.prediction.pvforecastvrm import PVForecastVrm
from akkudoktoreos.prediction.weatherbrightsky import WeatherBrightSky
from akkudoktoreos.prediction.weatherclearoutside import WeatherClearOutside
from akkudoktoreos.prediction.weatherimport import WeatherImport
from akkudoktoreos.prediction.weatheropenmeteo import WeatherOpenMeteo
@pytest.fixture
def prediction():
"""All EOS predictions."""
return get_prediction()
@pytest.fixture
def forecast_providers():
"""Fixture for singleton forecast provider instances."""
return [
WeatherBrightSky(),
WeatherClearOutside(),
WeatherImport(),
WeatherOpenMeteo(),
ElecFeeFixed(),
ElecFeeImport(),
ElecPriceAkkudoktor(),
ElecPriceEnergyCharts(),
ElecPriceFixed(),
ElecPriceImport(),
ElecPriceSMARD(),
ElecPriceTibber(),
FeedInTariffAkkudoktor(),
FeedInTariffDvhubOnline(),
FeedInTariffEnergyCharts(),
FeedInTariffFixed(),
FeedInTariffImport(),
FeedInTariffSMARD(),
FeedInTariffTibber(),
LoadAkkudoktor(),
LoadAkkudoktorAdjusted(),
LoadImport(),
LoadVrm(),
PVForecastAkkudoktor(),
PVForecastAdaptiveAkkudoktor(),
PVForecastForecastSolar(),
PVForecastHomeAssistant(),
PVForecastImport(),
PVForecastPVLib(),
PVForecastPVNode(),
PVForecastSolcast(),
PVForecastVrm(),
]
@pytest.mark.parametrize(
"field_name, invalid_value, expected_error",
[
("hours", -1, "Input should be greater than or equal to 0"),
("historic_hours", -5, "Input should be greater than or equal to 0"),
],
)
def test_prediction_common_settings_invalid(field_name, invalid_value, expected_error, config_eos):
"""Test invalid settings for PredictionCommonSettings."""
valid_data = {
"hours": 48,
"historic_hours": 24,
}
assert PredictionCommonSettings(**valid_data) is not None
valid_data[field_name] = invalid_value
with pytest.raises(ValidationError, match=expected_error):
PredictionCommonSettings(**valid_data)
def test_initialization(prediction, forecast_providers):
"""Test that Prediction is initialized with the correct providers in sequence."""
assert isinstance(prediction, Prediction)
for idx, provider in enumerate(prediction.providers):
assert provider.provider_id() == forecast_providers[idx].provider_id()
def test_provider_sequence(prediction):
"""Test the provider sequence is maintained in the Prediction instance."""
assert isinstance(prediction.providers[0], WeatherBrightSky)
assert isinstance(prediction.providers[1], WeatherClearOutside)
assert isinstance(prediction.providers[2], WeatherImport)
assert isinstance(prediction.providers[3], WeatherOpenMeteo)
assert isinstance(prediction.providers[4], ElecFeeFixed)
assert isinstance(prediction.providers[5], ElecFeeImport)
assert isinstance(prediction.providers[6], ElecPriceAkkudoktor)
assert isinstance(prediction.providers[7], ElecPriceEnergyCharts)
assert isinstance(prediction.providers[8], ElecPriceFixed)
assert isinstance(prediction.providers[9], ElecPriceImport)
assert isinstance(prediction.providers[10], ElecPriceSMARD)
assert isinstance(prediction.providers[11], ElecPriceTibber)
assert isinstance(prediction.providers[12], FeedInTariffAkkudoktor)
assert isinstance(prediction.providers[13], FeedInTariffDvhubOnline)
assert isinstance(prediction.providers[14], FeedInTariffEnergyCharts)
assert isinstance(prediction.providers[15], FeedInTariffFixed)
assert isinstance(prediction.providers[16], FeedInTariffImport)
assert isinstance(prediction.providers[17], FeedInTariffSMARD)
assert isinstance(prediction.providers[18], FeedInTariffTibber)
assert isinstance(prediction.providers[19], LoadAkkudoktor)
assert isinstance(prediction.providers[20], LoadAkkudoktorAdjusted)
assert isinstance(prediction.providers[21], LoadImport)
assert isinstance(prediction.providers[22], LoadVrm)
assert isinstance(prediction.providers[23], PVForecastAkkudoktor)
assert isinstance(prediction.providers[24], PVForecastAdaptiveAkkudoktor)
assert isinstance(prediction.providers[25], PVForecastForecastSolar)
assert isinstance(prediction.providers[26], PVForecastHomeAssistant)
assert isinstance(prediction.providers[27], PVForecastImport)
assert isinstance(prediction.providers[28], PVForecastPVLib)
assert isinstance(prediction.providers[29], PVForecastPVNode)
assert isinstance(prediction.providers[30], PVForecastSolcast)
assert isinstance(prediction.providers[31], PVForecastVrm)
def test_provider_by_id(prediction, forecast_providers):
"""Test that provider_by_id method returns the correct provider."""
for provider in forecast_providers:
assert (
prediction.provider_by_id(provider.provider_id()).provider_id()
== provider.provider_id()
)
def test_prediction_repr(prediction):
"""Test that the Prediction instance's representation is correct."""
result = repr(prediction)
assert "Prediction([" in result
assert "ElecFeeFixed" in result
assert "ElecFeeImport" in result
assert "ElecPriceAkkudoktor" in result
assert "ElecPriceEnergyCharts" in result
assert "ElecPriceFixed" in result
assert "ElecPriceImport" in result
assert "ElecPriceSMARD" in result
assert "ElecPriceTibber" in result
assert "FeedInTariffAkkudoktor" in result
assert "FeedInTariffDvhubOnline" in result
assert "FeedInTariffEnergyCharts" in result
assert "FeedInTariffFixed" in result
assert "FeedInTariffImport" in result
assert "FeedInTariffSMARD" in result
assert "FeedInTariffTibber" in result
assert "LoadAkkudoktor" in result
assert "LoadAkkudoktorAdjusted" in result
assert "LoadImport" in result
assert "LoadVrm" in result
assert "PVForecastAkkudoktor" in result
assert "PVForecastForecastSolar" in result
assert "PVForecastHomeAssistant" in result
assert "PVForecastImport" in result
assert "PVForecastPVLib" in result
assert "PVForecastPVNode" in result
assert "PVForecastSolcast" in result
assert "PVForecastVrm" in result
assert "WeatherBrightSky" in result
assert "WeatherClearOutside" in result
assert "WeatherImport" in result
assert "WeatherOpenMeteo" in result
@pytest.mark.asyncio
async def test_empty_providers(prediction, forecast_providers):
"""Test behavior when Prediction does not have providers."""
# Clear all prediction providers from prediction
providers_bkup = prediction.providers.copy()
prediction.providers.clear()
assert prediction.providers == []
await prediction.update_data() # Should not raise an error even with no providers
# Cleanup after Test
prediction.providers = providers_bkup
@pytest.mark.parametrize(
"provider_type",
[
"WeatherProvider",
"PVForecastProvider",
"LoadProvider",
"ElecPriceProvider",
"FeedInTariffProvider",
"ElecFeeProvider",
],
)
def test_provider_ids_without_prediction_instance(prediction, provider_type):
"""The ids from the provider classes match the providers of the prediction."""
import importlib
from akkudoktoreos.prediction.prediction import prediction_provider_ids
module_name = {
"WeatherProvider": "weatherabc",
"PVForecastProvider": "pvforecastabc",
"LoadProvider": "loadabc",
"ElecPriceProvider": "elecpriceabc",
"FeedInTariffProvider": "feedintariffabc",
"ElecFeeProvider": "elecfeeabc",
}[provider_type]
base = getattr(importlib.import_module(f"akkudoktoreos.prediction.{module_name}"), provider_type)
expected = {p.provider_id() for p in prediction.providers if isinstance(p, base)}
assert expected
assert set(prediction_provider_ids(base)) == expected