feat(pvforecast): local pvlib provider with measurement calibration

Add PVForecastAkkudoktorLocal, which runs the modelling chain inside EOS on
raw Open-Meteo irradiance instead of calling a forecast service: solar
position, horizon shading, plane transposition, incidence-angle modifier,
cell temperature, PVWatts DC and inverter AC.

It needs no API key and serves up to 16 days at 15-minute resolution from a
single hourly request, which is what keeps `optimization.tail_horizon_hours`
fed - services wrapping Open-Meteo cut the horizon much shorter. Several
Open-Meteo models can be listed in `weather_models` and are averaged per
variable at no extra request cost.

With `calibration_enabled` the provider fits itself against
`measurement.pv_production_emr_keys` over the past `calibration_days`: a
global scale factor plus optional per-solar-azimuth factors, each weighted by
modelled energy, shrunk toward the global factor by `calibration_prior_kwh`
and clamped to `[calibration_min_factor, calibration_max_factor]`. The
comparison runs on past intervals, where Open-Meteo serves analysed rather
than forecast weather, so it corrects the error of the PV model and not that
of the weather forecast.

Calibration is a scale factor on the output and never touches `userhorizon`,
`surface_tilt`, `surface_azimuth` or `peakpower`. The docs say so, and say
why a short window and a plant fault inside it are the two ways to end up
with a misleading factor.

