PVForecast: planes as nested config (list)

This commit is contained in:
Dominique Lasserre 2025-01-19 18:12:50 +01:00
parent e0b1ece524
commit af5e4a753a
13 changed files with 1085 additions and 4142 deletions

3
.gitignore vendored
View File

@ -260,3 +260,6 @@ tests/testdata/new_optimize_result*
tests/testdata/openapi-new.json tests/testdata/openapi-new.json
tests/testdata/openapi-new.md tests/testdata/openapi-new.md
tests/testdata/config-new.md tests/testdata/config-new.md
# FastHTML session key
.sesskey

View File

@ -510,109 +510,13 @@ Validators:
| Name | Environment Variable | Type | Read-Only | Default | Description | | Name | Environment Variable | Type | Read-Only | Default | Description |
| ---- | -------------------- | ---- | --------- | ------- | ----------- | | ---- | -------------------- | ---- | --------- | ------- | ----------- |
| provider | `EOS_PVFORECAST__PROVIDER` | `Optional[str]` | `rw` | `None` | PVForecast provider id of provider to be used. | | provider | `EOS_PVFORECAST__PROVIDER` | `Optional[str]` | `rw` | `None` | PVForecast provider id of provider to be used. |
| pvforecast0_surface_tilt | `EOS_PVFORECAST__PVFORECAST0_SURFACE_TILT` | `Optional[float]` | `rw` | `None` | Tilt angle from horizontal plane. Ignored for two-axis tracking. | | planes | `EOS_PVFORECAST__PLANES` | `Optional[list[akkudoktoreos.prediction.pvforecast.PVForecastPlaneSetting]]` | `rw` | `None` | Plane configuration. |
| pvforecast0_surface_azimuth | `EOS_PVFORECAST__PVFORECAST0_SURFACE_AZIMUTH` | `Optional[float]` | `rw` | `None` | Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270). |
| pvforecast0_userhorizon | `EOS_PVFORECAST__PVFORECAST0_USERHORIZON` | `Optional[List[float]]` | `rw` | `None` | Elevation of horizon in degrees, at equally spaced azimuth clockwise from north. |
| pvforecast0_peakpower | `EOS_PVFORECAST__PVFORECAST0_PEAKPOWER` | `Optional[float]` | `rw` | `None` | Nominal power of PV system in kW. |
| pvforecast0_pvtechchoice | `EOS_PVFORECAST__PVFORECAST0_PVTECHCHOICE` | `Optional[str]` | `rw` | `crystSi` | PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'. |
| pvforecast0_mountingplace | `EOS_PVFORECAST__PVFORECAST0_MOUNTINGPLACE` | `Optional[str]` | `rw` | `free` | Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated. |
| pvforecast0_loss | `EOS_PVFORECAST__PVFORECAST0_LOSS` | `Optional[float]` | `rw` | `14.0` | Sum of PV system losses in percent |
| pvforecast0_trackingtype | `EOS_PVFORECAST__PVFORECAST0_TRACKINGTYPE` | `Optional[int]` | `rw` | `None` | 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 | `EOS_PVFORECAST__PVFORECAST0_OPTIMAL_SURFACE_TILT` | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt angle. Ignored for two-axis tracking. |
| pvforecast0_optimalangles | `EOS_PVFORECAST__PVFORECAST0_OPTIMALANGLES` | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking. |
| pvforecast0_albedo | `EOS_PVFORECAST__PVFORECAST0_ALBEDO` | `Optional[float]` | `rw` | `None` | Proportion of the light hitting the ground that it reflects back. |
| pvforecast0_module_model | `EOS_PVFORECAST__PVFORECAST0_MODULE_MODEL` | `Optional[str]` | `rw` | `None` | Model of the PV modules of this plane. |
| pvforecast0_inverter_model | `EOS_PVFORECAST__PVFORECAST0_INVERTER_MODEL` | `Optional[str]` | `rw` | `None` | Model of the inverter of this plane. |
| pvforecast0_inverter_paco | `EOS_PVFORECAST__PVFORECAST0_INVERTER_PACO` | `Optional[int]` | `rw` | `None` | AC power rating of the inverter. [W] |
| pvforecast0_modules_per_string | `EOS_PVFORECAST__PVFORECAST0_MODULES_PER_STRING` | `Optional[int]` | `rw` | `None` | Number of the PV modules of the strings of this plane. |
| pvforecast0_strings_per_inverter | `EOS_PVFORECAST__PVFORECAST0_STRINGS_PER_INVERTER` | `Optional[int]` | `rw` | `None` | Number of the strings of the inverter of this plane. |
| pvforecast1_surface_tilt | `EOS_PVFORECAST__PVFORECAST1_SURFACE_TILT` | `Optional[float]` | `rw` | `None` | Tilt angle from horizontal plane. Ignored for two-axis tracking. |
| pvforecast1_surface_azimuth | `EOS_PVFORECAST__PVFORECAST1_SURFACE_AZIMUTH` | `Optional[float]` | `rw` | `None` | Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270). |
| pvforecast1_userhorizon | `EOS_PVFORECAST__PVFORECAST1_USERHORIZON` | `Optional[List[float]]` | `rw` | `None` | Elevation of horizon in degrees, at equally spaced azimuth clockwise from north. |
| pvforecast1_peakpower | `EOS_PVFORECAST__PVFORECAST1_PEAKPOWER` | `Optional[float]` | `rw` | `None` | Nominal power of PV system in kW. |
| pvforecast1_pvtechchoice | `EOS_PVFORECAST__PVFORECAST1_PVTECHCHOICE` | `Optional[str]` | `rw` | `crystSi` | PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'. |
| pvforecast1_mountingplace | `EOS_PVFORECAST__PVFORECAST1_MOUNTINGPLACE` | `Optional[str]` | `rw` | `free` | Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated. |
| pvforecast1_loss | `EOS_PVFORECAST__PVFORECAST1_LOSS` | `Optional[float]` | `rw` | `14.0` | Sum of PV system losses in percent |
| pvforecast1_trackingtype | `EOS_PVFORECAST__PVFORECAST1_TRACKINGTYPE` | `Optional[int]` | `rw` | `None` | 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 | `EOS_PVFORECAST__PVFORECAST1_OPTIMAL_SURFACE_TILT` | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt angle. Ignored for two-axis tracking. |
| pvforecast1_optimalangles | `EOS_PVFORECAST__PVFORECAST1_OPTIMALANGLES` | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking. |
| pvforecast1_albedo | `EOS_PVFORECAST__PVFORECAST1_ALBEDO` | `Optional[float]` | `rw` | `None` | Proportion of the light hitting the ground that it reflects back. |
| pvforecast1_module_model | `EOS_PVFORECAST__PVFORECAST1_MODULE_MODEL` | `Optional[str]` | `rw` | `None` | Model of the PV modules of this plane. |
| pvforecast1_inverter_model | `EOS_PVFORECAST__PVFORECAST1_INVERTER_MODEL` | `Optional[str]` | `rw` | `None` | Model of the inverter of this plane. |
| pvforecast1_inverter_paco | `EOS_PVFORECAST__PVFORECAST1_INVERTER_PACO` | `Optional[int]` | `rw` | `None` | AC power rating of the inverter. [W] |
| pvforecast1_modules_per_string | `EOS_PVFORECAST__PVFORECAST1_MODULES_PER_STRING` | `Optional[int]` | `rw` | `None` | Number of the PV modules of the strings of this plane. |
| pvforecast1_strings_per_inverter | `EOS_PVFORECAST__PVFORECAST1_STRINGS_PER_INVERTER` | `Optional[int]` | `rw` | `None` | Number of the strings of the inverter of this plane. |
| pvforecast2_surface_tilt | `EOS_PVFORECAST__PVFORECAST2_SURFACE_TILT` | `Optional[float]` | `rw` | `None` | Tilt angle from horizontal plane. Ignored for two-axis tracking. |
| pvforecast2_surface_azimuth | `EOS_PVFORECAST__PVFORECAST2_SURFACE_AZIMUTH` | `Optional[float]` | `rw` | `None` | Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270). |
| pvforecast2_userhorizon | `EOS_PVFORECAST__PVFORECAST2_USERHORIZON` | `Optional[List[float]]` | `rw` | `None` | Elevation of horizon in degrees, at equally spaced azimuth clockwise from north. |
| pvforecast2_peakpower | `EOS_PVFORECAST__PVFORECAST2_PEAKPOWER` | `Optional[float]` | `rw` | `None` | Nominal power of PV system in kW. |
| pvforecast2_pvtechchoice | `EOS_PVFORECAST__PVFORECAST2_PVTECHCHOICE` | `Optional[str]` | `rw` | `crystSi` | PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'. |
| pvforecast2_mountingplace | `EOS_PVFORECAST__PVFORECAST2_MOUNTINGPLACE` | `Optional[str]` | `rw` | `free` | Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated. |
| pvforecast2_loss | `EOS_PVFORECAST__PVFORECAST2_LOSS` | `Optional[float]` | `rw` | `14.0` | Sum of PV system losses in percent |
| pvforecast2_trackingtype | `EOS_PVFORECAST__PVFORECAST2_TRACKINGTYPE` | `Optional[int]` | `rw` | `None` | 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 | `EOS_PVFORECAST__PVFORECAST2_OPTIMAL_SURFACE_TILT` | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt angle. Ignored for two-axis tracking. |
| pvforecast2_optimalangles | `EOS_PVFORECAST__PVFORECAST2_OPTIMALANGLES` | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking. |
| pvforecast2_albedo | `EOS_PVFORECAST__PVFORECAST2_ALBEDO` | `Optional[float]` | `rw` | `None` | Proportion of the light hitting the ground that it reflects back. |
| pvforecast2_module_model | `EOS_PVFORECAST__PVFORECAST2_MODULE_MODEL` | `Optional[str]` | `rw` | `None` | Model of the PV modules of this plane. |
| pvforecast2_inverter_model | `EOS_PVFORECAST__PVFORECAST2_INVERTER_MODEL` | `Optional[str]` | `rw` | `None` | Model of the inverter of this plane. |
| pvforecast2_inverter_paco | `EOS_PVFORECAST__PVFORECAST2_INVERTER_PACO` | `Optional[int]` | `rw` | `None` | AC power rating of the inverter. [W] |
| pvforecast2_modules_per_string | `EOS_PVFORECAST__PVFORECAST2_MODULES_PER_STRING` | `Optional[int]` | `rw` | `None` | Number of the PV modules of the strings of this plane. |
| pvforecast2_strings_per_inverter | `EOS_PVFORECAST__PVFORECAST2_STRINGS_PER_INVERTER` | `Optional[int]` | `rw` | `None` | Number of the strings of the inverter of this plane. |
| pvforecast3_surface_tilt | `EOS_PVFORECAST__PVFORECAST3_SURFACE_TILT` | `Optional[float]` | `rw` | `None` | Tilt angle from horizontal plane. Ignored for two-axis tracking. |
| pvforecast3_surface_azimuth | `EOS_PVFORECAST__PVFORECAST3_SURFACE_AZIMUTH` | `Optional[float]` | `rw` | `None` | Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270). |
| pvforecast3_userhorizon | `EOS_PVFORECAST__PVFORECAST3_USERHORIZON` | `Optional[List[float]]` | `rw` | `None` | Elevation of horizon in degrees, at equally spaced azimuth clockwise from north. |
| pvforecast3_peakpower | `EOS_PVFORECAST__PVFORECAST3_PEAKPOWER` | `Optional[float]` | `rw` | `None` | Nominal power of PV system in kW. |
| pvforecast3_pvtechchoice | `EOS_PVFORECAST__PVFORECAST3_PVTECHCHOICE` | `Optional[str]` | `rw` | `crystSi` | PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'. |
| pvforecast3_mountingplace | `EOS_PVFORECAST__PVFORECAST3_MOUNTINGPLACE` | `Optional[str]` | `rw` | `free` | Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated. |
| pvforecast3_loss | `EOS_PVFORECAST__PVFORECAST3_LOSS` | `Optional[float]` | `rw` | `14.0` | Sum of PV system losses in percent |
| pvforecast3_trackingtype | `EOS_PVFORECAST__PVFORECAST3_TRACKINGTYPE` | `Optional[int]` | `rw` | `None` | 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 | `EOS_PVFORECAST__PVFORECAST3_OPTIMAL_SURFACE_TILT` | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt angle. Ignored for two-axis tracking. |
| pvforecast3_optimalangles | `EOS_PVFORECAST__PVFORECAST3_OPTIMALANGLES` | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking. |
| pvforecast3_albedo | `EOS_PVFORECAST__PVFORECAST3_ALBEDO` | `Optional[float]` | `rw` | `None` | Proportion of the light hitting the ground that it reflects back. |
| pvforecast3_module_model | `EOS_PVFORECAST__PVFORECAST3_MODULE_MODEL` | `Optional[str]` | `rw` | `None` | Model of the PV modules of this plane. |
| pvforecast3_inverter_model | `EOS_PVFORECAST__PVFORECAST3_INVERTER_MODEL` | `Optional[str]` | `rw` | `None` | Model of the inverter of this plane. |
| pvforecast3_inverter_paco | `EOS_PVFORECAST__PVFORECAST3_INVERTER_PACO` | `Optional[int]` | `rw` | `None` | AC power rating of the inverter. [W] |
| pvforecast3_modules_per_string | `EOS_PVFORECAST__PVFORECAST3_MODULES_PER_STRING` | `Optional[int]` | `rw` | `None` | Number of the PV modules of the strings of this plane. |
| pvforecast3_strings_per_inverter | `EOS_PVFORECAST__PVFORECAST3_STRINGS_PER_INVERTER` | `Optional[int]` | `rw` | `None` | Number of the strings of the inverter of this plane. |
| pvforecast4_surface_tilt | `EOS_PVFORECAST__PVFORECAST4_SURFACE_TILT` | `Optional[float]` | `rw` | `None` | Tilt angle from horizontal plane. Ignored for two-axis tracking. |
| pvforecast4_surface_azimuth | `EOS_PVFORECAST__PVFORECAST4_SURFACE_AZIMUTH` | `Optional[float]` | `rw` | `None` | Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270). |
| pvforecast4_userhorizon | `EOS_PVFORECAST__PVFORECAST4_USERHORIZON` | `Optional[List[float]]` | `rw` | `None` | Elevation of horizon in degrees, at equally spaced azimuth clockwise from north. |
| pvforecast4_peakpower | `EOS_PVFORECAST__PVFORECAST4_PEAKPOWER` | `Optional[float]` | `rw` | `None` | Nominal power of PV system in kW. |
| pvforecast4_pvtechchoice | `EOS_PVFORECAST__PVFORECAST4_PVTECHCHOICE` | `Optional[str]` | `rw` | `crystSi` | PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'. |
| pvforecast4_mountingplace | `EOS_PVFORECAST__PVFORECAST4_MOUNTINGPLACE` | `Optional[str]` | `rw` | `free` | Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated. |
| pvforecast4_loss | `EOS_PVFORECAST__PVFORECAST4_LOSS` | `Optional[float]` | `rw` | `14.0` | Sum of PV system losses in percent |
| pvforecast4_trackingtype | `EOS_PVFORECAST__PVFORECAST4_TRACKINGTYPE` | `Optional[int]` | `rw` | `None` | 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 | `EOS_PVFORECAST__PVFORECAST4_OPTIMAL_SURFACE_TILT` | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt angle. Ignored for two-axis tracking. |
| pvforecast4_optimalangles | `EOS_PVFORECAST__PVFORECAST4_OPTIMALANGLES` | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking. |
| pvforecast4_albedo | `EOS_PVFORECAST__PVFORECAST4_ALBEDO` | `Optional[float]` | `rw` | `None` | Proportion of the light hitting the ground that it reflects back. |
| pvforecast4_module_model | `EOS_PVFORECAST__PVFORECAST4_MODULE_MODEL` | `Optional[str]` | `rw` | `None` | Model of the PV modules of this plane. |
| pvforecast4_inverter_model | `EOS_PVFORECAST__PVFORECAST4_INVERTER_MODEL` | `Optional[str]` | `rw` | `None` | Model of the inverter of this plane. |
| pvforecast4_inverter_paco | `EOS_PVFORECAST__PVFORECAST4_INVERTER_PACO` | `Optional[int]` | `rw` | `None` | AC power rating of the inverter. [W] |
| pvforecast4_modules_per_string | `EOS_PVFORECAST__PVFORECAST4_MODULES_PER_STRING` | `Optional[int]` | `rw` | `None` | Number of the PV modules of the strings of this plane. |
| pvforecast4_strings_per_inverter | `EOS_PVFORECAST__PVFORECAST4_STRINGS_PER_INVERTER` | `Optional[int]` | `rw` | `None` | Number of the strings of the inverter of this plane. |
| pvforecast5_surface_tilt | `EOS_PVFORECAST__PVFORECAST5_SURFACE_TILT` | `Optional[float]` | `rw` | `None` | Tilt angle from horizontal plane. Ignored for two-axis tracking. |
| pvforecast5_surface_azimuth | `EOS_PVFORECAST__PVFORECAST5_SURFACE_AZIMUTH` | `Optional[float]` | `rw` | `None` | Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270). |
| pvforecast5_userhorizon | `EOS_PVFORECAST__PVFORECAST5_USERHORIZON` | `Optional[List[float]]` | `rw` | `None` | Elevation of horizon in degrees, at equally spaced azimuth clockwise from north. |
| pvforecast5_peakpower | `EOS_PVFORECAST__PVFORECAST5_PEAKPOWER` | `Optional[float]` | `rw` | `None` | Nominal power of PV system in kW. |
| pvforecast5_pvtechchoice | `EOS_PVFORECAST__PVFORECAST5_PVTECHCHOICE` | `Optional[str]` | `rw` | `crystSi` | PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'. |
| pvforecast5_mountingplace | `EOS_PVFORECAST__PVFORECAST5_MOUNTINGPLACE` | `Optional[str]` | `rw` | `free` | Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated. |
| pvforecast5_loss | `EOS_PVFORECAST__PVFORECAST5_LOSS` | `Optional[float]` | `rw` | `14.0` | Sum of PV system losses in percent |
| pvforecast5_trackingtype | `EOS_PVFORECAST__PVFORECAST5_TRACKINGTYPE` | `Optional[int]` | `rw` | `None` | 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 | `EOS_PVFORECAST__PVFORECAST5_OPTIMAL_SURFACE_TILT` | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt angle. Ignored for two-axis tracking. |
| pvforecast5_optimalangles | `EOS_PVFORECAST__PVFORECAST5_OPTIMALANGLES` | `Optional[bool]` | `rw` | `False` | Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking. |
| pvforecast5_albedo | `EOS_PVFORECAST__PVFORECAST5_ALBEDO` | `Optional[float]` | `rw` | `None` | Proportion of the light hitting the ground that it reflects back. |
| pvforecast5_module_model | `EOS_PVFORECAST__PVFORECAST5_MODULE_MODEL` | `Optional[str]` | `rw` | `None` | Model of the PV modules of this plane. |
| pvforecast5_inverter_model | `EOS_PVFORECAST__PVFORECAST5_INVERTER_MODEL` | `Optional[str]` | `rw` | `None` | Model of the inverter of this plane. |
| pvforecast5_inverter_paco | `EOS_PVFORECAST__PVFORECAST5_INVERTER_PACO` | `Optional[int]` | `rw` | `None` | AC power rating of the inverter. [W] |
| pvforecast5_modules_per_string | `EOS_PVFORECAST__PVFORECAST5_MODULES_PER_STRING` | `Optional[int]` | `rw` | `None` | Number of the PV modules of the strings of this plane. |
| pvforecast5_strings_per_inverter | `EOS_PVFORECAST__PVFORECAST5_STRINGS_PER_INVERTER` | `Optional[int]` | `rw` | `None` | Number of the strings of the inverter of this plane. |
| provider_settings | `EOS_PVFORECAST__PROVIDER_SETTINGS` | `Optional[akkudoktoreos.prediction.pvforecastimport.PVForecastImportCommonSettings]` | `rw` | `None` | Provider settings | | provider_settings | `EOS_PVFORECAST__PROVIDER_SETTINGS` | `Optional[akkudoktoreos.prediction.pvforecastimport.PVForecastImportCommonSettings]` | `rw` | `None` | Provider settings |
| pvforecast_planes | | `List[str]` | `ro` | `N/A` | Compute a list of active planes. | | planes_peakpower | | `List[float]` | `ro` | `N/A` | Compute a list of the peak power per active planes. |
| pvforecast_planes_peakpower | | `List[float]` | `ro` | `N/A` | Compute a list of the peak power per active planes. | | planes_azimuth | | `List[float]` | `ro` | `N/A` | Compute a list of the azimuths per active planes. |
| pvforecast_planes_azimuth | | `List[float]` | `ro` | `N/A` | Compute a list of the azimuths per active planes. | | planes_tilt | | `List[float]` | `ro` | `N/A` | Compute a list of the tilts per active planes. |
| pvforecast_planes_tilt | | `List[float]` | `ro` | `N/A` | Compute a list of the tilts per active planes. | | planes_userhorizon | | `Any` | `ro` | `N/A` | Compute a list of the user horizon per active planes. |
| pvforecast_planes_userhorizon | | `Any` | `ro` | `N/A` | Compute a list of the user horizon per active planes. | | planes_inverter_paco | | `Any` | `ro` | `N/A` | Compute a list of the maximum power rating of the inverter per active planes. |
| pvforecast_planes_inverter_paco | | `Any` | `ro` | `N/A` | Compute a list of the maximum power rating of the inverter per active planes. |
::: :::
### Example Input ### Example Input
@ -623,110 +527,52 @@ Validators:
{ {
"pvforecast": { "pvforecast": {
"provider": "PVForecastAkkudoktor", "provider": "PVForecastAkkudoktor",
"pvforecast0_surface_tilt": 10.0, "planes": [
"pvforecast0_surface_azimuth": 10.0, {
"pvforecast0_userhorizon": [ "surface_tilt": 10.0,
10.0, "surface_azimuth": 10.0,
20.0, "userhorizon": [
30.0 10.0,
20.0,
30.0
],
"peakpower": 5.0,
"pvtechchoice": "crystSi",
"mountingplace": "free",
"loss": 14.0,
"trackingtype": 0,
"optimal_surface_tilt": false,
"optimalangles": false,
"albedo": null,
"module_model": null,
"inverter_model": null,
"inverter_paco": 6000,
"modules_per_string": 20,
"strings_per_inverter": 2
},
{
"surface_tilt": 20.0,
"surface_azimuth": 20.0,
"userhorizon": [
5.0,
15.0,
25.0
],
"peakpower": 3.5,
"pvtechchoice": "crystSi",
"mountingplace": "free",
"loss": 14.0,
"trackingtype": 1,
"optimal_surface_tilt": false,
"optimalangles": false,
"albedo": null,
"module_model": null,
"inverter_model": null,
"inverter_paco": 4000,
"modules_per_string": 20,
"strings_per_inverter": 2
}
], ],
"pvforecast0_peakpower": 5.0,
"pvforecast0_pvtechchoice": "crystSi",
"pvforecast0_mountingplace": "free",
"pvforecast0_loss": 14.0,
"pvforecast0_trackingtype": 0,
"pvforecast0_optimal_surface_tilt": false,
"pvforecast0_optimalangles": false,
"pvforecast0_albedo": null,
"pvforecast0_module_model": null,
"pvforecast0_inverter_model": null,
"pvforecast0_inverter_paco": 6000,
"pvforecast0_modules_per_string": 20,
"pvforecast0_strings_per_inverter": 2,
"pvforecast1_surface_tilt": 20.0,
"pvforecast1_surface_azimuth": 20.0,
"pvforecast1_userhorizon": [
5.0,
15.0,
25.0
],
"pvforecast1_peakpower": 3.5,
"pvforecast1_pvtechchoice": "crystSi",
"pvforecast1_mountingplace": "free",
"pvforecast1_loss": 14.0,
"pvforecast1_trackingtype": null,
"pvforecast1_optimal_surface_tilt": false,
"pvforecast1_optimalangles": false,
"pvforecast1_albedo": null,
"pvforecast1_module_model": null,
"pvforecast1_inverter_model": null,
"pvforecast1_inverter_paco": 4000,
"pvforecast1_modules_per_string": 20,
"pvforecast1_strings_per_inverter": 2,
"pvforecast2_surface_tilt": null,
"pvforecast2_surface_azimuth": null,
"pvforecast2_userhorizon": null,
"pvforecast2_peakpower": null,
"pvforecast2_pvtechchoice": null,
"pvforecast2_mountingplace": null,
"pvforecast2_loss": null,
"pvforecast2_trackingtype": null,
"pvforecast2_optimal_surface_tilt": null,
"pvforecast2_optimalangles": null,
"pvforecast2_albedo": null,
"pvforecast2_module_model": null,
"pvforecast2_inverter_model": null,
"pvforecast2_inverter_paco": null,
"pvforecast2_modules_per_string": null,
"pvforecast2_strings_per_inverter": null,
"pvforecast3_surface_tilt": null,
"pvforecast3_surface_azimuth": null,
"pvforecast3_userhorizon": null,
"pvforecast3_peakpower": null,
"pvforecast3_pvtechchoice": null,
"pvforecast3_mountingplace": null,
"pvforecast3_loss": null,
"pvforecast3_trackingtype": null,
"pvforecast3_optimal_surface_tilt": null,
"pvforecast3_optimalangles": null,
"pvforecast3_albedo": null,
"pvforecast3_module_model": null,
"pvforecast3_inverter_model": null,
"pvforecast3_inverter_paco": null,
"pvforecast3_modules_per_string": null,
"pvforecast3_strings_per_inverter": null,
"pvforecast4_surface_tilt": null,
"pvforecast4_surface_azimuth": null,
"pvforecast4_userhorizon": null,
"pvforecast4_peakpower": null,
"pvforecast4_pvtechchoice": null,
"pvforecast4_mountingplace": null,
"pvforecast4_loss": null,
"pvforecast4_trackingtype": null,
"pvforecast4_optimal_surface_tilt": null,
"pvforecast4_optimalangles": null,
"pvforecast4_albedo": null,
"pvforecast4_module_model": null,
"pvforecast4_inverter_model": null,
"pvforecast4_inverter_paco": null,
"pvforecast4_modules_per_string": null,
"pvforecast4_strings_per_inverter": null,
"pvforecast5_surface_tilt": null,
"pvforecast5_surface_azimuth": null,
"pvforecast5_userhorizon": null,
"pvforecast5_peakpower": null,
"pvforecast5_pvtechchoice": null,
"pvforecast5_mountingplace": null,
"pvforecast5_loss": null,
"pvforecast5_trackingtype": null,
"pvforecast5_optimal_surface_tilt": null,
"pvforecast5_optimalangles": null,
"pvforecast5_albedo": null,
"pvforecast5_module_model": null,
"pvforecast5_inverter_model": null,
"pvforecast5_inverter_paco": null,
"pvforecast5_modules_per_string": null,
"pvforecast5_strings_per_inverter": null,
"provider_settings": null "provider_settings": null
} }
} }
@ -740,128 +586,66 @@ Validators:
{ {
"pvforecast": { "pvforecast": {
"provider": "PVForecastAkkudoktor", "provider": "PVForecastAkkudoktor",
"pvforecast0_surface_tilt": 10.0, "planes": [
"pvforecast0_surface_azimuth": 10.0, {
"pvforecast0_userhorizon": [ "surface_tilt": 10.0,
10.0, "surface_azimuth": 10.0,
20.0, "userhorizon": [
30.0 10.0,
20.0,
30.0
],
"peakpower": 5.0,
"pvtechchoice": "crystSi",
"mountingplace": "free",
"loss": 14.0,
"trackingtype": 0,
"optimal_surface_tilt": false,
"optimalangles": false,
"albedo": null,
"module_model": null,
"inverter_model": null,
"inverter_paco": 6000,
"modules_per_string": 20,
"strings_per_inverter": 2
},
{
"surface_tilt": 20.0,
"surface_azimuth": 20.0,
"userhorizon": [
5.0,
15.0,
25.0
],
"peakpower": 3.5,
"pvtechchoice": "crystSi",
"mountingplace": "free",
"loss": 14.0,
"trackingtype": 1,
"optimal_surface_tilt": false,
"optimalangles": false,
"albedo": null,
"module_model": null,
"inverter_model": null,
"inverter_paco": 4000,
"modules_per_string": 20,
"strings_per_inverter": 2
}
], ],
"pvforecast0_peakpower": 5.0,
"pvforecast0_pvtechchoice": "crystSi",
"pvforecast0_mountingplace": "free",
"pvforecast0_loss": 14.0,
"pvforecast0_trackingtype": 0,
"pvforecast0_optimal_surface_tilt": false,
"pvforecast0_optimalangles": false,
"pvforecast0_albedo": null,
"pvforecast0_module_model": null,
"pvforecast0_inverter_model": null,
"pvforecast0_inverter_paco": 6000,
"pvforecast0_modules_per_string": 20,
"pvforecast0_strings_per_inverter": 2,
"pvforecast1_surface_tilt": 20.0,
"pvforecast1_surface_azimuth": 20.0,
"pvforecast1_userhorizon": [
5.0,
15.0,
25.0
],
"pvforecast1_peakpower": 3.5,
"pvforecast1_pvtechchoice": "crystSi",
"pvforecast1_mountingplace": "free",
"pvforecast1_loss": 14.0,
"pvforecast1_trackingtype": null,
"pvforecast1_optimal_surface_tilt": false,
"pvforecast1_optimalangles": false,
"pvforecast1_albedo": null,
"pvforecast1_module_model": null,
"pvforecast1_inverter_model": null,
"pvforecast1_inverter_paco": 4000,
"pvforecast1_modules_per_string": 20,
"pvforecast1_strings_per_inverter": 2,
"pvforecast2_surface_tilt": null,
"pvforecast2_surface_azimuth": null,
"pvforecast2_userhorizon": null,
"pvforecast2_peakpower": null,
"pvforecast2_pvtechchoice": null,
"pvforecast2_mountingplace": null,
"pvforecast2_loss": null,
"pvforecast2_trackingtype": null,
"pvforecast2_optimal_surface_tilt": null,
"pvforecast2_optimalangles": null,
"pvforecast2_albedo": null,
"pvforecast2_module_model": null,
"pvforecast2_inverter_model": null,
"pvforecast2_inverter_paco": null,
"pvforecast2_modules_per_string": null,
"pvforecast2_strings_per_inverter": null,
"pvforecast3_surface_tilt": null,
"pvforecast3_surface_azimuth": null,
"pvforecast3_userhorizon": null,
"pvforecast3_peakpower": null,
"pvforecast3_pvtechchoice": null,
"pvforecast3_mountingplace": null,
"pvforecast3_loss": null,
"pvforecast3_trackingtype": null,
"pvforecast3_optimal_surface_tilt": null,
"pvforecast3_optimalangles": null,
"pvforecast3_albedo": null,
"pvforecast3_module_model": null,
"pvforecast3_inverter_model": null,
"pvforecast3_inverter_paco": null,
"pvforecast3_modules_per_string": null,
"pvforecast3_strings_per_inverter": null,
"pvforecast4_surface_tilt": null,
"pvforecast4_surface_azimuth": null,
"pvforecast4_userhorizon": null,
"pvforecast4_peakpower": null,
"pvforecast4_pvtechchoice": null,
"pvforecast4_mountingplace": null,
"pvforecast4_loss": null,
"pvforecast4_trackingtype": null,
"pvforecast4_optimal_surface_tilt": null,
"pvforecast4_optimalangles": null,
"pvforecast4_albedo": null,
"pvforecast4_module_model": null,
"pvforecast4_inverter_model": null,
"pvforecast4_inverter_paco": null,
"pvforecast4_modules_per_string": null,
"pvforecast4_strings_per_inverter": null,
"pvforecast5_surface_tilt": null,
"pvforecast5_surface_azimuth": null,
"pvforecast5_userhorizon": null,
"pvforecast5_peakpower": null,
"pvforecast5_pvtechchoice": null,
"pvforecast5_mountingplace": null,
"pvforecast5_loss": null,
"pvforecast5_trackingtype": null,
"pvforecast5_optimal_surface_tilt": null,
"pvforecast5_optimalangles": null,
"pvforecast5_albedo": null,
"pvforecast5_module_model": null,
"pvforecast5_inverter_model": null,
"pvforecast5_inverter_paco": null,
"pvforecast5_modules_per_string": null,
"pvforecast5_strings_per_inverter": null,
"provider_settings": null, "provider_settings": null,
"pvforecast_planes": [ "planes_peakpower": [
"pvforecast0",
"pvforecast1"
],
"pvforecast_planes_peakpower": [
5.0, 5.0,
3.5 3.5
], ],
"pvforecast_planes_azimuth": [ "planes_azimuth": [
10.0, 10.0,
20.0 20.0
], ],
"pvforecast_planes_tilt": [ "planes_tilt": [
10.0, 10.0,
20.0 20.0
], ],
"pvforecast_planes_userhorizon": [ "planes_userhorizon": [
[ [
10.0, 10.0,
20.0, 20.0,
@ -873,7 +657,7 @@ Validators:
25.0 25.0
] ]
], ],
"pvforecast_planes_inverter_paco": [ "planes_inverter_paco": [
6000.0, 6000.0,
4000.0 4000.0
] ]
@ -889,8 +673,8 @@ Validators:
| Name | Type | Read-Only | Default | Description | | Name | Type | Read-Only | Default | Description |
| ---- | ---- | --------- | ------- | ----------- | | ---- | ---- | --------- | ------- | ----------- |
| pvforecastimport_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | Path to the file to import PV forecast data from. | | import_file_path | `Union[str, pathlib.Path, NoneType]` | `rw` | `None` | Path to the file to import PV forecast data from. |
| pvforecastimport_json | `Optional[str]` | `rw` | `None` | JSON string, dictionary of PV forecast value lists. | | import_json | `Optional[str]` | `rw` | `None` | JSON string, dictionary of PV forecast value lists. |
::: :::
#### Example Input/Output #### Example Input/Output
@ -901,13 +685,96 @@ Validators:
{ {
"pvforecast": { "pvforecast": {
"provider_settings": { "provider_settings": {
"pvforecastimport_file_path": null, "import_file_path": null,
"pvforecastimport_json": "{\"pvforecast_ac_power\": [0, 8.05, 352.91]}" "import_json": "{\"pvforecast_ac_power\": [0, 8.05, 352.91]}"
} }
} }
} }
``` ```
### PV Forecast Plane Configuration
:::{table} pvforecast::planes::list
:widths: 10 10 5 5 30
:align: left
| Name | Type | Read-Only | Default | Description |
| ---- | ---- | --------- | ------- | ----------- |
| surface_tilt | `Optional[float]` | `rw` | `None` | Tilt angle from horizontal plane. Ignored for two-axis tracking. |
| surface_azimuth | `Optional[float]` | `rw` | `None` | Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270). |
| userhorizon | `Optional[List[float]]` | `rw` | `None` | Elevation of horizon in degrees, at equally spaced azimuth clockwise from north. |
| 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'. |
| mountingplace | `Optional[str]` | `rw` | `free` | Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated. |
| loss | `Optional[float]` | `rw` | `14.0` | Sum of PV system losses in percent |
| trackingtype | `Optional[int]` | `rw` | `None` | 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. |
| 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. |
| albedo | `Optional[float]` | `rw` | `None` | Proportion of the light hitting the ground that it reflects back. |
| module_model | `Optional[str]` | `rw` | `None` | Model of the PV modules of this plane. |
| 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] |
| modules_per_string | `Optional[int]` | `rw` | `None` | Number of the PV modules of the strings of this plane. |
| strings_per_inverter | `Optional[int]` | `rw` | `None` | Number of the strings of the inverter of this plane. |
:::
#### Example Input/Output
```{eval-rst}
.. code-block:: json
{
"pvforecast": {
"planes": [
{
"surface_tilt": 10.0,
"surface_azimuth": 10.0,
"userhorizon": [
10.0,
20.0,
30.0
],
"peakpower": 5.0,
"pvtechchoice": "crystSi",
"mountingplace": "free",
"loss": 14.0,
"trackingtype": 0,
"optimal_surface_tilt": false,
"optimalangles": false,
"albedo": null,
"module_model": null,
"inverter_model": null,
"inverter_paco": 6000,
"modules_per_string": 20,
"strings_per_inverter": 2
},
{
"surface_tilt": 20.0,
"surface_azimuth": 20.0,
"userhorizon": [
5.0,
15.0,
25.0
],
"peakpower": 3.5,
"pvtechchoice": "crystSi",
"mountingplace": "free",
"loss": 14.0,
"trackingtype": 1,
"optimal_surface_tilt": false,
"optimalangles": false,
"albedo": null,
"module_model": null,
"inverter_model": null,
"inverter_paco": 4000,
"modules_per_string": 20,
"strings_per_inverter": 2
}
]
}
}
```
## Weather Forecast Configuration ## Weather Forecast Configuration
:::{table} weather :::{table} weather

View File

@ -211,8 +211,8 @@ Configuration options:
- `pvforecast<0..5>_inverter_paco`: AC power rating of the inverter. [W] - `pvforecast<0..5>_inverter_paco`: AC power rating of the inverter. [W]
- `pvforecast<0..5>_modules_per_string`: Number of the PV modules of the strings of this plane. - `pvforecast<0..5>_modules_per_string`: Number of the PV modules of the strings of this plane.
- `pvforecast<0..5>_strings_per_inverter`: Number of the strings of the inverter of this plane. - `pvforecast<0..5>_strings_per_inverter`: Number of the strings of the inverter of this plane.
- `pvforecastimport_file_path`: Path to the file to import PV forecast data from. - `import_file_path`: Path to the file to import PV forecast data from.
- `pvforecastimport_json`: JSON string, dictionary of PV forecast value lists. - `import_json`: JSON string, dictionary of PV forecast value lists.
------ ------
@ -337,7 +337,7 @@ The prediction keys for the PV forecast data are:
The PV forecast data must be provided in one of the formats described in The PV forecast data must be provided in one of the formats described in
<project:#prediction-import-providers>. The data source must be given in the <project:#prediction-import-providers>. The data source must be given in the
`pvforecastimport_file_path` or `pvforecastimport_json` configuration option. `import_file_path` or `import_json` configuration option.
## Weather Prediction ## Weather Prediction
@ -459,4 +459,4 @@ The prediction keys for the PV forecast data are:
The PV forecast data must be provided in one of the formats described in The PV forecast data must be provided in one of the formats described in
<project:#prediction-import-providers>. The data source must be given in the <project:#prediction-import-providers>. The data source must be given in the
`import_file_path` or `pvforecastimport_json` configuration option. `import_file_path` or `import_json` configuration option.

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,7 @@ from pydantic_core import PydanticUndefined
from akkudoktoreos.config.config import get_config from akkudoktoreos.config.config import get_config
from akkudoktoreos.core.logging import get_logger from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import PydanticBaseModel from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.utils.docs import get_model_structure_from_examples
logger = get_logger(__name__) logger = get_logger(__name__)
@ -51,23 +52,13 @@ def resolve_nested_types(field_type: Any, parent_types: list[str]) -> list[tuple
return resolved_types return resolved_types
def get_example_or_default(field_name: str, field_info: FieldInfo) -> dict[str, Any]: def create_model_from_examples(
"""Generate a default value for a field, considering constraints.""" model_class: PydanticBaseModel, multiple: bool
if field_info.examples is not None: ) -> list[PydanticBaseModel]:
return field_info.examples[0]
if field_info.default is not None:
return field_info.default
raise NotImplementedError(f"No default or example provided '{field_name}': {field_info}")
def create_model_from_examples(model_class: PydanticBaseModel) -> PydanticBaseModel:
"""Create a model instance with default or example values, respecting constraints.""" """Create a model instance with default or example values, respecting constraints."""
example_data = {} return [
for field_name, field_info in model_class.model_fields.items(): model_class(**data) for data in get_model_structure_from_examples(model_class, multiple)
example_data[field_name] = get_example_or_default(field_name, field_info) ]
return model_class(**example_data)
def build_nested_structure(keys: list[str], value: Any) -> Any: def build_nested_structure(keys: list[str], value: Any) -> Any:
@ -198,21 +189,29 @@ def generate_config_table_md(
if toplevel: if toplevel:
table += ":::\n\n" # Add an empty line after the table table += ":::\n\n" # Add an empty line after the table
ins = create_model_from_examples(config) has_examples_list = toplevel_keys[-1] == "list"
if ins: instance_list = create_model_from_examples(config, has_examples_list)
# Transform to JSON (and manually to dict) to use custom serializers and then merge with parent keys if instance_list:
ins_json = ins.model_dump_json(include_computed_fields=False) ins_dict_list = []
ins_dict = json.loads(ins_json) ins_out_dict_list = []
for ins in instance_list:
# Transform to JSON (and manually to dict) to use custom serializers and then merge with parent keys
ins_json = ins.model_dump_json(include_computed_fields=False)
ins_dict_list.append(json.loads(ins_json))
ins_out_json = ins.model_dump_json(include_computed_fields=True) ins_out_json = ins.model_dump_json(include_computed_fields=True)
ins_out_dict = json.loads(ins_out_json) ins_out_dict_list.append(json.loads(ins_out_json))
same_output = ins_out_dict == ins_dict
same_output = ins_out_dict_list == ins_dict_list
same_output_str = "/Output" if same_output else "" same_output_str = "/Output" if same_output else ""
table += f"#{heading_level} Example Input{same_output_str}\n\n" table += f"#{heading_level} Example Input{same_output_str}\n\n"
table += "```{eval-rst}\n" table += "```{eval-rst}\n"
table += ".. code-block:: json\n\n" table += ".. code-block:: json\n\n"
input_dict = build_nested_structure(toplevel_keys, ins_dict) if has_examples_list:
input_dict = build_nested_structure(toplevel_keys[:-1], ins_dict_list)
else:
input_dict = build_nested_structure(toplevel_keys, ins_dict_list[0])
table += textwrap.indent(json.dumps(input_dict, indent=4), " ") table += textwrap.indent(json.dumps(input_dict, indent=4), " ")
table += "\n" table += "\n"
table += "```\n\n" table += "```\n\n"
@ -221,7 +220,10 @@ def generate_config_table_md(
table += f"#{heading_level} Example Output\n\n" table += f"#{heading_level} Example Output\n\n"
table += "```{eval-rst}\n" table += "```{eval-rst}\n"
table += ".. code-block:: json\n\n" table += ".. code-block:: json\n\n"
output_dict = build_nested_structure(toplevel_keys, ins_out_dict) if has_examples_list:
output_dict = build_nested_structure(toplevel_keys[:-1], ins_out_dict_list)
else:
output_dict = build_nested_structure(toplevel_keys, ins_out_dict_list[0])
table += textwrap.indent(json.dumps(output_dict, indent=4), " ") table += textwrap.indent(json.dumps(output_dict, indent=4), " ")
table += "\n" table += "\n"
table += "```\n\n" table += "```\n\n"

View File

@ -24,27 +24,36 @@ def config_pvforecast() -> dict:
}, },
"pvforecast": { "pvforecast": {
"provider": "PVForecastAkkudoktor", "provider": "PVForecastAkkudoktor",
"pvforecast0_peakpower": 5.0, "planes": [
"pvforecast0_surface_azimuth": -10, {
"pvforecast0_surface_tilt": 7, "peakpower": 5.0,
"pvforecast0_userhorizon": [20, 27, 22, 20], "surface_azimuth": -10,
"pvforecast0_inverter_paco": 10000, "surface_tilt": 7,
"pvforecast1_peakpower": 4.8, "userhorizon": [20, 27, 22, 20],
"pvforecast1_surface_azimuth": -90, "inverter_paco": 10000,
"pvforecast1_surface_tilt": 7, },
"pvforecast1_userhorizon": [30, 30, 30, 50], {
"pvforecast1_inverter_paco": 10000, "peakpower": 4.8,
"pvforecast2_peakpower": 1.4, "surface_azimuth": -90,
"pvforecast2_surface_azimuth": -40, "surface_tilt": 7,
"pvforecast2_surface_tilt": 60, "userhorizon": [30, 30, 30, 50],
"pvforecast2_userhorizon": [60, 30, 0, 30], "inverter_paco": 10000,
"pvforecast2_inverter_paco": 2000, },
"pvforecast3_peakpower": 1.6, {
"pvforecast3_surface_azimuth": 5, "peakpower": 1.4,
"pvforecast3_surface_tilt": 45, "surface_azimuth": -40,
"pvforecast3_userhorizon": [45, 25, 30, 60], "surface_tilt": 60,
"pvforecast3_inverter_paco": 1400, "userhorizon": [60, 30, 0, 30],
"pvforecast4_peakpower": None, "inverter_paco": 2000,
},
{
"peakpower": 1.6,
"surface_azimuth": 5,
"surface_tilt": 45,
"userhorizon": [45, 25, 30, 60],
"inverter_paco": 1400,
},
],
}, },
} }
return settings return settings
@ -112,7 +121,7 @@ def run_prediction(provider_id: str, verbose: bool = False) -> str:
elif provider_id in ("BrightSky", "ClearOutside"): elif provider_id in ("BrightSky", "ClearOutside"):
settings = config_weather() settings = config_weather()
settings["weather"]["provider"] = provider_id settings["weather"]["provider"] = provider_id
elif provider_id in ("Akkudoktor",): elif provider_id in ("ElecPriceAkkudoktor",):
settings = config_elecprice() settings = config_elecprice()
settings["elecprice"]["provider"] = provider_id settings["elecprice"]["provider"] = provider_id
elif provider_id in ("LoadAkkudoktor",): elif provider_id in ("LoadAkkudoktor",):

View File

@ -1,16 +1,113 @@
"""PV forecast module for PV power predictions.""" """PV forecast module for PV power predictions."""
from typing import Any, ClassVar, List, Optional from typing import Any, ClassVar, List, Optional, Self
from pydantic import Field, computed_field from pydantic import Field, computed_field, field_validator, model_validator
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.pvforecastimport import PVForecastImportCommonSettings from akkudoktoreos.prediction.pvforecastimport import PVForecastImportCommonSettings
from akkudoktoreos.utils.docs import get_model_structure_from_examples
logger = get_logger(__name__) logger = get_logger(__name__)
class PVForecastPlaneSetting(SettingsBaseModel):
"""PV Forecast Plane Configuration."""
# latitude: Optional[float] = Field(default=None, description="Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°)")
surface_tilt: Optional[float] = Field(
default=None,
description="Tilt angle from horizontal plane. Ignored for two-axis tracking.",
examples=[10.0, 20.0],
)
surface_azimuth: Optional[float] = Field(
default=None,
description="Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).",
examples=[10.0, 20.0],
)
userhorizon: Optional[List[float]] = Field(
default=None,
description="Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.",
examples=[[10.0, 20.0, 30.0], [5.0, 15.0, 25.0]],
)
peakpower: Optional[float] = Field(
default=None, description="Nominal power of PV system in kW.", examples=[5.0, 3.5]
)
pvtechchoice: Optional[str] = Field(
default="crystSi", description="PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'."
)
mountingplace: Optional[str] = Field(
default="free",
description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.",
)
loss: Optional[float] = Field(default=14.0, description="Sum of PV system losses in percent")
trackingtype: Optional[int] = Field(
default=None,
ge=0,
le=5,
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.",
examples=[0, 1, 2, 3, 4, 5],
)
optimal_surface_tilt: Optional[bool] = Field(
default=False,
description="Calculate the optimum tilt angle. Ignored for two-axis tracking.",
examples=[False],
)
optimalangles: Optional[bool] = Field(
default=False,
description="Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking.",
examples=[False],
)
albedo: Optional[float] = Field(
default=None,
description="Proportion of the light hitting the ground that it reflects back.",
examples=[None],
)
module_model: Optional[str] = Field(
default=None, description="Model of the PV modules of this plane.", examples=[None]
)
inverter_model: Optional[str] = Field(
default=None, description="Model of the inverter of this plane.", examples=[None]
)
inverter_paco: Optional[int] = Field(
default=None, description="AC power rating of the inverter. [W]", examples=[6000, 4000]
)
modules_per_string: Optional[int] = Field(
default=None,
description="Number of the PV modules of the strings of this plane.",
examples=[20],
)
strings_per_inverter: Optional[int] = Field(
default=None,
description="Number of the strings of the inverter of this plane.",
examples=[2],
)
@model_validator(mode="after")
def validate_list_length(self) -> Self:
# Check if either attribute is set and add to active planes
if self.trackingtype == 2:
# Tilt angle from horizontal plane is ignored for two-axis tracking.
if self.surface_azimuth is None:
raise ValueError("If trackingtype is set, azimuth must be set as well.")
elif self.surface_tilt is None or self.surface_azimuth is None:
raise ValueError("surface_tilt and surface_azimuth must be set.")
return self
@field_validator("mountingplace")
def validate_mountingplace(cls, mountingplace: Optional[str]) -> Optional[str]:
if mountingplace is not None and mountingplace not in ["free", "building"]:
raise ValueError(f"Invalid mountingplace: {mountingplace}")
return mountingplace
@field_validator("pvtechchoice")
def validate_pvtechchoice(cls, pvtechchoice: Optional[str]) -> Optional[str]:
if pvtechchoice is not None and pvtechchoice not in ["crystSi", "CIS", "CdTe", "Unknown"]:
raise ValueError(f"Invalid pvtechchoice: {pvtechchoice}")
return pvtechchoice
class PVForecastCommonSettings(SettingsBaseModel): class PVForecastCommonSettings(SettingsBaseModel):
"""PV Forecast Configuration.""" """PV Forecast Configuration."""
@ -24,539 +121,109 @@ class PVForecastCommonSettings(SettingsBaseModel):
description="PVForecast provider id of provider to be used.", description="PVForecast provider id of provider to be used.",
examples=["PVForecastAkkudoktor"], examples=["PVForecastAkkudoktor"],
) )
# pvforecast0_latitude: Optional[float] = Field(default=None, description="Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°)")
# Plane 0 planes: Optional[list[PVForecastPlaneSetting]] = Field(
pvforecast0_surface_tilt: Optional[float] = Field(
default=None, default=None,
description="Tilt angle from horizontal plane. Ignored for two-axis tracking.", description="Plane configuration.",
examples=[10.0], examples=[get_model_structure_from_examples(PVForecastPlaneSetting, True)],
)
pvforecast0_surface_azimuth: Optional[float] = Field(
default=None,
description="Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).",
examples=[10.0],
)
pvforecast0_userhorizon: Optional[List[float]] = Field(
default=None,
description="Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.",
examples=[[10.0, 20.0, 30.0]],
)
pvforecast0_peakpower: Optional[float] = Field(
default=None, description="Nominal power of PV system in kW.", examples=[5.0]
)
pvforecast0_pvtechchoice: Optional[str] = Field(
default="crystSi", description="PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'."
)
pvforecast0_mountingplace: Optional[str] = Field(
default="free",
description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.",
)
pvforecast0_loss: Optional[float] = Field(
default=14.0, description="Sum of PV system losses in percent"
)
pvforecast0_trackingtype: Optional[int] = Field(
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.",
examples=[0, 1, 2, 3, 4, 5],
)
pvforecast0_optimal_surface_tilt: Optional[bool] = Field(
default=False,
description="Calculate the optimum tilt angle. Ignored for two-axis tracking.",
examples=[False],
)
pvforecast0_optimalangles: Optional[bool] = Field(
default=False,
description="Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking.",
examples=[False],
)
pvforecast0_albedo: Optional[float] = Field(
default=None,
description="Proportion of the light hitting the ground that it reflects back.",
examples=[None],
)
pvforecast0_module_model: Optional[str] = Field(
default=None, description="Model of the PV modules of this plane.", examples=[None]
)
pvforecast0_inverter_model: Optional[str] = Field(
default=None, description="Model of the inverter of this plane.", examples=[None]
)
pvforecast0_inverter_paco: Optional[int] = Field(
default=None, description="AC power rating of the inverter. [W]", examples=[6000]
)
pvforecast0_modules_per_string: Optional[int] = Field(
default=None,
description="Number of the PV modules of the strings of this plane.",
examples=[20],
)
pvforecast0_strings_per_inverter: Optional[int] = Field(
default=None,
description="Number of the strings of the inverter of this plane.",
examples=[2],
)
# Plane 1
pvforecast1_surface_tilt: Optional[float] = Field(
default=None,
description="Tilt angle from horizontal plane. Ignored for two-axis tracking.",
examples=[20.0],
)
pvforecast1_surface_azimuth: Optional[float] = Field(
default=None,
description="Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).",
examples=[20.0],
)
pvforecast1_userhorizon: Optional[List[float]] = Field(
default=None,
description="Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.",
examples=[[5.0, 15.0, 25.0]],
)
pvforecast1_peakpower: Optional[float] = Field(
default=None, description="Nominal power of PV system in kW.", examples=[3.5]
)
pvforecast1_pvtechchoice: Optional[str] = Field(
default="crystSi", description="PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'."
)
pvforecast1_mountingplace: Optional[str] = Field(
default="free",
description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.",
)
pvforecast1_loss: Optional[float] = Field(
default=14.0, description="Sum of PV system losses in percent"
)
pvforecast1_trackingtype: Optional[int] = Field(
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.",
examples=[None],
)
pvforecast1_optimal_surface_tilt: Optional[bool] = Field(
default=False,
description="Calculate the optimum tilt angle. Ignored for two-axis tracking.",
examples=[False],
)
pvforecast1_optimalangles: Optional[bool] = Field(
default=False,
description="Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking.",
examples=[False],
)
pvforecast1_albedo: Optional[float] = Field(
default=None,
description="Proportion of the light hitting the ground that it reflects back.",
examples=[None],
)
pvforecast1_module_model: Optional[str] = Field(
default=None, description="Model of the PV modules of this plane.", examples=[None]
)
pvforecast1_inverter_model: Optional[str] = Field(
default=None, description="Model of the inverter of this plane.", examples=[None]
)
pvforecast1_inverter_paco: Optional[int] = Field(
default=None, description="AC power rating of the inverter. [W]", examples=[4000]
)
pvforecast1_modules_per_string: Optional[int] = Field(
default=None,
description="Number of the PV modules of the strings of this plane.",
examples=[20],
)
pvforecast1_strings_per_inverter: Optional[int] = Field(
default=None,
description="Number of the strings of the inverter of this plane.",
examples=[2],
)
# Plane 2
pvforecast2_surface_tilt: Optional[float] = Field(
default=None,
description="Tilt angle from horizontal plane. Ignored for two-axis tracking.",
examples=[None],
)
pvforecast2_surface_azimuth: Optional[float] = Field(
default=None,
description="Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).",
examples=[None],
)
pvforecast2_userhorizon: Optional[List[float]] = Field(
default=None,
description="Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.",
examples=[None],
)
pvforecast2_peakpower: Optional[float] = Field(
default=None, description="Nominal power of PV system in kW.", examples=[None]
)
pvforecast2_pvtechchoice: Optional[str] = Field(
default="crystSi",
description="PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'.",
examples=[None],
)
pvforecast2_mountingplace: Optional[str] = Field(
default="free",
description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.",
examples=[None],
)
pvforecast2_loss: Optional[float] = Field(
default=14.0, description="Sum of PV system losses in percent", examples=[None]
)
pvforecast2_trackingtype: Optional[int] = Field(
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.",
examples=[None],
)
pvforecast2_optimal_surface_tilt: Optional[bool] = Field(
default=False,
description="Calculate the optimum tilt angle. Ignored for two-axis tracking.",
examples=[None],
)
pvforecast2_optimalangles: Optional[bool] = Field(
default=False,
description="Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking.",
examples=[None],
)
pvforecast2_albedo: Optional[float] = Field(
default=None,
description="Proportion of the light hitting the ground that it reflects back.",
examples=[None],
)
pvforecast2_module_model: Optional[str] = Field(
default=None, description="Model of the PV modules of this plane.", examples=[None]
)
pvforecast2_inverter_model: Optional[str] = Field(
default=None, description="Model of the inverter of this plane.", examples=[None]
)
pvforecast2_inverter_paco: Optional[int] = Field(
default=None, description="AC power rating of the inverter. [W]", examples=[None]
)
pvforecast2_modules_per_string: Optional[int] = Field(
default=None,
description="Number of the PV modules of the strings of this plane.",
examples=[None],
)
pvforecast2_strings_per_inverter: Optional[int] = Field(
default=None,
description="Number of the strings of the inverter of this plane.",
examples=[None],
)
# Plane 3
pvforecast3_surface_tilt: Optional[float] = Field(
default=None,
description="Tilt angle from horizontal plane. Ignored for two-axis tracking.",
examples=[None],
)
pvforecast3_surface_azimuth: Optional[float] = Field(
default=None,
description="Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).",
examples=[None],
)
pvforecast3_userhorizon: Optional[List[float]] = Field(
default=None,
description="Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.",
examples=[None],
)
pvforecast3_peakpower: Optional[float] = Field(
default=None, description="Nominal power of PV system in kW.", examples=[None]
)
pvforecast3_pvtechchoice: Optional[str] = Field(
default="crystSi",
description="PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'.",
examples=[None],
)
pvforecast3_mountingplace: Optional[str] = Field(
default="free",
description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.",
examples=[None],
)
pvforecast3_loss: Optional[float] = Field(
default=14.0, description="Sum of PV system losses in percent", examples=[None]
)
pvforecast3_trackingtype: Optional[int] = Field(
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.",
examples=[None],
)
pvforecast3_optimal_surface_tilt: Optional[bool] = Field(
default=False,
description="Calculate the optimum tilt angle. Ignored for two-axis tracking.",
examples=[None],
)
pvforecast3_optimalangles: Optional[bool] = Field(
default=False,
description="Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking.",
examples=[None],
)
pvforecast3_albedo: Optional[float] = Field(
default=None,
description="Proportion of the light hitting the ground that it reflects back.",
examples=[None],
)
pvforecast3_module_model: Optional[str] = Field(
default=None, description="Model of the PV modules of this plane.", examples=[None]
)
pvforecast3_inverter_model: Optional[str] = Field(
default=None, description="Model of the inverter of this plane.", examples=[None]
)
pvforecast3_inverter_paco: Optional[int] = Field(
default=None, description="AC power rating of the inverter. [W]", examples=[None]
)
pvforecast3_modules_per_string: Optional[int] = Field(
default=None,
description="Number of the PV modules of the strings of this plane.",
examples=[None],
)
pvforecast3_strings_per_inverter: Optional[int] = Field(
default=None,
description="Number of the strings of the inverter of this plane.",
examples=[None],
)
# Plane 4
pvforecast4_surface_tilt: Optional[float] = Field(
default=None,
description="Tilt angle from horizontal plane. Ignored for two-axis tracking.",
examples=[None],
)
pvforecast4_surface_azimuth: Optional[float] = Field(
default=None,
description="Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).",
examples=[None],
)
pvforecast4_userhorizon: Optional[List[float]] = Field(
default=None,
description="Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.",
examples=[None],
)
pvforecast4_peakpower: Optional[float] = Field(
default=None, description="Nominal power of PV system in kW.", examples=[None]
)
pvforecast4_pvtechchoice: Optional[str] = Field(
default="crystSi",
description="PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'.",
examples=[None],
)
pvforecast4_mountingplace: Optional[str] = Field(
default="free",
description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.",
examples=[None],
)
pvforecast4_loss: Optional[float] = Field(
default=14.0, description="Sum of PV system losses in percent", examples=[None]
)
pvforecast4_trackingtype: Optional[int] = Field(
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.",
examples=[None],
)
pvforecast4_optimal_surface_tilt: Optional[bool] = Field(
default=False,
description="Calculate the optimum tilt angle. Ignored for two-axis tracking.",
examples=[None],
)
pvforecast4_optimalangles: Optional[bool] = Field(
default=False,
description="Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking.",
examples=[None],
)
pvforecast4_albedo: Optional[float] = Field(
default=None,
description="Proportion of the light hitting the ground that it reflects back.",
examples=[None],
)
pvforecast4_module_model: Optional[str] = Field(
default=None, description="Model of the PV modules of this plane.", examples=[None]
)
pvforecast4_inverter_model: Optional[str] = Field(
default=None, description="Model of the inverter of this plane.", examples=[None]
)
pvforecast4_inverter_paco: Optional[int] = Field(
default=None, description="AC power rating of the inverter. [W]", examples=[None]
)
pvforecast4_modules_per_string: Optional[int] = Field(
default=None,
description="Number of the PV modules of the strings of this plane.",
examples=[None],
)
pvforecast4_strings_per_inverter: Optional[int] = Field(
default=None,
description="Number of the strings of the inverter of this plane.",
examples=[None],
)
# Plane 5
pvforecast5_surface_tilt: Optional[float] = Field(
default=None,
description="Tilt angle from horizontal plane. Ignored for two-axis tracking.",
examples=[None],
)
pvforecast5_surface_azimuth: Optional[float] = Field(
default=None,
description="Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).",
examples=[None],
)
pvforecast5_userhorizon: Optional[List[float]] = Field(
default=None,
description="Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.",
examples=[None],
)
pvforecast5_peakpower: Optional[float] = Field(
default=None, description="Nominal power of PV system in kW.", examples=[None]
)
pvforecast5_pvtechchoice: Optional[str] = Field(
default="crystSi",
description="PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'.",
examples=[None],
)
pvforecast5_mountingplace: Optional[str] = Field(
default="free",
description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.",
examples=[None],
)
pvforecast5_loss: Optional[float] = Field(
default=14.0, description="Sum of PV system losses in percent", examples=[None]
)
pvforecast5_trackingtype: Optional[int] = Field(
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.",
examples=[None],
)
pvforecast5_optimal_surface_tilt: Optional[bool] = Field(
default=False,
description="Calculate the optimum tilt angle. Ignored for two-axis tracking.",
examples=[None],
)
pvforecast5_optimalangles: Optional[bool] = Field(
default=False,
description="Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking.",
examples=[None],
)
pvforecast5_albedo: Optional[float] = Field(
default=None,
description="Proportion of the light hitting the ground that it reflects back.",
examples=[None],
)
pvforecast5_module_model: Optional[str] = Field(
default=None, description="Model of the PV modules of this plane.", examples=[None]
)
pvforecast5_inverter_model: Optional[str] = Field(
default=None, description="Model of the inverter of this plane.", examples=[None]
)
pvforecast5_inverter_paco: Optional[int] = Field(
default=None, description="AC power rating of the inverter. [W]", examples=[None]
)
pvforecast5_modules_per_string: Optional[int] = Field(
default=None,
description="Number of the PV modules of the strings of this plane.",
examples=[None],
)
pvforecast5_strings_per_inverter: Optional[int] = Field(
default=None,
description="Number of the strings of the inverter of this plane.",
examples=[None],
) )
pvforecast_max_planes: ClassVar[int] = 6 # Maximum number of planes that can be set max_planes: ClassVar[int] = 6 # Maximum number of planes that can be set
@field_validator("planes")
def validate_planes(
cls, planes: Optional[list[PVForecastPlaneSetting]]
) -> Optional[list[PVForecastPlaneSetting]]:
if planes is not None and len(planes) > cls.max_planes:
raise ValueError(f"Maximum number of supported planes: {cls.max_planes}.")
return planes
provider_settings: Optional[PVForecastImportCommonSettings] = Field( provider_settings: Optional[PVForecastImportCommonSettings] = Field(
default=None, description="Provider settings", examples=[None] default=None, description="Provider settings", examples=[None]
) )
# Computed fields ## Computed fields
@computed_field # type: ignore[prop-decorator] @computed_field # type: ignore[prop-decorator]
@property @property
def pvforecast_planes(self) -> List[str]: def planes_peakpower(self) -> List[float]:
"""Compute a list of active planes."""
active_planes = []
# Loop through pvforecast0 to pvforecast4
for i in range(self.pvforecast_max_planes):
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, 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
@computed_field # type: ignore[prop-decorator]
@property
def pvforecast_planes_peakpower(self) -> List[float]:
"""Compute a list of the peak power per active planes.""" """Compute a list of the peak power per active planes."""
planes_peakpower = [] planes_peakpower = []
for plane in self.pvforecast_planes: if self.planes:
peakpower_attr = f"{plane}_peakpower" for plane in self.planes:
peakpower = getattr(self, peakpower_attr, None) peakpower = plane.peakpower
if peakpower is None: if peakpower is None:
# TODO calculate peak power from modules/strings # TODO calculate peak power from modules/strings
planes_peakpower.append(float(5000)) planes_peakpower.append(float(5000))
else: else:
planes_peakpower.append(float(peakpower)) planes_peakpower.append(float(peakpower))
return planes_peakpower return planes_peakpower
@computed_field # type: ignore[prop-decorator] @computed_field # type: ignore[prop-decorator]
@property @property
def pvforecast_planes_azimuth(self) -> List[float]: def planes_azimuth(self) -> List[float]:
"""Compute a list of the azimuths per active planes.""" """Compute a list of the azimuths per active planes."""
planes_azimuth = [] planes_azimuth = []
for plane in self.pvforecast_planes: if self.planes:
azimuth_attr = f"{plane}_surface_azimuth" for plane in self.planes:
azimuth = getattr(self, azimuth_attr, None) azimuth = plane.surface_azimuth
if azimuth is None: if azimuth is None:
# TODO Use default # TODO Use default
planes_azimuth.append(float(180)) planes_azimuth.append(float(180))
else: else:
planes_azimuth.append(float(azimuth)) planes_azimuth.append(float(azimuth))
return planes_azimuth return planes_azimuth
@computed_field # type: ignore[prop-decorator] @computed_field # type: ignore[prop-decorator]
@property @property
def pvforecast_planes_tilt(self) -> List[float]: def planes_tilt(self) -> List[float]:
"""Compute a list of the tilts per active planes.""" """Compute a list of the tilts per active planes."""
planes_tilt = [] planes_tilt = []
for plane in self.pvforecast_planes: if self.planes:
tilt_attr = f"{plane}_surface_tilt" for plane in self.planes:
tilt = getattr(self, tilt_attr, None) tilt = plane.surface_tilt
if tilt is None: if tilt is None:
# TODO Use default # TODO Use default
planes_tilt.append(float(30)) planes_tilt.append(float(30))
else: else:
planes_tilt.append(float(tilt)) planes_tilt.append(float(tilt))
return planes_tilt return planes_tilt
@computed_field # type: ignore[prop-decorator] @computed_field # type: ignore[prop-decorator]
@property @property
def pvforecast_planes_userhorizon(self) -> Any: def planes_userhorizon(self) -> Any:
"""Compute a list of the user horizon per active planes.""" """Compute a list of the user horizon per active planes."""
planes_userhorizon = [] planes_userhorizon = []
for plane in self.pvforecast_planes: if self.planes:
userhorizon_attr = f"{plane}_userhorizon" for plane in self.planes:
userhorizon = getattr(self, userhorizon_attr, None) userhorizon = plane.userhorizon
if userhorizon is None: if userhorizon is None:
# TODO Use default # TODO Use default
planes_userhorizon.append([float(0), float(0)]) planes_userhorizon.append([float(0), float(0)])
else: else:
planes_userhorizon.append(userhorizon) planes_userhorizon.append(userhorizon)
return planes_userhorizon return planes_userhorizon
@computed_field # type: ignore[prop-decorator] @computed_field # type: ignore[prop-decorator]
@property @property
def pvforecast_planes_inverter_paco(self) -> Any: def planes_inverter_paco(self) -> Any:
"""Compute a list of the maximum power rating of the inverter per active planes.""" """Compute a list of the maximum power rating of the inverter per active planes."""
planes_inverter_paco = [] planes_inverter_paco = []
for plane in self.pvforecast_planes: if self.planes:
inverter_paco_attr = f"{plane}_inverter_paco" for plane in self.planes:
inverter_paco = getattr(self, inverter_paco_attr, None) inverter_paco = plane.inverter_paco
if inverter_paco is None: if inverter_paco is None:
# TODO Use default - no clipping # TODO Use default - no clipping
planes_inverter_paco.append(25000.0) planes_inverter_paco.append(25000.0)
else: else:
planes_inverter_paco.append(float(inverter_paco)) planes_inverter_paco.append(float(inverter_paco))
return planes_inverter_paco return planes_inverter_paco

