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:
Andreas
2026-11-03 15:31:32 +01:00
committed by GitHub
co-authored by Normann Christin Claude Opus 5.5 YMT sarankumar1325
parent 298d46a527
commit f804d48072
24 changed files with 6063 additions and 13 deletions
+770 -1
View File
@@ -8,7 +8,7 @@
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "v0.3.0.dev2610301534325811"
"version": "v0.3.0.dev2611011668106370"
},
"paths": {
"/v1/measurement/battery-capacity/{battery_id}": {
@@ -330,6 +330,182 @@
}
}
},
"/v1/prediction/pv-adaptive/status": {
"get": {
"tags": [
"prediction"
],
"summary": "Status",
"description": "Status contains honest cold-start counts and matched per-lead error metrics.",
"operationId": "status_v1_prediction_pv_adaptive_status_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"additionalProperties": true,
"type": "object",
"title": "Response Status V1 Prediction Pv Adaptive Status Get"
}
}
}
}
}
}
},
"/v1/prediction/pv-adaptive/samples": {
"put": {
"tags": [
"prediction"
],
"summary": "Samples",
"description": "Store original power readings in the bounded adaptive archive, without zero filling.",
"operationId": "samples_v1_prediction_pv_adaptive_samples_put",
"requestBody": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/MeasurementSample"
},
"type": "array",
"maxItems": 10000,
"title": "Values"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"additionalProperties": {
"type": "integer"
},
"type": "object",
"title": "Response Samples V1 Prediction Pv Adaptive Samples Put"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/v1/prediction/pv-adaptive/comparisons": {
"get": {
"tags": [
"prediction"
],
"summary": "Comparisons",
"description": "Export genuinely issued forecasts with later measured truth, issuance and lead.",
"operationId": "comparisons_v1_prediction_pv_adaptive_comparisons_get",
"parameters": [
{
"name": "days",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"maximum": 30,
"minimum": 1,
"default": 2,
"title": "Days"
}
},
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"maximum": 20000,
"minimum": 1,
"default": 2000,
"title": "Limit"
}
},
{
"name": "since_ms",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"minimum": 0,
"default": 0,
"title": "Since Ms"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
},
"title": "Response Comparisons V1 Prediction Pv Adaptive Comparisons Get"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/v1/prediction/pv-adaptive/latest": {
"get": {
"tags": [
"prediction"
],
"summary": "Latest",
"description": "Latest complete output curve; validation uses the separate immutable archive.",
"operationId": "latest_v1_prediction_pv_adaptive_latest_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": {
"additionalProperties": true,
"type": "object"
},
"type": "array",
"title": "Response Latest V1 Prediction Pv Adaptive Latest Get"
}
}
}
}
}
}
},
"/v1/admin/cache/clear": {
"post": {
"tags": [
@@ -14127,6 +14303,470 @@
"title": "PPBCStartInterruptionInstruction",
"description": "Represents an instruction to interrupt execution of a running power sequence.\n\nThis model defines a control instruction that interrupts the execution of an\nactive power sequence. It enables dynamic control over sequence execution,\nallowing temporary suspension of a sequence in response to changing system conditions\nor requirements, particularly for sequences marked as interruptible."
},
"PVForecastAdaptiveAkkudoktorCommonSettings": {
"properties": {
"backend": {
"type": "string",
"enum": [
"remote",
"local"
],
"title": "Backend",
"default": "local"
},
"resolution_minutes": {
"type": "integer",
"title": "Resolution Minutes",
"description": "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.",
"default": 15,
"examples": [
15,
60
]
},
"forecast_days": {
"anyOf": [
{
"type": "integer",
"maximum": 16.0,
"minimum": 1.0
},
{
"type": "null"
}
],
"title": "Forecast Days",
"description": "Forecast horizon in days (1-16). Leave empty to derive it from `prediction.hours`, which is what keeps the optimizer's tail horizon fed.",
"examples": [
null,
7
]
},
"past_days": {
"anyOf": [
{
"type": "integer",
"maximum": 92.0,
"minimum": 0.0
},
{
"type": "null"
}
],
"title": "Past Days",
"description": "Days of past data to request (0-92). Leave empty to derive it from `prediction.historic_hours`.",
"examples": [
null,
3
]
},
"weather_models": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"title": "Weather Models",
"description": "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.",
"default": [
"best_match"
],
"examples": [
[
"best_match"
],
[
"icon_seamless",
"ecmwf_ifs025",
"gfs_seamless"
]
]
},
"transposition_model": {
"type": "string",
"title": "Transposition Model",
"description": "pvlib sky-diffuse transposition model: isotropic, klucher, haydavies, reindl, king or perez.",
"default": "perez",
"examples": [
"perez",
"haydavies"
]
},
"albedo": {
"type": "number",
"maximum": 1.0,
"minimum": 0.0,
"title": "Albedo",
"description": "Ground albedo used for planes that do not set their own.",
"default": 0.25,
"examples": [
0.25,
0.2
]
},
"inverter_efficiency": {
"type": "number",
"maximum": 1.0,
"exclusiveMinimum": 0.0,
"title": "Inverter Efficiency",
"description": "Nominal inverter efficiency (PVWatts eta_inv_nom).",
"default": 0.96,
"examples": [
0.96,
0.94
]
},
"temperature_coefficient": {
"type": "number",
"title": "Temperature Coefficient",
"description": "Module power temperature coefficient in %/degC (negative). Matches the `cellCoEff` the akkudoktor.net forecast uses.",
"default": -0.36,
"examples": [
-0.36,
-0.29
]
},
"apply_iam": {
"type": "boolean",
"title": "Apply Iam",
"description": "Apply the ASHRAE incidence-angle modifier to the beam component.",
"default": true,
"examples": [
true
]
},
"shift_to_interval_start": {
"type": "boolean",
"title": "Shift To Interval Start",
"description": "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.",
"default": true,
"examples": [
true
]
},
"calibration_enabled": {
"type": "boolean",
"title": "Calibration Enabled",
"default": false
},
"calibration_days": {
"type": "integer",
"maximum": 92.0,
"minimum": 1.0,
"title": "Calibration Days",
"description": "Length of the measurement window used to fit the correction.",
"default": 30,
"examples": [
30,
14
]
},
"calibration_reference_days": {
"type": "integer",
"maximum": 92.0,
"minimum": 3.0,
"title": "Calibration Reference Days",
"description": "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.",
"default": 30,
"examples": [
30,
14
]
},
"calibration_outage_filter_enabled": {
"type": "boolean",
"title": "Calibration Outage Filter Enabled",
"description": "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.",
"default": true,
"examples": [
true
]
},
"calibration_outage_threshold": {
"type": "number",
"exclusiveMaximum": 1.0,
"exclusiveMinimum": 0.0,
"title": "Calibration Outage Threshold",
"description": "A day is treated as unavailable when its measured/modelled energy ratio is below this fraction of the robust healthy reference ratio.",
"default": 0.55,
"examples": [
0.55,
0.5
]
},
"calibration_min_healthy_days": {
"type": "integer",
"maximum": 31.0,
"minimum": 1.0,
"title": "Calibration Min Healthy Days",
"description": "Minimum number of healthy days used for a fit. Older healthy days from the reference window are added when the recent window contains fewer.",
"default": 3,
"examples": [
3
]
},
"calibration_azimuth_bin_degrees": {
"type": "integer",
"maximum": 180.0,
"minimum": 0.0,
"title": "Calibration Azimuth Bin Degrees",
"description": "Width of the solar-azimuth bins for the correction. 0 fits a single global factor only.",
"default": 45,
"examples": [
45,
30,
15,
0
]
},
"calibration_prior_kwh": {
"type": "number",
"minimum": 0.0,
"title": "Calibration Prior Kwh",
"description": "Shrinkage strength: a bin needs this much modelled energy before its own factor outweighs the global one. Higher is more conservative.",
"default": 5.0,
"examples": [
5.0,
20.0
]
},
"calibration_min_factor": {
"type": "number",
"exclusiveMinimum": 0.0,
"title": "Calibration Min Factor",
"description": "Lower clamp on any fitted correction factor.",
"default": 0.5,
"examples": [
0.5
]
},
"calibration_max_factor": {
"type": "number",
"exclusiveMinimum": 0.0,
"title": "Calibration Max Factor",
"description": "Upper clamp on any fitted correction factor.",
"default": 1.5,
"examples": [
1.5
]
},
"regional_weather_model": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Regional Weather Model",
"default": "icon_d2"
},
"regional_horizon_hours": {
"type": "number",
"maximum": 48.0,
"exclusiveMinimum": 0.0,
"title": "Regional Horizon Hours",
"default": 6
},
"learning_days": {
"type": "integer",
"maximum": 30.0,
"minimum": 7.0,
"title": "Learning Days",
"default": 21
},
"min_training_days": {
"type": "integer",
"maximum": 14.0,
"minimum": 2.0,
"title": "Min Training Days",
"default": 5
},
"max_ac_power_w": {
"type": "number",
"minimum": 0.0,
"title": "Max Ac Power W",
"default": 0
},
"nominal_total_peakpower_kw": {
"anyOf": [
{
"type": "number",
"exclusiveMinimum": 0.0
},
{
"type": "null"
}
],
"title": "Nominal Total Peakpower Kw",
"description": "Known installed nameplate total; fitted plane powers may be effective estimates.",
"examples": [
null
]
},
"diagnostics_days": {
"type": "integer",
"maximum": 30.0,
"minimum": 1.0,
"title": "Diagnostics Days",
"default": 7
},
"archive_retention_days": {
"type": "integer",
"maximum": 365.0,
"minimum": 31.0,
"title": "Archive Retention Days",
"default": 60
},
"nowcast_enabled": {
"type": "boolean",
"title": "Nowcast Enabled",
"default": true
},
"trend_enabled": {
"type": "boolean",
"title": "Trend Enabled",
"default": true
},
"solar_response_enabled": {
"type": "boolean",
"title": "Solar Response Enabled",
"description": "Learn empirical angular response relative to separate, eligible reference arrays; activate only after issued-candidate validation.",
"default": true
},
"weather_response_enabled": {
"type": "boolean",
"title": "Weather Response Enabled",
"description": "Archive received model weather and learn horizon/regime corrections; activate only after issued-candidate validation.",
"default": true
},
"correction_mode": {
"type": "string",
"enum": [
"auto",
"calibrated",
"local",
"trend"
],
"title": "Correction Mode",
"description": "Choose validated short-term candidates per group/lead, or explicitly fix a correction stage.",
"default": "auto"
},
"validation_days": {
"type": "integer",
"maximum": 30.0,
"minimum": 3.0,
"title": "Validation Days",
"default": 7
},
"min_validation_days": {
"type": "integer",
"maximum": 14.0,
"minimum": 2.0,
"title": "Min Validation Days",
"default": 3
},
"min_validation_samples": {
"type": "integer",
"minimum": 8.0,
"title": "Min Validation Samples",
"default": 32
},
"min_validation_improvement": {
"type": "number",
"maximum": 0.5,
"minimum": 0.0,
"title": "Min Validation Improvement",
"default": 0.02
},
"training_delay_minutes": {
"type": "integer",
"maximum": 120.0,
"minimum": 0.0,
"title": "Training Delay Minutes",
"default": 15
},
"total_measurement_key": {
"anyOf": [
{
"type": "string",
"maxLength": 128,
"minLength": 1
},
{
"type": "null"
}
],
"title": "Total Measurement Key",
"description": "Optional whole-plant power channel when individual plane measurements are unavailable.",
"examples": [
null
]
},
"total_curtailment_sensitive": {
"type": "boolean",
"title": "Total Curtailment Sensitive",
"default": false
},
"total_curtailment_key": {
"anyOf": [
{
"type": "string",
"maxLength": 128,
"minLength": 1
},
{
"type": "null"
}
],
"title": "Total Curtailment Key",
"examples": [
null
]
},
"total_learning_guard_key": {
"anyOf": [
{
"type": "string",
"maxLength": 128,
"minLength": 1
},
{
"type": "null"
}
],
"title": "Total Learning Guard Key",
"examples": [
null
]
},
"total_learning_guard_max": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Total Learning Guard Max",
"examples": [
null
]
},
"total_learning_guard_max_gap_seconds": {
"type": "integer",
"maximum": 3600.0,
"minimum": 1.0,
"title": "Total Learning Guard Max Gap Seconds",
"default": 600
}
},
"type": "object",
"title": "PVForecastAdaptiveAkkudoktorCommonSettings",
"description": "Automatic defaults; configure measurements and reference flags on each plane."
},
"PVForecastAkkudoktorLocalCommonSettings": {
"properties": {
"backend": {
@@ -14409,6 +15049,10 @@
"$ref": "#/components/schemas/PVForecastAkkudoktorLocalCommonSettings",
"description": "Akkudoktor forecast backend and local calibration settings"
},
"adaptive": {
"$ref": "#/components/schemas/PVForecastAdaptiveAkkudoktorCommonSettings",
"description": "Adaptive Akkudoktor: configure string measurements and independent reference flags on the planes."
},
"pvforecastimport": {
"$ref": "#/components/schemas/PVForecastImportCommonSettings",
"description": "PV forecast import provider settings"
@@ -14538,6 +15182,10 @@
"$ref": "#/components/schemas/PVForecastAkkudoktorLocalCommonSettings",
"description": "Akkudoktor forecast backend and local calibration settings"
},
"adaptive": {
"$ref": "#/components/schemas/PVForecastAdaptiveAkkudoktorCommonSettings",
"description": "Adaptive Akkudoktor: configure string measurements and independent reference flags on the planes."
},
"pvforecastimport": {
"$ref": "#/components/schemas/PVForecastImportCommonSettings",
"description": "PV forecast import provider settings"
@@ -14902,6 +15550,127 @@
},
"PVForecastPlaneSetting": {
"properties": {
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Name",
"description": "Readable PV string name.",
"examples": [
null
]
},
"measurement_key": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Measurement Key",
"description": "Matching EOS power measurement key for adaptive learning.",
"examples": [
null
]
},
"nowcast_reference": {
"type": "boolean",
"title": "Nowcast Reference",
"description": "Use this independent array as a local weather reference.",
"default": false
},
"curtailment_sensitive": {
"type": "boolean",
"title": "Curtailment Sensitive",
"description": "Battery-full curtailment possible; exclude from unrestricted weather learning.",
"default": false
},
"parameters_estimated": {
"type": "boolean",
"title": "Parameters Estimated",
"description": "Geometry and peak power are effective fitted model estimates, not verified nameplate data.",
"default": false
},
"correction_group": {
"anyOf": [
{
"type": "string",
"maxLength": 128,
"minLength": 1
},
{
"type": "null"
}
],
"title": "Correction Group",
"description": "Optional group of planes sharing short-term validation. Unset independent references share one group; other planes are separate.",
"examples": [
null
]
},
"curtailment_key": {
"anyOf": [
{
"type": "string",
"maxLength": 128,
"minLength": 1
},
{
"type": "null"
}
],
"title": "Curtailment Key",
"description": "Optional measured flag: 0 explicitly unrestricted, 1 curtailed. Missing or stale flags never authorize learning a curtailment-sensitive plane.",
"examples": [
null
]
},
"learning_guard_key": {
"anyOf": [
{
"type": "string",
"maxLength": 128,
"minLength": 1
},
{
"type": "null"
}
],
"title": "Learning Guard Key",
"description": "Optional site-specific positive learning guard, e.g. battery SoC, as an alternative to a binary curtailment flag.",
"examples": [
null
]
},
"learning_guard_max": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Learning Guard Max",
"description": "Learn only while every supported guard value is below this bound. This is an explicit site assumption, not automatic curtailment detection.",
"examples": [
null
]
},
"learning_guard_max_gap_seconds": {
"type": "integer",
"maximum": 3600.0,
"minimum": 1.0,
"title": "Learning Guard Max Gap Seconds",
"default": 600
},
"surface_tilt": {
"anyOf": [
{