From ed79cacf63401ca21f71a16d114293d872e3bce2 Mon Sep 17 00:00:00 2001 From: Bobby Noelte Date: Thu, 2 Jan 2025 12:40:12 +0100 Subject: [PATCH] Fix PVForecast settings active plane detection. (#320) Detect active PV planes by pvforecast_surface_tilt and pvforecast_surface_azimuth to be non None. Assure by default these configuration values are None. Signed-off-by: Bobby Noelte --- docs/akkudoktoreos/openapi.json | 132 +++++--------- src/akkudoktoreos/prediction/pvforecast.py | 106 +++++------ tests/test_pvforecast.py | 199 +++++++++++++++++++++ tests/testdata/EOS.config.json | 30 ++-- 4 files changed, 318 insertions(+), 149 deletions(-) create mode 100644 tests/test_pvforecast.py diff --git a/docs/akkudoktoreos/openapi.json b/docs/akkudoktoreos/openapi.json index 75f002f..d434ba1 100644 --- a/docs/akkudoktoreos/openapi.json +++ b/docs/akkudoktoreos/openapi.json @@ -1309,8 +1309,7 @@ } ], "title": "Pvforecast0 Surface Tilt", - "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking.", - "default": 0 + "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking." }, "pvforecast0_surface_azimuth": { "anyOf": [ @@ -1322,8 +1321,7 @@ } ], "title": "Pvforecast0 Surface Azimuth", - "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", - "default": 180 + "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270)." }, "pvforecast0_userhorizon": { "anyOf": [ @@ -1400,8 +1398,7 @@ } ], "title": "Pvforecast0 Trackingtype", - "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", - "default": 0 + "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south." }, "pvforecast0_optimal_surface_tilt": { "anyOf": [ @@ -1492,7 +1489,7 @@ "pvforecast0_strings_per_inverter": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" @@ -1511,8 +1508,7 @@ } ], "title": "Pvforecast1 Surface Tilt", - "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking.", - "default": 0 + "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking." }, "pvforecast1_surface_azimuth": { "anyOf": [ @@ -1524,8 +1520,7 @@ } ], "title": "Pvforecast1 Surface Azimuth", - "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", - "default": 180 + "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270)." }, "pvforecast1_userhorizon": { "anyOf": [ @@ -1603,8 +1598,7 @@ } ], "title": "Pvforecast1 Trackingtype", - "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", - "default": 0 + "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south." }, "pvforecast1_optimal_surface_tilt": { "anyOf": [ @@ -1695,7 +1689,7 @@ "pvforecast1_strings_per_inverter": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" @@ -1714,8 +1708,7 @@ } ], "title": "Pvforecast2 Surface Tilt", - "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking.", - "default": 0 + "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking." }, "pvforecast2_surface_azimuth": { "anyOf": [ @@ -1727,8 +1720,7 @@ } ], "title": "Pvforecast2 Surface Azimuth", - "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", - "default": 180 + "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270)." }, "pvforecast2_userhorizon": { "anyOf": [ @@ -1806,8 +1798,7 @@ } ], "title": "Pvforecast2 Trackingtype", - "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", - "default": 0 + "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south." }, "pvforecast2_optimal_surface_tilt": { "anyOf": [ @@ -1898,7 +1889,7 @@ "pvforecast2_strings_per_inverter": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" @@ -1917,8 +1908,7 @@ } ], "title": "Pvforecast3 Surface Tilt", - "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking.", - "default": 0 + "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking." }, "pvforecast3_surface_azimuth": { "anyOf": [ @@ -1930,8 +1920,7 @@ } ], "title": "Pvforecast3 Surface Azimuth", - "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", - "default": 180 + "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270)." }, "pvforecast3_userhorizon": { "anyOf": [ @@ -2009,8 +1998,7 @@ } ], "title": "Pvforecast3 Trackingtype", - "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", - "default": 0 + "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south." }, "pvforecast3_optimal_surface_tilt": { "anyOf": [ @@ -2101,7 +2089,7 @@ "pvforecast3_strings_per_inverter": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" @@ -2120,8 +2108,7 @@ } ], "title": "Pvforecast4 Surface Tilt", - "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking.", - "default": 0 + "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking." }, "pvforecast4_surface_azimuth": { "anyOf": [ @@ -2133,8 +2120,7 @@ } ], "title": "Pvforecast4 Surface Azimuth", - "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", - "default": 180 + "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270)." }, "pvforecast4_userhorizon": { "anyOf": [ @@ -2212,8 +2198,7 @@ } ], "title": "Pvforecast4 Trackingtype", - "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", - "default": 0 + "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south." }, "pvforecast4_optimal_surface_tilt": { "anyOf": [ @@ -2304,7 +2289,7 @@ "pvforecast4_strings_per_inverter": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" @@ -2323,8 +2308,7 @@ } ], "title": "Pvforecast5 Surface Tilt", - "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking.", - "default": 0 + "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking." }, "pvforecast5_surface_azimuth": { "anyOf": [ @@ -2336,8 +2320,7 @@ } ], "title": "Pvforecast5 Surface Azimuth", - "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", - "default": 180 + "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270)." }, "pvforecast5_userhorizon": { "anyOf": [ @@ -2415,8 +2398,7 @@ } ], "title": "Pvforecast5 Trackingtype", - "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", - "default": 0 + "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south." }, "pvforecast5_optimal_surface_tilt": { "anyOf": [ @@ -2507,7 +2489,7 @@ "pvforecast5_strings_per_inverter": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" @@ -3980,8 +3962,7 @@ } ], "title": "Pvforecast0 Surface Tilt", - "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking.", - "default": 0 + "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking." }, "pvforecast0_surface_azimuth": { "anyOf": [ @@ -3993,8 +3974,7 @@ } ], "title": "Pvforecast0 Surface Azimuth", - "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", - "default": 180 + "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270)." }, "pvforecast0_userhorizon": { "anyOf": [ @@ -4071,8 +4051,7 @@ } ], "title": "Pvforecast0 Trackingtype", - "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", - "default": 0 + "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south." }, "pvforecast0_optimal_surface_tilt": { "anyOf": [ @@ -4163,7 +4142,7 @@ "pvforecast0_strings_per_inverter": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" @@ -4182,8 +4161,7 @@ } ], "title": "Pvforecast1 Surface Tilt", - "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking.", - "default": 0 + "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking." }, "pvforecast1_surface_azimuth": { "anyOf": [ @@ -4195,8 +4173,7 @@ } ], "title": "Pvforecast1 Surface Azimuth", - "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", - "default": 180 + "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270)." }, "pvforecast1_userhorizon": { "anyOf": [ @@ -4274,8 +4251,7 @@ } ], "title": "Pvforecast1 Trackingtype", - "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", - "default": 0 + "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south." }, "pvforecast1_optimal_surface_tilt": { "anyOf": [ @@ -4366,7 +4342,7 @@ "pvforecast1_strings_per_inverter": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" @@ -4385,8 +4361,7 @@ } ], "title": "Pvforecast2 Surface Tilt", - "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking.", - "default": 0 + "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking." }, "pvforecast2_surface_azimuth": { "anyOf": [ @@ -4398,8 +4373,7 @@ } ], "title": "Pvforecast2 Surface Azimuth", - "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", - "default": 180 + "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270)." }, "pvforecast2_userhorizon": { "anyOf": [ @@ -4477,8 +4451,7 @@ } ], "title": "Pvforecast2 Trackingtype", - "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", - "default": 0 + "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south." }, "pvforecast2_optimal_surface_tilt": { "anyOf": [ @@ -4569,7 +4542,7 @@ "pvforecast2_strings_per_inverter": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" @@ -4588,8 +4561,7 @@ } ], "title": "Pvforecast3 Surface Tilt", - "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking.", - "default": 0 + "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking." }, "pvforecast3_surface_azimuth": { "anyOf": [ @@ -4601,8 +4573,7 @@ } ], "title": "Pvforecast3 Surface Azimuth", - "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", - "default": 180 + "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270)." }, "pvforecast3_userhorizon": { "anyOf": [ @@ -4680,8 +4651,7 @@ } ], "title": "Pvforecast3 Trackingtype", - "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", - "default": 0 + "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south." }, "pvforecast3_optimal_surface_tilt": { "anyOf": [ @@ -4772,7 +4742,7 @@ "pvforecast3_strings_per_inverter": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" @@ -4791,8 +4761,7 @@ } ], "title": "Pvforecast4 Surface Tilt", - "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking.", - "default": 0 + "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking." }, "pvforecast4_surface_azimuth": { "anyOf": [ @@ -4804,8 +4773,7 @@ } ], "title": "Pvforecast4 Surface Azimuth", - "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", - "default": 180 + "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270)." }, "pvforecast4_userhorizon": { "anyOf": [ @@ -4883,8 +4851,7 @@ } ], "title": "Pvforecast4 Trackingtype", - "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", - "default": 0 + "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south." }, "pvforecast4_optimal_surface_tilt": { "anyOf": [ @@ -4975,7 +4942,7 @@ "pvforecast4_strings_per_inverter": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" @@ -4994,8 +4961,7 @@ } ], "title": "Pvforecast5 Surface Tilt", - "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking.", - "default": 0 + "description": "Tilt angle from horizontal plane. Ignored for two-axis tracking." }, "pvforecast5_surface_azimuth": { "anyOf": [ @@ -5007,8 +4973,7 @@ } ], "title": "Pvforecast5 Surface Azimuth", - "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", - "default": 180 + "description": "Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270)." }, "pvforecast5_userhorizon": { "anyOf": [ @@ -5086,8 +5051,7 @@ } ], "title": "Pvforecast5 Trackingtype", - "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", - "default": 0 + "description": "Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south." }, "pvforecast5_optimal_surface_tilt": { "anyOf": [ @@ -5178,7 +5142,7 @@ "pvforecast5_strings_per_inverter": { "anyOf": [ { - "type": "string" + "type": "integer" }, { "type": "null" diff --git a/src/akkudoktoreos/prediction/pvforecast.py b/src/akkudoktoreos/prediction/pvforecast.py index 8455960..5b7b0cb 100644 --- a/src/akkudoktoreos/prediction/pvforecast.py +++ b/src/akkudoktoreos/prediction/pvforecast.py @@ -22,10 +22,10 @@ class PVForecastCommonSettings(SettingsBaseModel): # pvforecast0_latitude: Optional[float] = Field(default=None, description="Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°)") # Plane 0 pvforecast0_surface_tilt: Optional[float] = Field( - default=0, description="Tilt angle from horizontal plane. Ignored for two-axis tracking." + default=None, description="Tilt angle from horizontal plane. Ignored for two-axis tracking." ) pvforecast0_surface_azimuth: Optional[float] = Field( - default=180, + default=None, description="Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", ) pvforecast0_userhorizon: Optional[List[float]] = Field( @@ -46,7 +46,7 @@ class PVForecastCommonSettings(SettingsBaseModel): default=None, description="Sum of PV system losses in percent" ) pvforecast0_trackingtype: Optional[int] = Field( - default=0, + default=None, description="Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", ) pvforecast0_optimal_surface_tilt: Optional[bool] = Field( @@ -73,15 +73,15 @@ class PVForecastCommonSettings(SettingsBaseModel): pvforecast0_modules_per_string: Optional[int] = Field( default=None, description="Number of the PV modules of the strings of this plane." ) - pvforecast0_strings_per_inverter: Optional[str] = Field( + pvforecast0_strings_per_inverter: Optional[int] = Field( default=None, description="Number of the strings of the inverter of this plane." ) # Plane 1 pvforecast1_surface_tilt: Optional[float] = Field( - default=0, description="Tilt angle from horizontal plane. Ignored for two-axis tracking." + default=None, description="Tilt angle from horizontal plane. Ignored for two-axis tracking." ) pvforecast1_surface_azimuth: Optional[float] = Field( - default=180, + default=None, description="Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", ) pvforecast1_userhorizon: Optional[List[float]] = Field( @@ -100,7 +100,7 @@ class PVForecastCommonSettings(SettingsBaseModel): ) pvforecast1_loss: Optional[float] = Field(0, description="Sum of PV system losses in percent") pvforecast1_trackingtype: Optional[int] = Field( - default=0, + default=None, description="Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", ) pvforecast1_optimal_surface_tilt: Optional[bool] = Field( @@ -127,15 +127,15 @@ class PVForecastCommonSettings(SettingsBaseModel): pvforecast1_modules_per_string: Optional[int] = Field( default=None, description="Number of the PV modules of the strings of this plane." ) - pvforecast1_strings_per_inverter: Optional[str] = Field( + pvforecast1_strings_per_inverter: Optional[int] = Field( default=None, description="Number of the strings of the inverter of this plane." ) # Plane 2 pvforecast2_surface_tilt: Optional[float] = Field( - default=0, description="Tilt angle from horizontal plane. Ignored for two-axis tracking." + default=None, description="Tilt angle from horizontal plane. Ignored for two-axis tracking." ) pvforecast2_surface_azimuth: Optional[float] = Field( - default=180, + default=None, description="Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", ) pvforecast2_userhorizon: Optional[List[float]] = Field( @@ -154,7 +154,7 @@ class PVForecastCommonSettings(SettingsBaseModel): ) pvforecast2_loss: Optional[float] = Field(0, description="Sum of PV system losses in percent") pvforecast2_trackingtype: Optional[int] = Field( - default=0, + default=None, description="Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", ) pvforecast2_optimal_surface_tilt: Optional[bool] = Field( @@ -181,15 +181,15 @@ class PVForecastCommonSettings(SettingsBaseModel): pvforecast2_modules_per_string: Optional[int] = Field( default=None, description="Number of the PV modules of the strings of this plane." ) - pvforecast2_strings_per_inverter: Optional[str] = Field( + pvforecast2_strings_per_inverter: Optional[int] = Field( default=None, description="Number of the strings of the inverter of this plane." ) # Plane 3 pvforecast3_surface_tilt: Optional[float] = Field( - default=0, description="Tilt angle from horizontal plane. Ignored for two-axis tracking." + default=None, description="Tilt angle from horizontal plane. Ignored for two-axis tracking." ) pvforecast3_surface_azimuth: Optional[float] = Field( - default=180, + default=None, description="Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", ) pvforecast3_userhorizon: Optional[List[float]] = Field( @@ -208,7 +208,7 @@ class PVForecastCommonSettings(SettingsBaseModel): ) pvforecast3_loss: Optional[float] = Field(0, description="Sum of PV system losses in percent") pvforecast3_trackingtype: Optional[int] = Field( - default=0, + default=None, description="Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", ) pvforecast3_optimal_surface_tilt: Optional[bool] = Field( @@ -235,15 +235,15 @@ class PVForecastCommonSettings(SettingsBaseModel): pvforecast3_modules_per_string: Optional[int] = Field( default=None, description="Number of the PV modules of the strings of this plane." ) - pvforecast3_strings_per_inverter: Optional[str] = Field( + pvforecast3_strings_per_inverter: Optional[int] = Field( default=None, description="Number of the strings of the inverter of this plane." ) # Plane 4 pvforecast4_surface_tilt: Optional[float] = Field( - default=0, description="Tilt angle from horizontal plane. Ignored for two-axis tracking." + default=None, description="Tilt angle from horizontal plane. Ignored for two-axis tracking." ) pvforecast4_surface_azimuth: Optional[float] = Field( - default=180, + default=None, description="Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", ) pvforecast4_userhorizon: Optional[List[float]] = Field( @@ -262,7 +262,7 @@ class PVForecastCommonSettings(SettingsBaseModel): ) pvforecast4_loss: Optional[float] = Field(0, description="Sum of PV system losses in percent") pvforecast4_trackingtype: Optional[int] = Field( - default=0, + default=None, description="Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", ) pvforecast4_optimal_surface_tilt: Optional[bool] = Field( @@ -289,15 +289,15 @@ class PVForecastCommonSettings(SettingsBaseModel): pvforecast4_modules_per_string: Optional[int] = Field( default=None, description="Number of the PV modules of the strings of this plane." ) - pvforecast4_strings_per_inverter: Optional[str] = Field( + pvforecast4_strings_per_inverter: Optional[int] = Field( default=None, description="Number of the strings of the inverter of this plane." ) # Plane 5 pvforecast5_surface_tilt: Optional[float] = Field( - default=0, description="Tilt angle from horizontal plane. Ignored for two-axis tracking." + default=None, description="Tilt angle from horizontal plane. Ignored for two-axis tracking." ) pvforecast5_surface_azimuth: Optional[float] = Field( - default=180, + default=None, description="Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).", ) pvforecast5_userhorizon: Optional[List[float]] = Field( @@ -316,7 +316,7 @@ class PVForecastCommonSettings(SettingsBaseModel): ) pvforecast5_loss: Optional[float] = Field(0, description="Sum of PV system losses in percent") pvforecast5_trackingtype: Optional[int] = Field( - default=0, + default=None, description="Type of suntracking. 0=fixed, 1=single horizontal axis aligned north-south, 2=two-axis tracking, 3=vertical axis tracking, 4=single horizontal axis aligned east-west, 5=single inclined axis aligned north-south.", ) pvforecast5_optimal_surface_tilt: Optional[bool] = Field( @@ -343,7 +343,7 @@ class PVForecastCommonSettings(SettingsBaseModel): pvforecast5_modules_per_string: Optional[int] = Field( default=None, description="Number of the PV modules of the strings of this plane." ) - pvforecast5_strings_per_inverter: Optional[str] = Field( + pvforecast5_strings_per_inverter: Optional[int] = Field( default=None, description="Number of the strings of the inverter of this plane." ) @@ -358,11 +358,17 @@ class PVForecastCommonSettings(SettingsBaseModel): # Loop through pvforecast0 to pvforecast4 for i in range(self.pvforecast_max_planes): - peakpower_attr = f"pvforecast{i}_peakpower" - modules_attr = f"pvforecast{i}_modules_per_string" + plane = f"pvforecast{i}" + tackingtype_attr = f"{plane}_trackingtype" + tilt_attr = f"{plane}_surface_tilt" + azimuth_attr = f"{plane}_surface_azimuth" # Check if either attribute is set and add to active planes - if getattr(self, peakpower_attr, None) or getattr(self, modules_attr, None): + if getattr(self, tackingtype_attr, None) == 2: + # Tilt angle from horizontal plane is gnored for two-axis tracking. + if getattr(self, azimuth_attr, None) is not None: + active_planes.append(f"pvforecast{i}") + elif getattr(self, tilt_attr, None) and getattr(self, azimuth_attr, None): active_planes.append(f"pvforecast{i}") return active_planes @@ -376,11 +382,11 @@ class PVForecastCommonSettings(SettingsBaseModel): for plane in self.pvforecast_planes: peakpower_attr = f"{plane}_peakpower" peakpower = getattr(self, peakpower_attr, None) - if peakpower: + if peakpower is None: + # TODO calculate peak power from modules/strings + planes_peakpower.append(float(5000)) + else: planes_peakpower.append(float(peakpower)) - continue - # TODO calculate peak power from modules/strings - planes_peakpower.append(float(5000)) return planes_peakpower @@ -391,13 +397,13 @@ class PVForecastCommonSettings(SettingsBaseModel): planes_azimuth = [] for plane in self.pvforecast_planes: - azimuth_attr = f"{plane}_azimuth" + azimuth_attr = f"{plane}_surface_azimuth" azimuth = getattr(self, azimuth_attr, None) - if azimuth: + if azimuth is None: + # TODO Use default + planes_azimuth.append(float(180)) + else: planes_azimuth.append(float(azimuth)) - continue - # TODO Use default - planes_azimuth.append(float(180)) return planes_azimuth @@ -408,13 +414,13 @@ class PVForecastCommonSettings(SettingsBaseModel): planes_tilt = [] for plane in self.pvforecast_planes: - tilt_attr = f"{plane}_tilt" + tilt_attr = f"{plane}_surface_tilt" tilt = getattr(self, tilt_attr, None) - if tilt: + if tilt is None: + # TODO Use default + planes_tilt.append(float(30)) + else: planes_tilt.append(float(tilt)) - continue - # TODO Use default - planes_tilt.append(float(0)) return planes_tilt @@ -427,11 +433,11 @@ class PVForecastCommonSettings(SettingsBaseModel): for plane in self.pvforecast_planes: userhorizon_attr = f"{plane}_userhorizon" userhorizon = getattr(self, userhorizon_attr, None) - if userhorizon: + if userhorizon is None: + # TODO Use default + planes_userhorizon.append([float(0), float(0)]) + else: planes_userhorizon.append(userhorizon) - continue - # TODO Use default - planes_userhorizon.append([float(0), float(0)]) return planes_userhorizon @@ -444,10 +450,10 @@ class PVForecastCommonSettings(SettingsBaseModel): for plane in self.pvforecast_planes: inverter_paco_attr = f"{plane}_inverter_paco" inverter_paco = getattr(self, inverter_paco_attr, None) - if inverter_paco: - planes_inverter_paco.append(inverter_paco) - continue - # TODO Use default - no clipping - planes_inverter_paco.append(25000) + if inverter_paco is None: + # TODO Use default - no clipping + planes_inverter_paco.append(25000.0) + else: + planes_inverter_paco.append(float(inverter_paco)) return planes_inverter_paco diff --git a/tests/test_pvforecast.py b/tests/test_pvforecast.py new file mode 100644 index 0000000..2721cd4 --- /dev/null +++ b/tests/test_pvforecast.py @@ -0,0 +1,199 @@ +import pytest + +from akkudoktoreos.prediction.pvforecast import PVForecastCommonSettings + + +@pytest.fixture +def settings(): + """Fixture that creates an empty PVForecastSettings.""" + settings = PVForecastCommonSettings() + + # Check default values for plane 0 + assert settings.pvforecast0_surface_tilt is None + assert settings.pvforecast0_surface_azimuth is None + assert settings.pvforecast0_pvtechchoice == "crystSi" + assert settings.pvforecast0_mountingplace == "free" + assert settings.pvforecast0_trackingtype is None + assert settings.pvforecast0_optimal_surface_tilt is False + assert settings.pvforecast0_optimalangles is False + # Check default values for plane 1 + assert settings.pvforecast1_surface_azimuth is None + assert settings.pvforecast1_pvtechchoice == "crystSi" + assert settings.pvforecast1_mountingplace == "free" + assert settings.pvforecast1_trackingtype is None + assert settings.pvforecast1_optimal_surface_tilt is False + assert settings.pvforecast1_optimalangles is False + + expected_planes: list[str] = [] + assert settings.pvforecast_planes == expected_planes + + return settings + + +def test_active_planes_detection(settings): + """Test that active planes are correctly detected based on tilt and azimuth.""" + settings.pvforecast1_surface_tilt = 10.0 + settings.pvforecast1_surface_azimuth = 10.0 + settings.pvforecast2_surface_tilt = 20.0 + settings.pvforecast2_surface_azimuth = 20.0 + + expected_planes = ["pvforecast1", "pvforecast2"] + assert settings.pvforecast_planes == expected_planes + + +def test_planes_peakpower_computation(settings): + """Test computation of peak power for active planes.""" + settings.pvforecast1_surface_tilt = 10.0 + settings.pvforecast1_surface_azimuth = 10.0 + settings.pvforecast1_peakpower = 5.0 + settings.pvforecast2_surface_tilt = 20.0 + settings.pvforecast2_surface_azimuth = 20.0 + settings.pvforecast2_peakpower = 3.5 + settings.pvforecast3_surface_tilt = 30.0 + settings.pvforecast3_surface_azimuth = 30.0 + settings.pvforecast3_modules_per_string = 20 # Should use default 5000W + + expected_peakpower = [5.0, 3.5, 5000.0] + assert settings.pvforecast_planes_peakpower == expected_peakpower + + +def test_planes_azimuth_computation(settings): + """Test computation of azimuth values for active planes.""" + settings.pvforecast1_surface_tilt = 10.0 + settings.pvforecast1_surface_azimuth = 10.0 + settings.pvforecast2_surface_tilt = 20.0 + settings.pvforecast2_surface_azimuth = 20.0 + + expected_azimuths = [10.0, 20.0] + assert settings.pvforecast_planes_azimuth == expected_azimuths + + +def test_planes_tilt_computation(settings): + """Test computation of tilt values for active planes.""" + settings.pvforecast1_surface_tilt = 10.0 + settings.pvforecast1_surface_azimuth = 10.0 + settings.pvforecast2_surface_tilt = 20.0 + settings.pvforecast2_surface_azimuth = 20.0 + + expected_tilts = [10.0, 20.0] + assert settings.pvforecast_planes_tilt == expected_tilts + + +def test_planes_userhorizon_computation(settings): + """Test computation of user horizon values for active planes.""" + horizon1 = [10.0, 20.0, 30.0] + horizon2 = [5.0, 15.0, 25.0] + + settings.pvforecast1_surface_tilt = 10.0 + settings.pvforecast1_surface_azimuth = 10.0 + settings.pvforecast1_userhorizon = horizon1 + settings.pvforecast2_surface_tilt = 20.0 + settings.pvforecast2_surface_azimuth = 20.0 + settings.pvforecast2_userhorizon = horizon2 + + expected_horizons = [horizon1, horizon2] + assert settings.pvforecast_planes_userhorizon == expected_horizons + + +def test_planes_inverter_paco_computation(settings): + """Test computation of inverter power rating for active planes.""" + settings.pvforecast1_surface_tilt = 10.0 + settings.pvforecast1_surface_azimuth = 10.0 + settings.pvforecast1_inverter_paco = 6000 + settings.pvforecast2_surface_tilt = 20.0 + settings.pvforecast2_surface_azimuth = 20.0 + settings.pvforecast2_inverter_paco = 4000 + + expected_paco = [6000, 4000] + assert settings.pvforecast_planes_inverter_paco == expected_paco + + +def test_non_sequential_plane_numbers(settings): + """Test that non-sequential plane numbers are handled correctly.""" + settings.pvforecast1_surface_tilt = 10.0 + settings.pvforecast1_surface_azimuth = 10.0 + settings.pvforecast1_peakpower = 5.0 + settings.pvforecast3_surface_tilt = 30.0 + settings.pvforecast3_surface_azimuth = 30.0 + settings.pvforecast3_peakpower = 3.5 + settings.pvforecast5_surface_tilt = 50.0 + settings.pvforecast5_surface_azimuth = 50.0 + settings.pvforecast5_peakpower = 2.0 + + expected_planes = ["pvforecast1", "pvforecast3", "pvforecast5"] + assert settings.pvforecast_planes == expected_planes + assert settings.pvforecast_planes_peakpower == [5.0, 3.5, 2.0] + + +def test_mixed_plane_configuration(settings): + """Test mixed configuration with some planes having peak power and others having modules.""" + settings.pvforecast1_surface_tilt = 10.0 + settings.pvforecast1_surface_azimuth = 10.0 + settings.pvforecast1_peakpower = 5.0 + settings.pvforecast2_surface_tilt = 20.0 + settings.pvforecast2_surface_azimuth = 20.0 + settings.pvforecast2_modules_per_string = 20 + settings.pvforecast2_strings_per_inverter = 2 + settings.pvforecast4_surface_tilt = 40.0 + settings.pvforecast4_surface_azimuth = 40.0 + settings.pvforecast4_peakpower = 3.0 + + expected_planes = ["pvforecast1", "pvforecast2", "pvforecast4"] + assert settings.pvforecast_planes == expected_planes + # First plane uses specified peak power, second uses default, third uses specified + assert settings.pvforecast_planes_peakpower == [5.0, 5000.0, 3.0] + + +def test_max_planes_limit(settings): + """Test that the maximum number of planes is enforced.""" + assert settings.pvforecast_max_planes == 6 + + # Create settings with more planes than allowed (should only recognize up to max) + plane_settings = {} + for i in range(1, 8): # Try to set up 7 planes, skipping plane 0 + plane_settings[f"pvforecast{i}_peakpower"] = 5.0 + + settings = PVForecastCommonSettings(**plane_settings) + assert len(settings.pvforecast_planes) <= settings.pvforecast_max_planes + + +def test_optional_parameters_non_zero_plane(settings): + """Test that optional parameters can be None for non-zero planes.""" + settings.pvforecast1_peakpower = 5.0 + settings.pvforecast1_albedo = None + settings.pvforecast1_module_model = None + settings.pvforecast1_userhorizon = None + + assert settings.pvforecast1_albedo is None + assert settings.pvforecast1_module_model is None + assert settings.pvforecast1_userhorizon is None + + +def test_tracking_type_values_non_zero_plane(settings): + """Test valid tracking type values for non-zero planes.""" + valid_types = [0, 1, 2, 3, 4, 5] + + for tracking_type in valid_types: + settings.pvforecast1_peakpower = 5.0 + settings.pvforecast1_trackingtype = tracking_type + assert settings.pvforecast1_trackingtype == tracking_type + + +def test_pv_technology_values_non_zero_plane(settings): + """Test valid PV technology values for non-zero planes.""" + valid_technologies = ["crystSi", "CIS", "CdTe", "Unknown"] + + for tech in valid_technologies: + settings.pvforecast2_peakpower = 5.0 + settings.pvforecast2_pvtechchoice = tech + assert settings.pvforecast2_pvtechchoice == tech + + +def test_mounting_place_values_non_zero_plane(settings): + """Test valid mounting place values for non-zero planes.""" + valid_mounting = ["free", "building"] + + for mounting in valid_mounting: + settings.pvforecast3_peakpower = 5.0 + settings.pvforecast3_mountingplace = mounting + assert settings.pvforecast3_mountingplace == mounting diff --git a/tests/testdata/EOS.config.json b/tests/testdata/EOS.config.json index b1016c5..b0170c8 100644 --- a/tests/testdata/EOS.config.json +++ b/tests/testdata/EOS.config.json @@ -31,9 +31,9 @@ "pvforecast0_peakpower": null, "pvforecast0_pvtechchoice": "crystSi", "pvforecast0_strings_per_inverter": null, - "pvforecast0_surface_azimuth": 180, - "pvforecast0_surface_tilt": 0, - "pvforecast0_trackingtype": 0, + "pvforecast0_surface_azimuth": null, + "pvforecast0_surface_tilt": null, + "pvforecast0_trackingtype": null, "pvforecast0_userhorizon": null, "pvforecast1_albedo": null, "pvforecast1_inverter_model": null, @@ -47,9 +47,9 @@ "pvforecast1_peakpower": null, "pvforecast1_pvtechchoice": "crystSi", "pvforecast1_strings_per_inverter": null, - "pvforecast1_surface_azimuth": 180, - "pvforecast1_surface_tilt": 0, - "pvforecast1_trackingtype": 0, + "pvforecast1_surface_azimuth": null, + "pvforecast1_surface_tilt": null, + "pvforecast1_trackingtype": null, "pvforecast1_userhorizon": null, "pvforecast2_albedo": null, "pvforecast2_inverter_model": null, @@ -63,9 +63,9 @@ "pvforecast2_peakpower": null, "pvforecast2_pvtechchoice": "crystSi", "pvforecast2_strings_per_inverter": null, - "pvforecast2_surface_azimuth": 180, - "pvforecast2_surface_tilt": 0, - "pvforecast2_trackingtype": 0, + "pvforecast2_surface_azimuth": null, + "pvforecast2_surface_tilt": null, + "pvforecast2_trackingtype": null, "pvforecast2_userhorizon": null, "pvforecast3_albedo": null, "pvforecast3_inverter_model": null, @@ -79,9 +79,9 @@ "pvforecast3_peakpower": null, "pvforecast3_pvtechchoice": "crystSi", "pvforecast3_strings_per_inverter": null, - "pvforecast3_surface_azimuth": 180, - "pvforecast3_surface_tilt": 0, - "pvforecast3_trackingtype": 0, + "pvforecast3_surface_azimuth": null, + "pvforecast3_surface_tilt": null, + "pvforecast3_trackingtype": null, "pvforecast3_userhorizon": null, "pvforecast4_albedo": null, "pvforecast4_inverter_model": null, @@ -95,9 +95,9 @@ "pvforecast4_peakpower": null, "pvforecast4_pvtechchoice": "crystSi", "pvforecast4_strings_per_inverter": null, - "pvforecast4_surface_azimuth": 180, - "pvforecast4_surface_tilt": 0, - "pvforecast4_trackingtype": 0, + "pvforecast4_surface_azimuth": null, + "pvforecast4_surface_tilt": null, + "pvforecast4_trackingtype": null, "pvforecast4_userhorizon": null, "pvforecast_provider": null, "pvforecastimport_file_path": null,