View File

@ -22,16 +22,22 @@ Example:
}, },
"pvforecast": { "pvforecast": {
"provider": "PVForecastAkkudoktor", "provider": "PVForecastAkkudoktor",
"pvforecast0_peakpower": 5.0, "planes": [
"pvforecast0_surface_azimuth": -10, {
"pvforecast0_surface_tilt": 7, "peakpower": 5.0,
"pvforecast0_userhorizon": [20, 27, 22, 20], "surface_azimuth": -10,
"pvforecast0_inverter_paco": 10000, "surface_tilt": 7,
"pvforecast1_peakpower": 4.8, "userhorizon": [20, 27, 22, 20],
"pvforecast1_surface_azimuth": -90, "inverter_paco": 10000,
"pvforecast1_surface_tilt": 7, },
"pvforecast1_userhorizon": [30, 30, 30, 50], {
"pvforecast1_inverter_paco": 10000, "peakpower": 4.8,
"surface_azimuth": -90,
"surface_tilt": 7,
"userhorizon": [30, 30, 30, 50],
"inverter_paco": 10000,
}
]
} }
} }
@ -211,19 +217,15 @@ class PVForecastAkkudoktor(PVForecastProvider):
f"lon={self.config.prediction.longitude}", f"lon={self.config.prediction.longitude}",
] ]
for i in range(len(self.config.pvforecast.pvforecast_planes)): for i in range(len(self.config.pvforecast.planes)):
query_params.append(f"power={int(self.config.pvforecast.planes_peakpower[i] * 1000)}")
query_params.append(f"azimuth={int(self.config.pvforecast.planes_azimuth[i])}")
query_params.append(f"tilt={int(self.config.pvforecast.planes_tilt[i])}")
query_params.append( query_params.append(
f"power={int(self.config.pvforecast.pvforecast_planes_peakpower[i] * 1000)}" f"powerInverter={int(self.config.pvforecast.planes_inverter_paco[i])}"
)
query_params.append(
f"azimuth={int(self.config.pvforecast.pvforecast_planes_azimuth[i])}"
)
query_params.append(f"tilt={int(self.config.pvforecast.pvforecast_planes_tilt[i])}")
query_params.append(
f"powerInverter={int(self.config.pvforecast.pvforecast_planes_inverter_paco[i])}"
) )
horizon_values = ",".join( horizon_values = ",".join(
str(int(h)) for h in self.config.pvforecast.pvforecast_planes_userhorizon[i] str(int(h)) for h in self.config.pvforecast.planes_userhorizon[i]
) )
query_params.append(f"horizont={horizon_values}") query_params.append(f"horizont={horizon_values}")
@ -273,7 +275,7 @@ class PVForecastAkkudoktor(PVForecastProvider):
`PVForecastAkkudoktorDataRecord`. `PVForecastAkkudoktorDataRecord`.
""" """
# Assure we have something to request PV power for. # Assure we have something to request PV power for.
if not self.config.pvforecast.pvforecast_planes: if not self.config.pvforecast.planes:
# No planes for PV # No planes for PV
error_msg = "Requested PV forecast, but no planes configured." error_msg = "Requested PV forecast, but no planes configured."
logger.error(f"Configuration error: {error_msg}") logger.error(f"Configuration error: {error_msg}")
@ -381,26 +383,36 @@ if __name__ == "__main__":
}, },
"pvforecast": { "pvforecast": {
"provider": "PVForecastAkkudoktor", "provider": "PVForecastAkkudoktor",
"pvforecast0_peakpower": 5.0, "planes": [
"pvforecast0_surface_azimuth": -10, {
"pvforecast0_surface_tilt": 7, "peakpower": 5.0,
"pvforecast0_userhorizon": [20, 27, 22, 20], "surface_azimuth": -10,
"pvforecast0_inverter_paco": 10000, "surface_tilt": 7,
"pvforecast1_peakpower": 4.8, "userhorizon": [20, 27, 22, 20],
"pvforecast1_surface_azimuth": -90, "inverter_paco": 10000,
"pvforecast1_surface_tilt": 7, },
"pvforecast1_userhorizon": [30, 30, 30, 50], {
"pvforecast1_inverter_paco": 10000, "peakpower": 4.8,
"pvforecast2_peakpower": 1.4, "surface_azimuth": -90,
"pvforecast2_surface_azimuth": -40, "surface_tilt": 7,
"pvforecast2_surface_tilt": 60, "userhorizon": [30, 30, 30, 50],
"pvforecast2_userhorizon": [60, 30, 0, 30], "inverter_paco": 10000,
"pvforecast2_inverter_paco": 2000, },
"pvforecast3_peakpower": 1.6, {
"pvforecast3_surface_azimuth": 5, "peakpower": 1.4,
"pvforecast3_surface_tilt": 45, "surface_azimuth": -40,
"pvforecast3_userhorizon": [45, 25, 30, 60], "surface_tilt": 60,
"pvforecast3_inverter_paco": 1400, "userhorizon": [60, 30, 0, 30],
"inverter_paco": 2000,
},
{
"peakpower": 1.6,
"surface_azimuth": 5,
"surface_tilt": 45,
"userhorizon": [45, 25, 30, 60],
"inverter_paco": 1400,
},
],
}, },
} }

