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>
This commit is contained in:
Bobby Noelte
2026-11-09 12:40:15 +01:00
co-authored by Claude Opus 5.5
parent 4475774c06
commit a0e57a18b7
10 changed files with 132 additions and 98 deletions
+4 -14
View File
@@ -3,7 +3,6 @@ from typing import Optional
from pydantic import Field, computed_field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.coreabc import get_prediction
from akkudoktoreos.prediction.elecfeeabc import ElecFeeProvider
from akkudoktoreos.prediction.elecfeefixed import ElecFeeFixedCommonSettings
from akkudoktoreos.prediction.elecfeeimport import ElecFeeImportCommonSettings
@@ -11,20 +10,11 @@ from akkudoktoreos.prediction.elecfeeimport import ElecFeeImportCommonSettings
def elecfee_provider_ids() -> list[str]:
"""Valid elecfee provider ids."""
try:
prediction_eos = get_prediction()
except Exception:
# Prediction may not be initialized. Return static built-in provider ids.
return [
"ElecFeeFixed",
"ElecFeeImport",
]
# From the provider classes: valid also while the configuration is created,
# before the prediction instance exists.
from akkudoktoreos.prediction.prediction import prediction_provider_ids
return [
provider.provider_id()
for provider in prediction_eos.providers
if isinstance(provider, ElecFeeProvider)
]
return prediction_provider_ids(ElecFeeProvider)
class ElecFeeCommonSettings(SettingsBaseModel):
+4 -18
View File
@@ -3,7 +3,6 @@ from typing import Optional
from pydantic import Field, computed_field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.coreabc import get_prediction
from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider
from akkudoktoreos.prediction.elecpriceakkudoktor import (
ElecPriceAkkudoktorCommonSettings,
@@ -19,24 +18,11 @@ from akkudoktoreos.prediction.elecpricetibber import ElecPriceTibberCommonSettin
def elecprice_provider_ids() -> list[str]:
"""Valid elecprice provider ids."""
try:
prediction_eos = get_prediction()
except Exception:
# Prediction may not be initialized. Return static built-in provider ids.
return [
"ElecPriceAkkudoktor",
"ElecPriceEnergyCharts",
"ElecPriceFixed",
"ElecPriceSMARD",
"ElecPriceImport",
"ElecPriceTibber",
]
# From the provider classes: valid also while the configuration is created,
# before the prediction instance exists.
from akkudoktoreos.prediction.prediction import prediction_provider_ids
return [
provider.provider_id()
for provider in prediction_eos.providers
if isinstance(provider, ElecPriceProvider)
]
return prediction_provider_ids(ElecPriceProvider)
class ElecPriceCommonSettings(SettingsBaseModel):
+4 -19
View File
@@ -3,7 +3,6 @@ from typing import Optional
from pydantic import Field, computed_field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.coreabc import get_prediction
from akkudoktoreos.prediction.feedintariffabc import FeedInTariffProvider
from akkudoktoreos.prediction.feedintariffdvhubonline import (
FeedInTariffDvhubOnlineCommonSettings,
@@ -18,25 +17,11 @@ from akkudoktoreos.prediction.feedintariffsmard import FeedInTariffSMARDCommonSe
def feedintariff_provider_ids() -> list[str]:
"""Valid feedintariff provider ids."""
try:
prediction_eos = get_prediction()
except Exception:
# Prediction may not be initialized. Return static built-in provider ids.
return [
"FeedInTariffAkkudoktor",
"FeedInTariffDvhubOnline",
"FeedInTariffEnergyCharts",
"FeedInTariffFixed",
"FeedInTariffImport",
"FeedInTariffSMARD",
"FeedInTariffTibber",
]
# From the provider classes: valid also while the configuration is created,
# before the prediction instance exists.
from akkudoktoreos.prediction.prediction import prediction_provider_ids
return [
provider.provider_id()
for provider in prediction_eos.providers
if isinstance(provider, FeedInTariffProvider)
]
return prediction_provider_ids(FeedInTariffProvider)
class FeedInTariffCommonSettings(SettingsBaseModel):
+4 -12
View File
@@ -5,7 +5,6 @@ from typing import Optional
from pydantic import Field, computed_field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.coreabc import get_prediction
from akkudoktoreos.prediction.loadabc import LoadProvider
from akkudoktoreos.prediction.loadakkudoktor import LoadAkkudoktorCommonSettings
from akkudoktoreos.prediction.loadimport import LoadImportCommonSettings
@@ -14,18 +13,11 @@ from akkudoktoreos.prediction.loadvrm import LoadVrmCommonSettings
def load_providers() -> list[str]:
"""Valid load provider ids."""
try:
prediction_eos = get_prediction()
except Exception:
# Prediction may not be initialized
# Return at least provider used in example
return ["LoadAkkudoktor", "LoadVrm", "LoadImport"]
# From the provider classes: valid also while the configuration is created,
# before the prediction instance exists.
from akkudoktoreos.prediction.prediction import prediction_provider_ids
return [
provider.provider_id()
for provider in prediction_eos.providers
if isinstance(provider, LoadProvider)
]
return prediction_provider_ids(LoadProvider)
class LoadCommonSettings(SettingsBaseModel):
+21 -1
View File
@@ -26,7 +26,7 @@ Attributes:
weather_clearoutside (WeatherClearOutside): Weather forecast provider using ClearOutside.
"""
from typing import Optional, Union
from typing import Optional, Union, get_args
from pydantic import Field
@@ -159,6 +159,26 @@ PredictionProviderType = Union[
]
def prediction_provider_ids(provider_type: type) -> list[str]:
"""Return the ids of all prediction providers of a kind.
Derived from the provider classes, so it does not need the prediction
instance. The configuration validates its provider ids with it while it is
created, before the prediction instance exists.
Args:
provider_type: Provider base class of the kind, e.g. ``WeatherProvider``.
Returns:
The provider ids of all provider classes derived from ``provider_type``.
"""
return [
provider_class.provider_id()
for provider_class in get_args(PredictionProviderType)
if issubclass(provider_class, provider_type)
]
def prediction_providers() -> list[PredictionProviderType]:
"""Return list of prediction providers.
+4 -22
View File
@@ -5,7 +5,6 @@ from typing import Any, List, Optional, Self
from pydantic import Field, computed_field, field_validator, model_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.coreabc import get_prediction
from akkudoktoreos.prediction.pvakkuadapt import (
PVForecastAdaptiveAkkudoktorCommonSettings,
)
@@ -29,28 +28,11 @@ from akkudoktoreos.prediction.pvforecastvrm import PVForecastVrmCommonSettings
def pvforecast_provider_ids() -> list[str]:
"""Valid PV forecast providers."""
try:
prediction_eos = get_prediction()
except Exception:
# Prediction may not be initialized
# Return at least provider used in example
return [
"PVForecastAdaptiveAkkudoktor",
"PVForecastAkkudoktor",
"PVForecastForecastSolar",
"PVForecastHomeAssistant",
"PVForecastImport",
"PVForecastPVLib",
"PVForecastPVNode",
"PVForecastSolcast",
"PVForecastVrm",
]
# From the provider classes: valid also while the configuration is created,
# before the prediction instance exists.
from akkudoktoreos.prediction.prediction import prediction_provider_ids
return [
provider.provider_id()
for provider in prediction_eos.providers
if isinstance(provider, PVForecastProvider)
]
return prediction_provider_ids(PVForecastProvider)
class PVForecastPlaneSetting(SettingsBaseModel):
+4 -12
View File
@@ -5,25 +5,17 @@ from typing import Optional
from pydantic import Field, computed_field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.coreabc import get_prediction
from akkudoktoreos.prediction.weatherabc import WeatherProvider
from akkudoktoreos.prediction.weatherimport import WeatherImportCommonSettings
def weather_provider_ids() -> list[str]:
"""Valid weather provider ids."""
try:
prediction_eos = get_prediction()
except Exception:
# Prediction may not be initialized
# Return at least provider used in example
return ["WeatherImport"]
# From the provider classes: valid also while the configuration is created,
# before the prediction instance exists.
from akkudoktoreos.prediction.prediction import prediction_provider_ids
return [
provider.provider_id()
for provider in prediction_eos.providers
if isinstance(provider, WeatherProvider)
]
return prediction_provider_ids(WeatherProvider)
class WeatherCommonSettings(SettingsBaseModel):