mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-08-25 15:01:14 +00:00
PVForecast: planes as nested config (list)
This commit is contained in:
@@ -1,16 +1,113 @@
|
||||
"""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.core.logging import get_logger
|
||||
from akkudoktoreos.prediction.pvforecastimport import PVForecastImportCommonSettings
|
||||
from akkudoktoreos.utils.docs import get_model_structure_from_examples
|
||||
|
||||
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):
|
||||
"""PV Forecast Configuration."""
|
||||
|
||||
@@ -24,539 +121,109 @@ class PVForecastCommonSettings(SettingsBaseModel):
|
||||
description="PVForecast provider id of provider to be used.",
|
||||
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
|
||||
pvforecast0_surface_tilt: Optional[float] = Field(
|
||||
|
||||
planes: Optional[list[PVForecastPlaneSetting]] = Field(
|
||||
default=None,
|
||||
description="Tilt angle from horizontal plane. Ignored for two-axis tracking.",
|
||||
examples=[10.0],
|
||||
)
|
||||
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],
|
||||
description="Plane configuration.",
|
||||
examples=[get_model_structure_from_examples(PVForecastPlaneSetting, True)],
|
||||
)
|
||||
|
||||
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(
|
||||
default=None, description="Provider settings", examples=[None]
|
||||
)
|
||||
|
||||
# Computed fields
|
||||
## Computed fields
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def pvforecast_planes(self) -> List[str]:
|
||||
"""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]:
|
||||
def planes_peakpower(self) -> List[float]:
|
||||
"""Compute a list of the peak power per active planes."""
|
||||
planes_peakpower = []
|
||||
|
||||
for plane in self.pvforecast_planes:
|
||||
peakpower_attr = f"{plane}_peakpower"
|
||||
peakpower = getattr(self, peakpower_attr, None)
|
||||
if peakpower is None:
|
||||
# TODO calculate peak power from modules/strings
|
||||
planes_peakpower.append(float(5000))
|
||||
else:
|
||||
planes_peakpower.append(float(peakpower))
|
||||
if self.planes:
|
||||
for plane in self.planes:
|
||||
peakpower = plane.peakpower
|
||||
if peakpower is None:
|
||||
# TODO calculate peak power from modules/strings
|
||||
planes_peakpower.append(float(5000))
|
||||
else:
|
||||
planes_peakpower.append(float(peakpower))
|
||||
|
||||
return planes_peakpower
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def pvforecast_planes_azimuth(self) -> List[float]:
|
||||
def planes_azimuth(self) -> List[float]:
|
||||
"""Compute a list of the azimuths per active planes."""
|
||||
planes_azimuth = []
|
||||
|
||||
for plane in self.pvforecast_planes:
|
||||
azimuth_attr = f"{plane}_surface_azimuth"
|
||||
azimuth = getattr(self, azimuth_attr, None)
|
||||
if azimuth is None:
|
||||
# TODO Use default
|
||||
planes_azimuth.append(float(180))
|
||||
else:
|
||||
planes_azimuth.append(float(azimuth))
|
||||
if self.planes:
|
||||
for plane in self.planes:
|
||||
azimuth = plane.surface_azimuth
|
||||
if azimuth is None:
|
||||
# TODO Use default
|
||||
planes_azimuth.append(float(180))
|
||||
else:
|
||||
planes_azimuth.append(float(azimuth))
|
||||
|
||||
return planes_azimuth
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def pvforecast_planes_tilt(self) -> List[float]:
|
||||
def planes_tilt(self) -> List[float]:
|
||||
"""Compute a list of the tilts per active planes."""
|
||||
planes_tilt = []
|
||||
|
||||
for plane in self.pvforecast_planes:
|
||||
tilt_attr = f"{plane}_surface_tilt"
|
||||
tilt = getattr(self, tilt_attr, None)
|
||||
if tilt is None:
|
||||
# TODO Use default
|
||||
planes_tilt.append(float(30))
|
||||
else:
|
||||
planes_tilt.append(float(tilt))
|
||||
if self.planes:
|
||||
for plane in self.planes:
|
||||
tilt = plane.surface_tilt
|
||||
if tilt is None:
|
||||
# TODO Use default
|
||||
planes_tilt.append(float(30))
|
||||
else:
|
||||
planes_tilt.append(float(tilt))
|
||||
|
||||
return planes_tilt
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def pvforecast_planes_userhorizon(self) -> Any:
|
||||
def planes_userhorizon(self) -> Any:
|
||||
"""Compute a list of the user horizon per active planes."""
|
||||
planes_userhorizon = []
|
||||
|
||||
for plane in self.pvforecast_planes:
|
||||
userhorizon_attr = f"{plane}_userhorizon"
|
||||
userhorizon = getattr(self, userhorizon_attr, None)
|
||||
if userhorizon is None:
|
||||
# TODO Use default
|
||||
planes_userhorizon.append([float(0), float(0)])
|
||||
else:
|
||||
planes_userhorizon.append(userhorizon)
|
||||
if self.planes:
|
||||
for plane in self.planes:
|
||||
userhorizon = plane.userhorizon
|
||||
if userhorizon is None:
|
||||
# TODO Use default
|
||||
planes_userhorizon.append([float(0), float(0)])
|
||||
else:
|
||||
planes_userhorizon.append(userhorizon)
|
||||
|
||||
return planes_userhorizon
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@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."""
|
||||
planes_inverter_paco = []
|
||||
|
||||
for plane in self.pvforecast_planes:
|
||||
inverter_paco_attr = f"{plane}_inverter_paco"
|
||||
inverter_paco = getattr(self, inverter_paco_attr, None)
|
||||
if inverter_paco is None:
|
||||
# TODO Use default - no clipping
|
||||
planes_inverter_paco.append(25000.0)
|
||||
else:
|
||||
planes_inverter_paco.append(float(inverter_paco))
|
||||
if self.planes:
|
||||
for plane in self.planes:
|
||||
inverter_paco = plane.inverter_paco
|
||||
if inverter_paco is None:
|
||||
# TODO Use default - no clipping
|
||||
planes_inverter_paco.append(25000.0)
|
||||
else:
|
||||
planes_inverter_paco.append(float(inverter_paco))
|
||||
|
||||
return planes_inverter_paco
|
||||
|
@@ -22,16 +22,22 @@ Example:
|
||||
},
|
||||
"pvforecast": {
|
||||
"provider": "PVForecastAkkudoktor",
|
||||
"pvforecast0_peakpower": 5.0,
|
||||
"pvforecast0_surface_azimuth": -10,
|
||||
"pvforecast0_surface_tilt": 7,
|
||||
"pvforecast0_userhorizon": [20, 27, 22, 20],
|
||||
"pvforecast0_inverter_paco": 10000,
|
||||
"pvforecast1_peakpower": 4.8,
|
||||
"pvforecast1_surface_azimuth": -90,
|
||||
"pvforecast1_surface_tilt": 7,
|
||||
"pvforecast1_userhorizon": [30, 30, 30, 50],
|
||||
"pvforecast1_inverter_paco": 10000,
|
||||
"planes": [
|
||||
{
|
||||
"peakpower": 5.0,
|
||||
"surface_azimuth": -10,
|
||||
"surface_tilt": 7,
|
||||
"userhorizon": [20, 27, 22, 20],
|
||||
"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}",
|
||||
]
|
||||
|
||||
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(
|
||||
f"power={int(self.config.pvforecast.pvforecast_planes_peakpower[i] * 1000)}"
|
||||
)
|
||||
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])}"
|
||||
f"powerInverter={int(self.config.pvforecast.planes_inverter_paco[i])}"
|
||||
)
|
||||
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}")
|
||||
|
||||
@@ -273,7 +275,7 @@ class PVForecastAkkudoktor(PVForecastProvider):
|
||||
`PVForecastAkkudoktorDataRecord`.
|
||||
"""
|
||||
# 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
|
||||
error_msg = "Requested PV forecast, but no planes configured."
|
||||
logger.error(f"Configuration error: {error_msg}")
|
||||
@@ -381,26 +383,36 @@ if __name__ == "__main__":
|
||||
},
|
||||
"pvforecast": {
|
||||
"provider": "PVForecastAkkudoktor",
|
||||
"pvforecast0_peakpower": 5.0,
|
||||
"pvforecast0_surface_azimuth": -10,
|
||||
"pvforecast0_surface_tilt": 7,
|
||||
"pvforecast0_userhorizon": [20, 27, 22, 20],
|
||||
"pvforecast0_inverter_paco": 10000,
|
||||
"pvforecast1_peakpower": 4.8,
|
||||
"pvforecast1_surface_azimuth": -90,
|
||||
"pvforecast1_surface_tilt": 7,
|
||||
"pvforecast1_userhorizon": [30, 30, 30, 50],
|
||||
"pvforecast1_inverter_paco": 10000,
|
||||
"pvforecast2_peakpower": 1.4,
|
||||
"pvforecast2_surface_azimuth": -40,
|
||||
"pvforecast2_surface_tilt": 60,
|
||||
"pvforecast2_userhorizon": [60, 30, 0, 30],
|
||||
"pvforecast2_inverter_paco": 2000,
|
||||
"pvforecast3_peakpower": 1.6,
|
||||
"pvforecast3_surface_azimuth": 5,
|
||||
"pvforecast3_surface_tilt": 45,
|
||||
"pvforecast3_userhorizon": [45, 25, 30, 60],
|
||||
"pvforecast3_inverter_paco": 1400,
|
||||
"planes": [
|
||||
{
|
||||
"peakpower": 5.0,
|
||||
"surface_azimuth": -10,
|
||||
"surface_tilt": 7,
|
||||
"userhorizon": [20, 27, 22, 20],
|
||||
"inverter_paco": 10000,
|
||||
},
|
||||
{
|
||||
"peakpower": 4.8,
|
||||
"surface_azimuth": -90,
|
||||
"surface_tilt": 7,
|
||||
"userhorizon": [30, 30, 30, 50],
|
||||
"inverter_paco": 10000,
|
||||
},
|
||||
{
|
||||
"peakpower": 1.4,
|
||||
"surface_azimuth": -40,
|
||||
"surface_tilt": 60,
|
||||
"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,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
@@ -22,24 +22,22 @@ logger = get_logger(__name__)
|
||||
class PVForecastImportCommonSettings(SettingsBaseModel):
|
||||
"""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,
|
||||
description="Path to the file to import PV forecast data from.",
|
||||
examples=[None, "/path/to/pvforecast.json"],
|
||||
)
|
||||
|
||||
pvforecastimport_json: Optional[str] = Field(
|
||||
import_json: Optional[str] = Field(
|
||||
default=None,
|
||||
description="JSON string, dictionary of PV forecast value lists.",
|
||||
examples=['{"pvforecast_ac_power": [0, 8.05, 352.91]}'],
|
||||
)
|
||||
|
||||
# Validators
|
||||
@field_validator("pvforecastimport_file_path", mode="after")
|
||||
@field_validator("import_file_path", mode="after")
|
||||
@classmethod
|
||||
def validate_pvforecastimport_file_path(
|
||||
cls, value: Optional[Union[str, Path]]
|
||||
) -> Optional[Path]:
|
||||
def validate_import_file_path(cls, value: Optional[Union[str, Path]]) -> Optional[Path]:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
@@ -65,13 +63,13 @@ class PVForecastImport(PVForecastProvider, PredictionImportProvider):
|
||||
return "PVForecastImport"
|
||||
|
||||
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.config.pvforecast.provider_settings.pvforecastimport_file_path,
|
||||
self.config.pvforecast.provider_settings.import_file_path,
|
||||
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.config.pvforecast.provider_settings.pvforecastimport_json,
|
||||
self.config.pvforecast.provider_settings.import_json,
|
||||
key_prefix="pvforecast",
|
||||
)
|
||||
|
42
src/akkudoktoreos/utils/docs.py
Normal file
42
src/akkudoktoreos/utils/docs.py
Normal 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
|
Reference in New Issue
Block a user