View File

@ -22,24 +22,22 @@ logger = get_logger(__name__)
class PVForecastImportCommonSettings(SettingsBaseModel): class PVForecastImportCommonSettings(SettingsBaseModel):
"""Common settings for pvforecast data import from file or JSON string.""" """Common settings for pvforecast data import from file or JSON string."""
pvforecastimport_file_path: Optional[Union[str, Path]] = Field( import_file_path: Optional[Union[str, Path]] = Field(
default=None, default=None,
description="Path to the file to import PV forecast data from.", description="Path to the file to import PV forecast data from.",
examples=[None, "/path/to/pvforecast.json"], examples=[None, "/path/to/pvforecast.json"],
) )
pvforecastimport_json: Optional[str] = Field( import_json: Optional[str] = Field(
default=None, default=None,
description="JSON string, dictionary of PV forecast value lists.", description="JSON string, dictionary of PV forecast value lists.",
examples=['{"pvforecast_ac_power": [0, 8.05, 352.91]}'], examples=['{"pvforecast_ac_power": [0, 8.05, 352.91]}'],
) )
# Validators # Validators
@field_validator("pvforecastimport_file_path", mode="after") @field_validator("import_file_path", mode="after")
@classmethod @classmethod
def validate_pvforecastimport_file_path( def validate_import_file_path(cls, value: Optional[Union[str, Path]]) -> Optional[Path]:
cls, value: Optional[Union[str, Path]]
) -> Optional[Path]:
if value is None: if value is None:
return None return None
if isinstance(value, str): if isinstance(value, str):
@ -65,13 +63,13 @@ class PVForecastImport(PVForecastProvider, PredictionImportProvider):
return "PVForecastImport" return "PVForecastImport"
def _update_data(self, force_update: Optional[bool] = False) -> None: def _update_data(self, force_update: Optional[bool] = False) -> None:
if self.config.pvforecast.provider_settings.pvforecastimport_file_path is not None: if self.config.pvforecast.provider_settings.import_file_path is not None:
self.import_from_file( self.import_from_file(
self.config.pvforecast.provider_settings.pvforecastimport_file_path, self.config.pvforecast.provider_settings.import_file_path,
key_prefix="pvforecast", key_prefix="pvforecast",
) )
if self.config.pvforecast.provider_settings.pvforecastimport_json is not None: if self.config.pvforecast.provider_settings.import_json is not None:
self.import_from_json( self.import_from_json(
self.config.pvforecast.provider_settings.pvforecastimport_json, self.config.pvforecast.provider_settings.import_json,
key_prefix="pvforecast", key_prefix="pvforecast",
) )