Also add `Measurement.pv_production_total_kwh()` alongside the existing load
total, and `scripts/pvforecast_backtest.py`, which scores configuration
variants against the stored meter readings without waiting for new forecasts
to come true.
This commit is contained in:
Andreas
2026-09-06 18:21:20 +02:00
parent c0c9a1f669
commit f976335122
15 changed files with 1814 additions and 79 deletions
+84 -9
View File
@@ -8,7 +8,7 @@
"elevation": 291.0,
"hourly_units": {
"time": "iso8601",
"temperature_2m": "\u00b0C",
"temperature_2m": "°C",
"relative_humidity_2m": "%",
"precipitation": "mm",
"rain": "mm",
@@ -19,16 +19,17 @@
"pressure_msl": "hPa",
"surface_pressure": "hPa",
"wind_speed_10m": "km/h",
"wind_direction_10m": "\u00b0",
"wind_direction_10m": "°",
"wind_gusts_10m": "km/h",
"shortwave_radiation": "W/m\u00b2",
"direct_radiation": "W/m\u00b2",
"diffuse_radiation": "W/m\u00b2",
"dew_point_2m": "\u00b0C",
"apparent_temperature": "\u00b0C",
"shortwave_radiation": "W/m²",
"direct_radiation": "W/m²",
"diffuse_radiation": "W/m²",
"dew_point_2m": "°C",
"apparent_temperature": "°C",
"precipitation_probability": "%",
"visibility": "m",
"sunshine_duration": "s"
"sunshine_duration": "s",
"direct_normal_irradiance": "W/m²"
},
"hourly": {
"time": [
@@ -1658,6 +1659,80 @@
0.0,
0.0,
0.0
],
"direct_normal_irradiance": [
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
17.9,
62.9,
174.7,
669.0,
735.6,
765.5,
576.9,
433.0,
631.8,
465.1,
222.8,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
11.0,
44.4,
76.8,
394.1,
736.5,
774.1,
772.9,
738.2,
656.6,
502.2,
225.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
6.4,
54.1,
205.1,
490.6,
735.7,
767.7,
771.1,
731.0,
648.3,
498.4,
224.8,
0.0,
0.0,
0.0,
0.0,
0.0
]
}
}
}
+37 -37
View File
@@ -231,7 +231,7 @@
"weather_pressure": 10.26,
"weather_ozone": null,
"weather_ghi": 0.0,
"weather_dni": 0.0,
"weather_dni": 17.9,
"weather_dhi": 0.0
},
{
@@ -257,7 +257,7 @@
"weather_pressure": 10.265,
"weather_ozone": null,
"weather_ghi": 21.8,
"weather_dni": 1.2,
"weather_dni": 62.9,
"weather_dhi": 20.5
},
{
@@ -283,7 +283,7 @@
"weather_pressure": 10.269000000000002,
"weather_ozone": null,
"weather_ghi": 101.2,
"weather_dni": 13.8,
"weather_dni": 174.7,
"weather_dhi": 87.5
},
{
@@ -309,7 +309,7 @@
"weather_pressure": 10.262,
"weather_ozone": null,
"weather_ghi": 207.0,
"weather_dni": 61.5,
"weather_dni": 669.0,
"weather_dhi": 145.5
},
{
@@ -335,7 +335,7 @@
"weather_pressure": 10.257000000000001,
"weather_ozone": null,
"weather_ghi": 407.5,
"weather_dni": 304.2,
"weather_dni": 735.6,
"weather_dhi": 103.2
},
{
@@ -361,7 +361,7 @@
"weather_pressure": 10.252,
"weather_ozone": null,
"weather_ghi": 487.2,
"weather_dni": 382.5,
"weather_dni": 765.5,
"weather_dhi": 104.8
},
{
@@ -387,7 +387,7 @@
"weather_pressure": 10.247,
"weather_ozone": null,
"weather_ghi": 519.5,
"weather_dni": 416.0,
"weather_dni": 576.9,
"weather_dhi": 103.5
},
{
@@ -413,7 +413,7 @@
"weather_pressure": 10.235,
"weather_ozone": null,
"weather_ghi": 450.8,
"weather_dni": 302.0,
"weather_dni": 433.0,
"weather_dhi": 148.8
},
{
@@ -439,7 +439,7 @@
"weather_pressure": 10.23,
"weather_ozone": null,
"weather_ghi": 367.2,
"weather_dni": 199.8,
"weather_dni": 631.8,
"weather_dhi": 167.5
},
{
@@ -465,7 +465,7 @@
"weather_pressure": 10.228,
"weather_ozone": null,
"weather_ghi": 315.0,
"weather_dni": 228.5,
"weather_dni": 465.1,
"weather_dhi": 86.5
},
{
@@ -491,7 +491,7 @@
"weather_pressure": 10.227,
"weather_ozone": null,
"weather_ghi": 174.2,
"weather_dni": 107.5,
"weather_dni": 222.8,
"weather_dhi": 66.8
},
{
@@ -517,7 +517,7 @@
"weather_pressure": 10.23,
"weather_ozone": null,
"weather_ghi": 42.5,
"weather_dni": 17.8,
"weather_dni": 0.0,
"weather_dhi": 24.8
},
{
@@ -855,7 +855,7 @@
"weather_pressure": 10.278,
"weather_ozone": null,
"weather_ghi": 0.0,
"weather_dni": 0.0,
"weather_dni": 11.0,
"weather_dhi": 0.0
},
{
@@ -881,7 +881,7 @@
"weather_pressure": 10.287,
"weather_ozone": null,
"weather_ghi": 23.5,
"weather_dni": 0.8,
"weather_dni": 44.4,
"weather_dhi": 22.8
},
{
@@ -907,7 +907,7 @@
"weather_pressure": 10.288,
"weather_ozone": null,
"weather_ghi": 90.0,
"weather_dni": 10.0,
"weather_dni": 76.8,
"weather_dhi": 80.0
},
{
@@ -933,7 +933,7 @@
"weather_pressure": 10.286,
"weather_ozone": null,
"weather_ghi": 177.0,
"weather_dni": 27.5,
"weather_dni": 394.1,
"weather_dhi": 149.5
},
{
@@ -959,7 +959,7 @@
"weather_pressure": 10.279000000000002,
"weather_ozone": null,
"weather_ghi": 352.0,
"weather_dni": 181.5,
"weather_dni": 736.5,
"weather_dhi": 170.5
},
{
@@ -985,7 +985,7 @@
"weather_pressure": 10.272,
"weather_ozone": null,
"weather_ghi": 496.0,
"weather_dni": 387.2,
"weather_dni": 774.1,
"weather_dhi": 108.8
},
{
@@ -1011,7 +1011,7 @@
"weather_pressure": 10.267999999999999,
"weather_ozone": null,
"weather_ghi": 530.5,
"weather_dni": 425.0,
"weather_dni": 772.9,
"weather_dhi": 105.5
},
{
@@ -1037,7 +1037,7 @@
"weather_pressure": 10.265,
"weather_ozone": null,
"weather_ghi": 509.0,
"weather_dni": 408.8,
"weather_dni": 738.2,
"weather_dhi": 100.2
},
{
@@ -1063,7 +1063,7 @@
"weather_pressure": 10.261,
"weather_ozone": null,
"weather_ghi": 437.2,
"weather_dni": 344.5,
"weather_dni": 656.6,
"weather_dhi": 92.8
},
{
@@ -1089,7 +1089,7 @@
"weather_pressure": 10.257000000000001,
"weather_ozone": null,
"weather_ghi": 321.2,
"weather_dni": 240.8,
"weather_dni": 502.2,
"weather_dhi": 80.5
},
{
@@ -1115,7 +1115,7 @@
"weather_pressure": 10.257000000000001,
"weather_ozone": null,
"weather_ghi": 179.0,
"weather_dni": 118.5,
"weather_dni": 225.0,
"weather_dhi": 60.5
},
{
@@ -1141,7 +1141,7 @@
"weather_pressure": 10.26,
"weather_ozone": null,
"weather_ghi": 44.2,
"weather_dni": 19.0,
"weather_dni": 0.0,
"weather_dhi": 25.2
},
{
@@ -1479,7 +1479,7 @@
"weather_pressure": 10.290000000000001,
"weather_ozone": null,
"weather_ghi": 0.0,
"weather_dni": 0.0,
"weather_dni": 6.4,
"weather_dhi": 0.0
},
{
@@ -1505,7 +1505,7 @@
"weather_pressure": 10.293,
"weather_ozone": null,
"weather_ghi": 24.2,
"weather_dni": 0.5,
"weather_dni": 54.1,
"weather_dhi": 23.8
},
{
@@ -1531,7 +1531,7 @@
"weather_pressure": 10.295,
"weather_ozone": null,
"weather_ghi": 98.0,
"weather_dni": 12.5,
"weather_dni": 205.1,
"weather_dhi": 85.5
},
{
@@ -1557,7 +1557,7 @@
"weather_pressure": 10.294,
"weather_ozone": null,
"weather_ghi": 218.8,
"weather_dni": 74.6,
"weather_dni": 490.6,
"weather_dhi": 144.2
},
{
@@ -1583,7 +1583,7 @@
"weather_pressure": 10.288,
"weather_ozone": null,
"weather_ghi": 380.7,
"weather_dni": 228.8,
"weather_dni": 735.7,
"weather_dhi": 151.8
},
{
@@ -1609,7 +1609,7 @@
"weather_pressure": 10.278,
"weather_ozone": null,
"weather_ghi": 496.1,
"weather_dni": 391.0,
"weather_dni": 767.7,
"weather_dhi": 105.1
},
{
@@ -1635,7 +1635,7 @@
"weather_pressure": 10.267999999999999,
"weather_ozone": null,
"weather_ghi": 528.0,
"weather_dni": 425.8,
"weather_dni": 771.1,
"weather_dhi": 102.2
},
{
@@ -1661,7 +1661,7 @@
"weather_pressure": 10.263,
"weather_ozone": null,
"weather_ghi": 508.0,
"weather_dni": 412.0,
"weather_dni": 731.0,
"weather_dhi": 96.0
},
{
@@ -1687,7 +1687,7 @@
"weather_pressure": 10.261,
"weather_ozone": null,
"weather_ghi": 435.0,
"weather_dni": 345.0,
"weather_dni": 648.3,
"weather_dhi": 90.0
},
{
@@ -1713,7 +1713,7 @@
"weather_pressure": 10.258,
"weather_ozone": null,
"weather_ghi": 320.0,
"weather_dni": 241.0,
"weather_dni": 498.4,
"weather_dhi": 79.0
},
{
@@ -1739,7 +1739,7 @@
"weather_pressure": 10.255,
"weather_ozone": null,
"weather_ghi": 180.0,
"weather_dni": 120.0,
"weather_dni": 224.8,
"weather_dhi": 60.0
},
{
@@ -1765,7 +1765,7 @@
"weather_pressure": 10.255,
"weather_ozone": null,
"weather_ghi": 45.0,
"weather_dni": 20.0,
"weather_dni": 0.0,
"weather_dhi": 25.0
},
{
@@ -1956,4 +1956,4 @@
"keep_datetime": "2026-02-28 00:00:00+01:00",
"total_hours": 48,
"keep_hours": 48
}
}