feat(prediction): add native pvnode.com PV forecast provider

Add PVForecastPVNode, a native 15-minute PV forecast provider for the
pvnode.com V2 API, giving operators another forecast source to choose from
alongside Akkudoktor, VRM and Import.

Two request modes, selected by configuration:
  * site_id set   -> GET /v2/forecast/{site_id}   (a saved, possibly calibrated
    site managed on pvnode.com — the operator enters site id + API key)
  * site_id empty -> POST /v2/forecast/inline      (geometry sent inline from the
    configured pvforecast.planes; no web-app setup required)

V2 response timestamps are site-local wall-clock accompanied by an IANA
timezone; they are resolved to absolute instants before EOS resamples them.
Nullable pv_power (e.g. at night) is treated as 0 W so the optimizer's linear
resampling does not interpolate phantom production across the night.

Registered in pvforecast.py (provider settings + id list) and prediction.py
(singleton, factory list, container union). Adds tests covering timezone
resolution, null handling, both request modes and HTTP-error propagation.
This commit is contained in:
Christin
2026-06-28 02:52:47 +00:00
parent 441a4639c3
commit 314e45cfaa
4 changed files with 396 additions and 1 deletions

View File

@@ -8,6 +8,7 @@ from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.coreabc import get_prediction
from akkudoktoreos.prediction.pvforecastabc import PVForecastProvider
from akkudoktoreos.prediction.pvforecastimport import PVForecastImportCommonSettings
from akkudoktoreos.prediction.pvforecastpvnode import PVForecastPVNodeCommonSettings
from akkudoktoreos.prediction.pvforecastvrm import PVForecastVrmCommonSettings
@@ -18,7 +19,7 @@ def pvforecast_provider_ids() -> list[str]:
except:
# Prediction may not be initialized
# Return at least provider used in example
return ["PVForecastAkkudoktor", "PVForecastImport", "PVForecastVrm"]
return ["PVForecastAkkudoktor", "PVForecastImport", "PVForecastVrm", "PVForecastPVNode"]
return [
provider.provider_id()
@@ -179,6 +180,10 @@ class PVForecastCommonProviderSettings(SettingsBaseModel):
default=None,
json_schema_extra={"description": "PVForecastVrm settings", "examples": [None]},
)
PVForecastPVNode: Optional[PVForecastPVNodeCommonSettings] = Field(
default=None,
json_schema_extra={"description": "PVForecastPVNode settings", "examples": [None]},
)
class PVForecastCommonSettings(SettingsBaseModel):