View File

@ -0,0 +1,42 @@
from typing import Any
from pydantic.fields import FieldInfo
from akkudoktoreos.core.pydantic import PydanticBaseModel
def get_example_or_default(field_name: str, field_info: FieldInfo, example_ix: int) -> Any:
"""Generate a default value for a field, considering constraints."""
if field_info.examples is not None:
try:
return field_info.examples[example_ix]
except IndexError:
return field_info.examples[-1]
if field_info.default is not None:
return field_info.default
raise NotImplementedError(f"No default or example provided '{field_name}': {field_info}")
def get_model_structure_from_examples(
model_class: type[PydanticBaseModel], multiple: bool
) -> list[dict[str, Any]]:
"""Create a model instance with default or example values, respecting constraints."""
example_max_length = 1
# Get first field with examples (non-default) to get example_max_length
if multiple:
for _, field_info in model_class.model_fields.items():
if field_info.examples is not None:
example_max_length = len(field_info.examples)
break
example_data: list[dict[str, Any]] = [{} for _ in range(example_max_length)]
for field_name, field_info in model_class.model_fields.items():
for example_ix in range(example_max_length):
example_data[example_ix][field_name] = get_example_or_default(
field_name, field_info, example_ix
)
return example_data

View File

@ -1,82 +1,75 @@
import pytest import pytest
from akkudoktoreos.prediction.pvforecast import PVForecastCommonSettings from akkudoktoreos.prediction.pvforecast import (
PVForecastCommonSettings,
PVForecastPlaneSetting,
)
@pytest.fixture @pytest.fixture
def settings(): def settings():
"""Fixture that creates an empty PVForecastSettings.""" """Fixture that creates an empty PVForecastSettings."""
settings = PVForecastCommonSettings() settings = PVForecastCommonSettings()
assert settings.planes is None
# 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 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): def test_planes_peakpower_computation(settings):
"""Test computation of peak power for active planes.""" """Test computation of peak power for active planes."""
settings.pvforecast1_surface_tilt = 10.0 settings.planes = [
settings.pvforecast1_surface_azimuth = 10.0 PVForecastPlaneSetting(
settings.pvforecast1_peakpower = 5.0 surface_tilt=10.0,
settings.pvforecast2_surface_tilt = 20.0 surface_azimuth=10.0,
settings.pvforecast2_surface_azimuth = 20.0 peakpower=5.0,
settings.pvforecast2_peakpower = 3.5 ),
settings.pvforecast3_surface_tilt = 30.0 PVForecastPlaneSetting(
settings.pvforecast3_surface_azimuth = 30.0 surface_tilt=20.0,
settings.pvforecast3_modules_per_string = 20 # Should use default 5000W surface_azimuth=20.0,
peakpower=3.5,
),
PVForecastPlaneSetting(
surface_tilt=30.0,
surface_azimuth=30.0,
modules_per_string=20, # Should use default 5000W
),
]
expected_peakpower = [5.0, 3.5, 5000.0] expected_peakpower = [5.0, 3.5, 5000.0]
assert settings.pvforecast_planes_peakpower == expected_peakpower assert settings.planes_peakpower == expected_peakpower
def test_planes_azimuth_computation(settings): def test_planes_azimuth_computation(settings):
"""Test computation of azimuth values for active planes.""" """Test computation of azimuth values for active planes."""
settings.pvforecast1_surface_tilt = 10.0 settings.planes = [
settings.pvforecast1_surface_azimuth = 10.0 PVForecastPlaneSetting(
settings.pvforecast2_surface_tilt = 20.0 surface_tilt=10.0,
settings.pvforecast2_surface_azimuth = 20.0 surface_azimuth=10.0,
),
PVForecastPlaneSetting(
surface_tilt=20.0,
surface_azimuth=20.0,
),
]
expected_azimuths = [10.0, 20.0] expected_azimuths = [10.0, 20.0]
assert settings.pvforecast_planes_azimuth == expected_azimuths assert settings.planes_azimuth == expected_azimuths
def test_planes_tilt_computation(settings): def test_planes_tilt_computation(settings):
"""Test computation of tilt values for active planes.""" """Test computation of tilt values for active planes."""
settings.pvforecast1_surface_tilt = 10.0 settings.planes = [
settings.pvforecast1_surface_azimuth = 10.0 PVForecastPlaneSetting(
settings.pvforecast2_surface_tilt = 20.0 surface_tilt=10.0,
settings.pvforecast2_surface_azimuth = 20.0 surface_azimuth=10.0,
),
PVForecastPlaneSetting(
surface_tilt=20.0,
surface_azimuth=20.0,
),
]
expected_tilts = [10.0, 20.0] expected_tilts = [10.0, 20.0]
assert settings.pvforecast_planes_tilt == expected_tilts assert settings.planes_tilt == expected_tilts
def test_planes_userhorizon_computation(settings): def test_planes_userhorizon_computation(settings):
@ -84,116 +77,84 @@ def test_planes_userhorizon_computation(settings):
horizon1 = [10.0, 20.0, 30.0] horizon1 = [10.0, 20.0, 30.0]
horizon2 = [5.0, 15.0, 25.0] horizon2 = [5.0, 15.0, 25.0]
settings.pvforecast1_surface_tilt = 10.0 settings.planes = [
settings.pvforecast1_surface_azimuth = 10.0 PVForecastPlaneSetting(
settings.pvforecast1_userhorizon = horizon1 surface_tilt=10.0,
settings.pvforecast2_surface_tilt = 20.0 surface_azimuth=10.0,
settings.pvforecast2_surface_azimuth = 20.0 userhorizon=horizon1,
settings.pvforecast2_userhorizon = horizon2 ),
PVForecastPlaneSetting(
surface_tilt=20.0,
surface_azimuth=20.0,
userhorizon=horizon2,
),
]
expected_horizons = [horizon1, horizon2] expected_horizons = [horizon1, horizon2]
assert settings.pvforecast_planes_userhorizon == expected_horizons assert settings.planes_userhorizon == expected_horizons
def test_planes_inverter_paco_computation(settings): def test_planes_inverter_paco_computation(settings):
"""Test computation of inverter power rating for active planes.""" """Test computation of inverter power rating for active planes."""
settings.pvforecast1_surface_tilt = 10.0 settings.planes = [
settings.pvforecast1_surface_azimuth = 10.0 PVForecastPlaneSetting(
settings.pvforecast1_inverter_paco = 6000 surface_tilt=10.0,
settings.pvforecast2_surface_tilt = 20.0 surface_azimuth=10.0,
settings.pvforecast2_surface_azimuth = 20.0 inverter_paco=6000,
settings.pvforecast2_inverter_paco = 4000 ),
PVForecastPlaneSetting(
surface_tilt=20.0,
surface_azimuth=20.0,
inverter_paco=4000,
),
]
expected_paco = [6000, 4000] expected_paco = [6000, 4000]
assert settings.pvforecast_planes_inverter_paco == expected_paco assert settings.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): def test_mixed_plane_configuration(settings):
"""Test mixed configuration with some planes having peak power and others having modules.""" """Test mixed configuration with some planes having peak power and others having modules."""
settings.pvforecast1_surface_tilt = 10.0 settings.planes = [
settings.pvforecast1_surface_azimuth = 10.0 PVForecastPlaneSetting(
settings.pvforecast1_peakpower = 5.0 surface_tilt=10.0,
settings.pvforecast2_surface_tilt = 20.0 surface_azimuth=10.0,
settings.pvforecast2_surface_azimuth = 20.0 peakpower=5.0,
settings.pvforecast2_modules_per_string = 20 ),
settings.pvforecast2_strings_per_inverter = 2 PVForecastPlaneSetting(
settings.pvforecast4_surface_tilt = 40.0 surface_tilt=20.0,
settings.pvforecast4_surface_azimuth = 40.0 surface_azimuth=20.0,
settings.pvforecast4_peakpower = 3.0 modules_per_string=20,
strings_per_inverter=2,
),
PVForecastPlaneSetting(
surface_tilt=40.0,
surface_azimuth=40.0,
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 # First plane uses specified peak power, second uses default, third uses specified
assert settings.pvforecast_planes_peakpower == [5.0, 5000.0, 3.0] assert settings.planes_peakpower == [5.0, 5000.0, 3.0]
def test_max_planes_limit(settings): def test_max_planes_limit(settings):
"""Test that the maximum number of planes is enforced.""" """Test that the maximum number of planes is enforced."""
assert settings.pvforecast_max_planes == 6 assert settings.max_planes == 6
# Create settings with more planes than allowed (should only recognize up to max) # Create settings with more planes than allowed (should only recognize up to max)
plane_settings = {} plane_settings = [{"peakpower": 5.0} for _ in range(8)]
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) with pytest.raises(ValueError):
assert len(settings.pvforecast_planes) <= settings.pvforecast_max_planes PVForecastCommonSettings(planes=plane_settings)
def test_optional_parameters_non_zero_plane(settings): def test_invalid_plane_settings():
"""Test that optional parameters can be None for non-zero planes.""" """Test that optional parameters can be None for non-zero planes."""
settings.pvforecast1_peakpower = 5.0 with pytest.raises(ValueError):
settings.pvforecast1_albedo = None PVForecastPlaneSetting(
settings.pvforecast1_module_model = None peakpower=5.0,
settings.pvforecast1_userhorizon = None albedo=None,
module_model=None,
assert settings.pvforecast1_albedo is None userhorizon=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

View File

@ -33,27 +33,36 @@ def sample_settings(config_eos):
}, },
"pvforecast": { "pvforecast": {
"provider": "PVForecastAkkudoktor", "provider": "PVForecastAkkudoktor",
"pvforecast0_peakpower": 5.0, "planes": [
"pvforecast0_surface_azimuth": -10, {
"pvforecast0_surface_tilt": 7, "peakpower": 5.0,
"pvforecast0_userhorizon": [20, 27, 22, 20], "surface_azimuth": -10,
"pvforecast0_inverter_paco": 10000, "surface_tilt": 7,
"pvforecast1_peakpower": 4.8, "userhorizon": [20, 27, 22, 20],
"pvforecast1_surface_azimuth": -90, "inverter_paco": 10000,
"pvforecast1_surface_tilt": 7, },
"pvforecast1_userhorizon": [30, 30, 30, 50], {
"pvforecast1_inverter_paco": 10000, "peakpower": 4.8,
"pvforecast2_peakpower": 1.4, "surface_azimuth": -90,
"pvforecast2_surface_azimuth": -40, "surface_tilt": 7,
"pvforecast2_surface_tilt": 60, "userhorizon": [30, 30, 30, 50],
"pvforecast2_userhorizon": [60, 30, 0, 30], "inverter_paco": 10000,
"pvforecast2_inverter_paco": 2000, },
"pvforecast3_peakpower": 1.6, {
"pvforecast3_surface_azimuth": 5, "peakpower": 1.4,
"pvforecast3_surface_tilt": 45, "surface_azimuth": -40,
"pvforecast3_userhorizon": [45, 25, 30, 60], "surface_tilt": 60,
"pvforecast3_inverter_paco": 1400, "userhorizon": [60, 30, 0, 30],
"pvforecast4_peakpower": None, "inverter_paco": 2000,
},
{
"peakpower": 1.6,
"surface_azimuth": 5,
"surface_tilt": 45,
"userhorizon": [45, 25, 30, 60],
"inverter_paco": 1400,
},
],
}, },
} }

