feat(pvforecast): add calibrated local Akkudoktor backend

Port local PV modeling and outage calibration from feature commits f976335, 6dc58c3 and faed0fd by Andreas. Keep PVForecastAkkudoktor identity and remote default, adapt to async storage, and migrate legacy provider settings.
This commit is contained in:
Andreas
2026-09-17 13:53:54 +02:00
committed by Andreas
parent 75a65f4ff4
commit 6529c39df4
6 changed files with 2071 additions and 5 deletions
+18
View File
@@ -197,8 +197,26 @@ def migrate_config_data(config_data: Dict[str, Any]) -> "SettingsEOSDefaults":
skipped_paths = []
from akkudoktoreos.config.config import SettingsEOSDefaults
from akkudoktoreos.prediction.pvforecastakkudoktorlocal import (
PVForecastAkkudoktorLocalCommonSettings,
normalize_akkudoktor_settings,
)
# Normalize this provider before the generic field-by-field transfer. Validate
# coupled bounds together so a transient intermediate default cannot lose them.
config_data = dict(config_data)
pv_settings = normalize_akkudoktor_settings(config_data.get("pvforecast"))
if isinstance(pv_settings, dict):
config_data["pvforecast"] = pv_settings
new_config = SettingsEOSDefaults()
if isinstance(pv_settings, dict) and "akkudoktor" in pv_settings:
local_settings = PVForecastAkkudoktorLocalCommonSettings.model_validate(
pv_settings["akkudoktor"]
)
new_config.set_nested_value("pvforecast/akkudoktor", local_settings)
migrated_source_paths.add("pvforecast/akkudoktor")
mapped_count += 1
# 1) Apply explicit migration map
for old_path, mapping in MIGRATION_MAP.items():
@@ -7,6 +7,10 @@ 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.pvforecastabc import PVForecastProvider
from akkudoktoreos.prediction.pvforecastakkudoktorlocal import (
PVForecastAkkudoktorLocalCommonSettings,
normalize_akkudoktor_settings,
)
from akkudoktoreos.prediction.pvforecastforecastsolar import (
PVForecastForecastSolarCommonSettings,
)
@@ -202,6 +206,13 @@ class PVForecastCommonSettings(SettingsBaseModel):
},
)
akkudoktor: PVForecastAkkudoktorLocalCommonSettings = Field(
default_factory=PVForecastAkkudoktorLocalCommonSettings,
json_schema_extra={
"description": "Akkudoktor forecast backend and local calibration settings",
},
)
pvforecastimport: PVForecastImportCommonSettings = Field(
default_factory=PVForecastImportCommonSettings,
json_schema_extra={"description": "PV forecast import provider settings"},
@@ -300,6 +311,12 @@ class PVForecastCommonSettings(SettingsBaseModel):
return pvforecast_provider_ids()
# Validators
@model_validator(mode="before")
@classmethod
def migrate_local_akkudoktor_settings(cls, data: Any) -> Any:
"""Accept the feature branch's local backend configuration."""
return normalize_akkudoktor_settings(data)
@field_validator("provider", mode="after")
@classmethod
def validate_provider(cls, value: Optional[str]) -> Optional[str]:
@@ -87,10 +87,8 @@ from pydantic import Field, ValidationError, computed_field, field_validator
from akkudoktoreos.core.cache import cache_in_file
from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.prediction.pvforecast import PVForecastPlaneSetting
from akkudoktoreos.prediction.pvforecastabc import (
PVForecastDataRecord,
PVForecastProvider,
)
from akkudoktoreos.prediction.pvforecastabc import PVForecastDataRecord
from akkudoktoreos.prediction.pvforecastakkudoktorlocal import PVForecastAkkudoktorLocal
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime
@@ -188,7 +186,7 @@ class PVForecastAkkudoktorDataRecord(PVForecastDataRecord):
return self.pvforecast_ac_power
class PVForecastAkkudoktor(PVForecastProvider[PVForecastAkkudoktorDataRecord]):
class PVForecastAkkudoktor(PVForecastAkkudoktorLocal[PVForecastAkkudoktorDataRecord]):
"""Fetch and process PV forecast data from akkudoktor.net.
PVForecastAkkudoktor is a singleton-based class that retrieves weather forecast data
@@ -385,6 +383,10 @@ class PVForecastAkkudoktor(PVForecastProvider[PVForecastAkkudoktorDataRecord]):
# Get Akkudoktor PV Forecast data for the given configuration.
if force_update:
logger.info("[PVForecastAkkudoktor] force update.")
if self.config.pvforecast.akkudoktor.backend == "local":
await self._update_local_data(force_update=force_update)
return
akkudoktor_data = self._request_forecast(force_update=force_update) # type: ignore
# Timezone of the PV system
File diff suppressed because it is too large Load Diff