mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-11-13 09:13:30 +01:00
feat(pvforecast): add adaptive per-plane PV learning and diagnostics (#1422)
PV arrays with different orientations and curtailment need separate plant calibration and weather correction. This opt-in provider publishes normal EOS AC/DC forecast keys at 15-minute resolution. - Preserve daily per-plane energy/shape calibration and validated short-term local/trend selection. Curtailment-sensitive arrays require a fresh release flag or an explicitly configured learning guard. - Add switchable empirical solar-angle response relative to separate eligible reference arrays, removing their common weather error. This learns angular residuals, not physical angles or a measured geometric horizon. - Archive actually received weather model inputs, first local receipt and frozen per-member PV candidates. Returned past weather is excluded; unknown source issuance remains explicit. Learn model weights and forecast-regime bias separately at 2–6, 6–24 and 24–48 hours. - New long-horizon candidates remain in shadow until their genuinely issued outputs improve equal-day MAE by the configured margin, win on at least 60% of validation days and pass a matched-support energy-bias guard. Insufficient evidence retains calibration. Daily yields still require complete local days, including DST. - Expose fitted parameters, independent training/validation day counts, errors, selected methods and persistence in the existing status API, with additional normal prediction keys for Grafana. Retain physical DC through angular/weather corrections an enforce individual inverter AC limits. Co-authored-by: Normann <github@koldrack.com> Co-authored-by: Christin <chrissi@von-perbandt.de> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: YMT <yn276364@dal.ca> Co-authored-by: sarankumar1325 <120080287+sarankumar1325@users.noreply.github.com>
This commit is contained in:
co-authored by
Normann
Christin
Claude Opus 5.5
YMT
sarankumar1325
parent
298d46a527
commit
f804d48072
@@ -7,6 +7,7 @@
|
||||
|
||||
| Name | Environment Variable | Type | Read-Only | Default | Description |
|
||||
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
|
||||
| adaptive | `EOS_PVFORECAST__ADAPTIVE` | `PVForecastAdaptiveAkkudoktorCommonSettings` | `rw` | `required` | Adaptive Akkudoktor: configure string measurements and independent reference flags on the planes. |
|
||||
| akkudoktor | `EOS_PVFORECAST__AKKUDOKTOR` | `PVForecastAkkudoktorLocalCommonSettings` | `rw` | `required` | Akkudoktor forecast backend and local calibration settings |
|
||||
| forecastsolar | `EOS_PVFORECAST__FORECASTSOLAR` | `PVForecastForecastSolarCommonSettings` | `rw` | `required` | ForecastSolar provider settings |
|
||||
| homeassistant | `EOS_PVFORECAST__HOMEASSISTANT` | `PVForecastHomeAssistantCommonSettings` | `rw` | `required` | Home Assistant provider settings |
|
||||
@@ -61,6 +62,56 @@
|
||||
"calibration_min_factor": 0.5,
|
||||
"calibration_max_factor": 1.5
|
||||
},
|
||||
"adaptive": {
|
||||
"backend": "local",
|
||||
"resolution_minutes": 15,
|
||||
"forecast_days": null,
|
||||
"past_days": null,
|
||||
"weather_models": [
|
||||
"best_match",
|
||||
"icon_d2"
|
||||
],
|
||||
"transposition_model": "perez",
|
||||
"albedo": 0.25,
|
||||
"inverter_efficiency": 0.96,
|
||||
"temperature_coefficient": -0.36,
|
||||
"apply_iam": true,
|
||||
"shift_to_interval_start": true,
|
||||
"calibration_enabled": false,
|
||||
"calibration_days": 30,
|
||||
"calibration_reference_days": 30,
|
||||
"calibration_outage_filter_enabled": true,
|
||||
"calibration_outage_threshold": 0.55,
|
||||
"calibration_min_healthy_days": 3,
|
||||
"calibration_azimuth_bin_degrees": 45,
|
||||
"calibration_prior_kwh": 5.0,
|
||||
"calibration_min_factor": 0.5,
|
||||
"calibration_max_factor": 1.5,
|
||||
"regional_weather_model": "icon_d2",
|
||||
"regional_horizon_hours": 6,
|
||||
"learning_days": 21,
|
||||
"min_training_days": 5,
|
||||
"max_ac_power_w": 0,
|
||||
"nominal_total_peakpower_kw": null,
|
||||
"diagnostics_days": 7,
|
||||
"archive_retention_days": 60,
|
||||
"nowcast_enabled": true,
|
||||
"trend_enabled": true,
|
||||
"solar_response_enabled": true,
|
||||
"weather_response_enabled": true,
|
||||
"correction_mode": "auto",
|
||||
"validation_days": 7,
|
||||
"min_validation_days": 3,
|
||||
"min_validation_samples": 32,
|
||||
"min_validation_improvement": 0.02,
|
||||
"training_delay_minutes": 15,
|
||||
"total_measurement_key": null,
|
||||
"total_curtailment_sensitive": false,
|
||||
"total_curtailment_key": null,
|
||||
"total_learning_guard_key": null,
|
||||
"total_learning_guard_max": null,
|
||||
"total_learning_guard_max_gap_seconds": 600
|
||||
},
|
||||
"pvforecastimport": {
|
||||
"import_file_path": null,
|
||||
"import_json": null
|
||||
@@ -93,6 +144,16 @@
|
||||
},
|
||||
"planes": [
|
||||
{
|
||||
"name": null,
|
||||
"measurement_key": null,
|
||||
"nowcast_reference": false,
|
||||
"curtailment_sensitive": false,
|
||||
"parameters_estimated": false,
|
||||
"correction_group": null,
|
||||
"curtailment_key": null,
|
||||
"learning_guard_key": null,
|
||||
"learning_guard_max": null,
|
||||
"learning_guard_max_gap_seconds": 600,
|
||||
"surface_tilt": 10.0,
|
||||
"surface_azimuth": 180.0,
|
||||
"userhorizon": [
|
||||
@@ -115,6 +176,16 @@
|
||||
"strings_per_inverter": 2
|
||||
},
|
||||
{
|
||||
"name": null,
|
||||
"measurement_key": null,
|
||||
"nowcast_reference": false,
|
||||
"curtailment_sensitive": false,
|
||||
"parameters_estimated": false,
|
||||
"correction_group": null,
|
||||
"curtailment_key": null,
|
||||
"learning_guard_key": null,
|
||||
"learning_guard_max": null,
|
||||
"learning_guard_max_gap_seconds": 600,
|
||||
"surface_tilt": 20.0,
|
||||
"surface_azimuth": 90.0,
|
||||
"userhorizon": [
|
||||
@@ -177,6 +248,56 @@
|
||||
"calibration_min_factor": 0.5,
|
||||
"calibration_max_factor": 1.5
|
||||
},
|
||||
"adaptive": {
|
||||
"backend": "local",
|
||||
"resolution_minutes": 15,
|
||||
"forecast_days": null,
|
||||
"past_days": null,
|
||||
"weather_models": [
|
||||
"best_match",
|
||||
"icon_d2"
|
||||
],
|
||||
"transposition_model": "perez",
|
||||
"albedo": 0.25,
|
||||
"inverter_efficiency": 0.96,
|
||||
"temperature_coefficient": -0.36,
|
||||
"apply_iam": true,
|
||||
"shift_to_interval_start": true,
|
||||
"calibration_enabled": false,
|
||||
"calibration_days": 30,
|
||||
"calibration_reference_days": 30,
|
||||
"calibration_outage_filter_enabled": true,
|
||||
"calibration_outage_threshold": 0.55,
|
||||
"calibration_min_healthy_days": 3,
|
||||
"calibration_azimuth_bin_degrees": 45,
|
||||
"calibration_prior_kwh": 5.0,
|
||||
"calibration_min_factor": 0.5,
|
||||
"calibration_max_factor": 1.5,
|
||||
"regional_weather_model": "icon_d2",
|
||||
"regional_horizon_hours": 6,
|
||||
"learning_days": 21,
|
||||
"min_training_days": 5,
|
||||
"max_ac_power_w": 0,
|
||||
"nominal_total_peakpower_kw": null,
|
||||
"diagnostics_days": 7,
|
||||
"archive_retention_days": 60,
|
||||
"nowcast_enabled": true,
|
||||
"trend_enabled": true,
|
||||
"solar_response_enabled": true,
|
||||
"weather_response_enabled": true,
|
||||
"correction_mode": "auto",
|
||||
"validation_days": 7,
|
||||
"min_validation_days": 3,
|
||||
"min_validation_samples": 32,
|
||||
"min_validation_improvement": 0.02,
|
||||
"training_delay_minutes": 15,
|
||||
"total_measurement_key": null,
|
||||
"total_curtailment_sensitive": false,
|
||||
"total_curtailment_key": null,
|
||||
"total_learning_guard_key": null,
|
||||
"total_learning_guard_max": null,
|
||||
"total_learning_guard_max_gap_seconds": 600
|
||||
},
|
||||
"pvforecastimport": {
|
||||
"import_file_path": null,
|
||||
"import_json": null
|
||||
@@ -209,6 +330,16 @@
|
||||
},
|
||||
"planes": [
|
||||
{
|
||||
"name": null,
|
||||
"measurement_key": null,
|
||||
"nowcast_reference": false,
|
||||
"curtailment_sensitive": false,
|
||||
"parameters_estimated": false,
|
||||
"correction_group": null,
|
||||
"curtailment_key": null,
|
||||
"learning_guard_key": null,
|
||||
"learning_guard_max": null,
|
||||
"learning_guard_max_gap_seconds": 600,
|
||||
"surface_tilt": 10.0,
|
||||
"surface_azimuth": 180.0,
|
||||
"userhorizon": [
|
||||
@@ -231,6 +362,16 @@
|
||||
"strings_per_inverter": 2
|
||||
},
|
||||
{
|
||||
"name": null,
|
||||
"measurement_key": null,
|
||||
"nowcast_reference": false,
|
||||
"curtailment_sensitive": false,
|
||||
"parameters_estimated": false,
|
||||
"correction_group": null,
|
||||
"curtailment_key": null,
|
||||
"learning_guard_key": null,
|
||||
"learning_guard_max": null,
|
||||
"learning_guard_max_gap_seconds": 600,
|
||||
"surface_tilt": 20.0,
|
||||
"surface_azimuth": 90.0,
|
||||
"userhorizon": [
|
||||
@@ -256,6 +397,7 @@
|
||||
"max_planes": 1,
|
||||
"providers": [
|
||||
"PVForecastAkkudoktor",
|
||||
"PVForecastAdaptiveAkkudoktor",
|
||||
"PVForecastForecastSolar",
|
||||
"PVForecastHomeAssistant",
|
||||
"PVForecastImport",
|
||||
@@ -459,14 +601,24 @@
|
||||
| Name | Type | Read-Only | Default | Description |
|
||||
| ---- | ---- | --------- | ------- | ----------- |
|
||||
| albedo | `Optional[float]` | `rw` | `0.2` | Proportion of the light hitting the ground that it reflects back. |
|
||||
| correction_group | `Optional[str]` | `rw` | `None` | Optional group of planes sharing short-term validation. Unset independent references share one group; other planes are separate. |
|
||||
| curtailment_key | `Optional[str]` | `rw` | `None` | Optional measured flag: 0 explicitly unrestricted, 1 curtailed. Missing or stale flags never authorize learning a curtailment-sensitive plane. |
|
||||
| curtailment_sensitive | `bool` | `rw` | `False` | Battery-full curtailment possible; exclude from unrestricted weather learning. |
|
||||
| inverter_model | `Optional[str]` | `rw` | `None` | Model of the inverter of this plane. |
|
||||
| inverter_paco | `Optional[int]` | `rw` | `None` | AC power rating of the inverter [W]. |
|
||||
| learning_guard_key | `Optional[str]` | `rw` | `None` | Optional site-specific positive learning guard, e.g. battery SoC, as an alternative to a binary curtailment flag. |
|
||||
| learning_guard_max | `Optional[float]` | `rw` | `None` | Learn only while every supported guard value is below this bound. This is an explicit site assumption, not automatic curtailment detection. |
|
||||
| learning_guard_max_gap_seconds | `int` | `rw` | `600` | None |
|
||||
| loss | `Optional[float]` | `rw` | `14.0` | Sum of PV system losses in percent |
|
||||
| measurement_key | `Optional[str]` | `rw` | `None` | Matching EOS power measurement key for adaptive learning. |
|
||||
| module_model | `Optional[str]` | `rw` | `None` | Model of the PV modules of this plane. |
|
||||
| modules_per_string | `Optional[int]` | `rw` | `None` | Number of the PV modules of the strings of this plane. |
|
||||
| mountingplace | `Optional[str]` | `rw` | `building` | Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated. |
|
||||
| name | `Optional[str]` | `rw` | `None` | Readable PV string name. |
|
||||
| nowcast_reference | `bool` | `rw` | `False` | Use this independent array as a local weather reference. |
|
||||
| optimal_surface_tilt | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt angle. Ignored for two-axis tracking. |
|
||||
| optimalangles | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking. |
|
||||
| parameters_estimated | `bool` | `rw` | `False` | Geometry and peak power are effective fitted model estimates, not verified nameplate data. |
|
||||
| peakpower | `Optional[float]` | `rw` | `None` | Nominal power of PV system in kW. |
|
||||
| pvtechchoice | `Optional[str]` | `rw` | `crystSi` | PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'. |
|
||||
| strings_per_inverter | `Optional[int]` | `rw` | `None` | Number of the strings of the inverter of this plane. |
|
||||
@@ -487,6 +639,16 @@
|
||||
"pvforecast": {
|
||||
"planes": [
|
||||
{
|
||||
"name": null,
|
||||
"measurement_key": null,
|
||||
"nowcast_reference": false,
|
||||
"curtailment_sensitive": false,
|
||||
"parameters_estimated": false,
|
||||
"correction_group": null,
|
||||
"curtailment_key": null,
|
||||
"learning_guard_key": null,
|
||||
"learning_guard_max": null,
|
||||
"learning_guard_max_gap_seconds": 600,
|
||||
"surface_tilt": 10.0,
|
||||
"surface_azimuth": 180.0,
|
||||
"userhorizon": [
|
||||
@@ -654,3 +816,123 @@
|
||||
}
|
||||
```
|
||||
<!-- pyml enable line-length -->
|
||||
|
||||
### Automatic defaults; configure measurements and reference flags on each plane
|
||||
|
||||
<!-- pyml disable line-length -->
|
||||
:::{table} pvforecast::adaptive
|
||||
:widths: 10 10 5 5 30
|
||||
:align: left
|
||||
|
||||
| Name | Type | Read-Only | Default | Description |
|
||||
| ---- | ---- | --------- | ------- | ----------- |
|
||||
| albedo | `float` | `rw` | `0.25` | Ground albedo used for planes that do not set their own. |
|
||||
| apply_iam | `bool` | `rw` | `True` | Apply the ASHRAE incidence-angle modifier to the beam component. |
|
||||
| archive_retention_days | `int` | `rw` | `60` | None |
|
||||
| backend | `Literal['remote', 'local']` | `rw` | `local` | None |
|
||||
| calibration_azimuth_bin_degrees | `int` | `rw` | `45` | Width of the solar-azimuth bins for the correction. 0 fits a single global factor only. |
|
||||
| calibration_days | `int` | `rw` | `30` | Length of the measurement window used to fit the correction. |
|
||||
| calibration_enabled | `bool` | `rw` | `False` | None |
|
||||
| calibration_max_factor | `float` | `rw` | `1.5` | Upper clamp on any fitted correction factor. |
|
||||
| calibration_min_factor | `float` | `rw` | `0.5` | Lower clamp on any fitted correction factor. |
|
||||
| calibration_min_healthy_days | `int` | `rw` | `3` | Minimum number of healthy days used for a fit. Older healthy days from the reference window are added when the recent window contains fewer. |
|
||||
| calibration_outage_filter_enabled | `bool` | `rw` | `True` | Exclude days whose measured production is far below the recent healthy plant level. This prevents inverter, battery and curtailment events from being learned as permanent PV model losses. |
|
||||
| calibration_outage_threshold | `float` | `rw` | `0.55` | A day is treated as unavailable when its measured/modelled energy ratio is below this fraction of the robust healthy reference ratio. |
|
||||
| calibration_prior_kwh | `float` | `rw` | `5.0` | Shrinkage strength: a bin needs this much modelled energy before its own factor outweighs the global one. Higher is more conservative. |
|
||||
| calibration_reference_days | `int` | `rw` | `30` | Lookback used to distinguish healthy production from outages or curtailment. If the calibration window contains too few healthy days, the most recent healthy days from this reference window are used. |
|
||||
| correction_mode | `Literal['auto', 'calibrated', 'local', 'trend']` | `rw` | `auto` | Choose validated short-term candidates per group/lead, or explicitly fix a correction stage. |
|
||||
| diagnostics_days | `int` | `rw` | `7` | None |
|
||||
| forecast_days | `Optional[int]` | `rw` | `None` | Forecast horizon in days (1-16). Leave empty to derive it from `prediction.hours`, which is what keeps the optimizer's tail horizon fed. |
|
||||
| inverter_efficiency | `float` | `rw` | `0.96` | Nominal inverter efficiency (PVWatts eta_inv_nom). |
|
||||
| learning_days | `int` | `rw` | `21` | None |
|
||||
| max_ac_power_w | `float` | `rw` | `0` | None |
|
||||
| min_training_days | `int` | `rw` | `5` | None |
|
||||
| min_validation_days | `int` | `rw` | `3` | None |
|
||||
| min_validation_improvement | `float` | `rw` | `0.02` | None |
|
||||
| min_validation_samples | `int` | `rw` | `32` | None |
|
||||
| nominal_total_peakpower_kw | `Optional[float]` | `rw` | `None` | Known installed nameplate total; fitted plane powers may be effective estimates. |
|
||||
| nowcast_enabled | `bool` | `rw` | `True` | None |
|
||||
| past_days | `Optional[int]` | `rw` | `None` | Days of past data to request (0-92). Leave empty to derive it from `prediction.historic_hours`. |
|
||||
| regional_horizon_hours | `float` | `rw` | `6` | None |
|
||||
| regional_weather_model | `Optional[str]` | `rw` | `icon_d2` | None |
|
||||
| resolution_minutes | `int` | `rw` | `15` | Forecast resolution in minutes. 15 requests Open-Meteo's `minutely_15` block (natively resolved over Central Europe and North America, interpolated from hourly elsewhere); 60 requests the `hourly` block. |
|
||||
| shift_to_interval_start | `bool` | `rw` | `True` | Open-Meteo stamps an interval mean with the interval END. EOS labels an interval by its START, so records are shifted back by one interval. Disable only to compare like-for-like against a provider that does not. |
|
||||
| solar_response_enabled | `bool` | `rw` | `True` | Learn empirical angular response relative to separate, eligible reference arrays; activate only after issued-candidate validation. |
|
||||
| temperature_coefficient | `float` | `rw` | `-0.36` | Module power temperature coefficient in %/degC (negative). Matches the `cellCoEff` the akkudoktor.net forecast uses. |
|
||||
| total_curtailment_key | `Optional[str]` | `rw` | `None` | None |
|
||||
| total_curtailment_sensitive | `bool` | `rw` | `False` | None |
|
||||
| total_learning_guard_key | `Optional[str]` | `rw` | `None` | None |
|
||||
| total_learning_guard_max | `Optional[float]` | `rw` | `None` | None |
|
||||
| total_learning_guard_max_gap_seconds | `int` | `rw` | `600` | None |
|
||||
| total_measurement_key | `Optional[str]` | `rw` | `None` | Optional whole-plant power channel when individual plane measurements are unavailable. |
|
||||
| training_delay_minutes | `int` | `rw` | `15` | None |
|
||||
| transposition_model | `str` | `rw` | `perez` | pvlib sky-diffuse transposition model: isotropic, klucher, haydavies, reindl, king or perez. |
|
||||
| trend_enabled | `bool` | `rw` | `True` | None |
|
||||
| validation_days | `int` | `rw` | `7` | None |
|
||||
| weather_models | `list[str]` | `rw` | `['best_match']` | Open-Meteo weather models to request. Listing more than one turns the input into a poor-man's ensemble: the members are averaged per variable, which is the cheapest reliable way to cut irradiance forecast error. Costs no extra API calls. |
|
||||
| weather_response_enabled | `bool` | `rw` | `True` | Archive received model weather and learn horizon/regime corrections; activate only after issued-candidate validation. |
|
||||
:::
|
||||
<!-- pyml enable line-length -->
|
||||
|
||||
<!-- pyml disable no-emphasis-as-heading -->
|
||||
**Example Input/Output**
|
||||
<!-- pyml enable no-emphasis-as-heading -->
|
||||
|
||||
<!-- pyml disable line-length -->
|
||||
```json
|
||||
{
|
||||
"pvforecast": {
|
||||
"adaptive": {
|
||||
"backend": "local",
|
||||
"resolution_minutes": 15,
|
||||
"forecast_days": null,
|
||||
"past_days": null,
|
||||
"weather_models": [
|
||||
"best_match",
|
||||
"icon_d2"
|
||||
],
|
||||
"transposition_model": "perez",
|
||||
"albedo": 0.25,
|
||||
"inverter_efficiency": 0.96,
|
||||
"temperature_coefficient": -0.36,
|
||||
"apply_iam": true,
|
||||
"shift_to_interval_start": true,
|
||||
"calibration_enabled": false,
|
||||
"calibration_days": 30,
|
||||
"calibration_reference_days": 30,
|
||||
"calibration_outage_filter_enabled": true,
|
||||
"calibration_outage_threshold": 0.55,
|
||||
"calibration_min_healthy_days": 3,
|
||||
"calibration_azimuth_bin_degrees": 45,
|
||||
"calibration_prior_kwh": 5.0,
|
||||
"calibration_min_factor": 0.5,
|
||||
"calibration_max_factor": 1.5,
|
||||
"regional_weather_model": "icon_d2",
|
||||
"regional_horizon_hours": 6.0,
|
||||
"learning_days": 21,
|
||||
"min_training_days": 5,
|
||||
"max_ac_power_w": 0.0,
|
||||
"nominal_total_peakpower_kw": null,
|
||||
"diagnostics_days": 7,
|
||||
"archive_retention_days": 60,
|
||||
"nowcast_enabled": true,
|
||||
"trend_enabled": true,
|
||||
"solar_response_enabled": true,
|
||||
"weather_response_enabled": true,
|
||||
"correction_mode": "auto",
|
||||
"validation_days": 7,
|
||||
"min_validation_days": 3,
|
||||
"min_validation_samples": 32,
|
||||
"min_validation_improvement": 0.02,
|
||||
"training_delay_minutes": 15,
|
||||
"total_measurement_key": null,
|
||||
"total_curtailment_sensitive": false,
|
||||
"total_curtailment_key": null,
|
||||
"total_learning_guard_key": null,
|
||||
"total_learning_guard_max": null,
|
||||
"total_learning_guard_max_gap_seconds": 600
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
<!-- pyml enable line-length -->
|
||||
|
||||
Reference in New Issue
Block a user