View File

@ -19,8 +19,8 @@ def provider(sample_import_1_json, config_eos):
"pvforecast": { "pvforecast": {
"provider": "PVForecastImport", "provider": "PVForecastImport",
"provider_settings": { "provider_settings": {
"pvforecastimport_file_path": str(FILE_TESTDATA_PVFORECASTIMPORT_1_JSON), "import_file_path": str(FILE_TESTDATA_PVFORECASTIMPORT_1_JSON),
"pvforecastimport_json": json.dumps(sample_import_1_json), "import_json": json.dumps(sample_import_1_json),
}, },
} }
} }
@ -55,7 +55,7 @@ def test_invalid_provider(provider, config_eos):
"pvforecast": { "pvforecast": {
"provider": "<invalid>", "provider": "<invalid>",
"provider_settings": { "provider_settings": {
"pvforecastimport_file_path": str(FILE_TESTDATA_PVFORECASTIMPORT_1_JSON), "import_file_path": str(FILE_TESTDATA_PVFORECASTIMPORT_1_JSON),
}, },
} }
} }
@ -86,11 +86,11 @@ def test_import(provider, sample_import_1_json, start_datetime, from_file, confi
ems_eos = get_ems() ems_eos = get_ems()
ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin")) ems_eos.set_start_datetime(to_datetime(start_datetime, in_timezone="Europe/Berlin"))
if from_file: if from_file:
config_eos.pvforecast.provider_settings.pvforecastimport_json = None config_eos.pvforecast.provider_settings.import_json = None
assert config_eos.pvforecast.provider_settings.pvforecastimport_json is None assert config_eos.pvforecast.provider_settings.import_json is None
else: else:
config_eos.pvforecast.provider_settings.pvforecastimport_file_path = None config_eos.pvforecast.provider_settings.import_file_path = None
assert config_eos.pvforecast.provider_settings.pvforecastimport_file_path is None assert config_eos.pvforecast.provider_settings.import_file_path is None
provider.clear() provider.clear()
# Call the method # Call the method