Documentation: Support nested config

* Add examples to pydantic models.
This commit is contained in:
Dominique Lasserre 2025-01-15 00:54:45 +01:00
parent be26457563
commit d74a56b75a
29 changed files with 8200 additions and 9424 deletions

File diff suppressed because it is too large Load Diff

3
docs/_static/eos.css vendored Normal file
View File

@ -0,0 +1,3 @@
.wy-nav-content {
max-width: 90% !important;
}

View File

@ -99,6 +99,7 @@ html_theme_options = {
"logo_only": False,
"titles_only": True,
}
html_css_files = ["eos.css"]
# -- Options for autodoc -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html

15696
openapi.json

File diff suppressed because it is too large Load Diff

View File

@ -2,74 +2,237 @@
"""Utility functions for Configuration specification generation."""
import argparse
import json
import sys
import textwrap
from typing import Any, Union
from pydantic.fields import ComputedFieldInfo, FieldInfo
from pydantic_core import PydanticUndefined
from akkudoktoreos.config.config import get_config
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import PydanticBaseModel
logger = get_logger(__name__)
config_eos = get_config()
# Fixed set of prefixes to filter configuration values and their respective titles
CONFIG_PREFIXES = {
"battery": "Battery Device Simulation Configuration",
"bev": "Battery Electric Vehicle Device Simulation Configuration",
"dishwasher": "Dishwasher Device Simulation Configuration",
"inverter": "Inverter Device Simulation Configuration",
"measurement": "Measurement Configuration",
"optimization": "General Optimization Configuration",
"server": "Server Configuration",
"elecprice": "Electricity Price Prediction Configuration",
"load": "Load Prediction Configuration",
"logging": "Logging Configuration",
"prediction": "General Prediction Configuration",
"pvforecast": "PV Forecast Configuration",
"weather": "Weather Forecast Configuration",
}
# Static set of configuration names to include in a separate table
GENERAL_CONFIGS = [
"config_default_file_path",
"config_file_path",
"config_folder_path",
"config_keys",
"config_keys_read_only",
"data_cache_path",
"data_cache_subpath",
"data_folder_path",
"data_output_path",
"data_output_subpath",
"latitude",
"longitude",
"package_root_path",
"timezone",
]
documented_types: set[PydanticBaseModel] = set()
undocumented_types: dict[PydanticBaseModel, tuple[str, list[str]]] = dict()
def generate_config_table_md(configs, title):
def get_title(config: PydanticBaseModel) -> str:
if config.__doc__ is None:
raise NameError(f"Missing docstring: {config}")
return config.__doc__.strip().splitlines()[0].strip(".")
def get_body(config: PydanticBaseModel) -> str:
if config.__doc__ is None:
raise NameError(f"Missing docstring: {config}")
return textwrap.dedent("\n".join(config.__doc__.strip().splitlines()[1:])).strip()
def resolve_nested_types(field_type: Any, parent_types: list[str]) -> list[tuple[Any, list[str]]]:
resolved_types: list[tuple[type, list[str]]] = []
origin = getattr(field_type, "__origin__", field_type)
if origin is Union:
for arg in getattr(field_type, "__args__", []):
resolved_types.extend(resolve_nested_types(arg, parent_types))
elif origin is list:
for arg in getattr(field_type, "__args__", []):
resolved_types.extend(resolve_nested_types(arg, parent_types + ["list"]))
else:
resolved_types.append((field_type, parent_types))
return resolved_types
def get_example_or_default(field_name: str, field_info: FieldInfo) -> dict[str, Any]:
"""Generate a default value for a field, considering constraints."""
if field_info.examples is not None:
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."""
example_data = {}
for field_name, field_info in model_class.model_fields.items():
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:
if not keys:
return value
current_key = keys[0]
if current_key == "list":
return [build_nested_structure(keys[1:], value)]
else:
return {current_key: build_nested_structure(keys[1:], value)}
def get_default_value(field_info: Union[FieldInfo, ComputedFieldInfo], regular_field: bool) -> Any:
default_value = ""
if regular_field:
if (val := field_info.default) is not PydanticUndefined:
default_value = val
else:
default_value = "required"
else:
default_value = "N/A"
return default_value
def get_type_name(field_type: type) -> str:
type_name = str(field_type).replace("typing.", "")
if type_name.startswith("<class"):
type_name = field_type.__name__
return type_name
def generate_config_table_md(
config: PydanticBaseModel,
toplevel_keys: list[str],
prefix: str,
toplevel: bool = False,
extra_config: bool = False,
) -> str:
"""Generate a markdown table for given configurations.
Args:
configs (dict): Configuration values with keys and their descriptions.
title (str): Title for the table.
config (PydanticBaseModel): PydanticBaseModel configuration definition.
prefix (str): Prefix for table entries.
Returns:
str: The markdown table as a string.
"""
if not configs:
return ""
table = ""
if toplevel:
title = get_title(config)
heading_level = "###" if extra_config else "##"
env_header = ""
env_header_underline = ""
env_width = ""
if not extra_config:
env_header = "| Environment Variable "
env_header_underline = "| -------------------- "
env_width = "20 "
table += f"{heading_level} {title}\n\n"
body = get_body(config)
if body:
table += body
table += "\n\n"
table += (
":::{table} "
+ f"{'::'.join(toplevel_keys)}\n:widths: 10 {env_width}10 5 5 30\n:align: left\n\n"
)
table += f"| Name {env_header}| Type | Read-Only | Default | Description |\n"
table += f"| ---- {env_header_underline}| ---- | --------- | ------- | ----------- |\n"
for field_name, field_info in list(config.model_fields.items()) + list(
config.model_computed_fields.items()
):
regular_field = isinstance(field_info, FieldInfo)
config_name = field_name if extra_config else field_name.upper()
field_type = field_info.annotation if regular_field else field_info.return_type
default_value = get_default_value(field_info, regular_field)
description = field_info.description if field_info.description else "-"
read_only = "rw" if regular_field else "ro"
type_name = get_type_name(field_type)
env_entry = ""
if not extra_config:
if regular_field:
env_entry = f"| `{prefix}{config_name}` "
else:
env_entry = "| "
table += f"| {field_name} {env_entry}| `{type_name}` | `{read_only}` | `{default_value}` | {description} |\n"
inner_types: dict[PydanticBaseModel, tuple[str, list[str]]] = dict()
def extract_nested_models(subtype: Any, subprefix: str, parent_types: list[str]):
if subtype in inner_types.keys():
return
nested_types = resolve_nested_types(subtype, [])
for nested_type, nested_parent_types in nested_types:
if issubclass(nested_type, PydanticBaseModel):
new_parent_types = parent_types + nested_parent_types
if "list" in parent_types:
new_prefix = ""
else:
new_prefix = f"{subprefix}"
inner_types.setdefault(nested_type, (new_prefix, new_parent_types))
for nested_field_name, nested_field_info in list(
nested_type.model_fields.items()
) + list(nested_type.model_computed_fields.items()):
nested_field_type = nested_field_info.annotation
if new_prefix:
new_prefix += f"{nested_field_name.upper()}__"
extract_nested_models(
nested_field_type,
new_prefix,
new_parent_types + [nested_field_name],
)
extract_nested_models(field_type, f"{prefix}{config_name}__", toplevel_keys + [field_name])
for new_type, info in inner_types.items():
if new_type not in documented_types:
undocumented_types.setdefault(new_type, (info[0], info[1]))
if toplevel:
table += ":::\n\n" # Add an empty line after the table
ins = create_model_from_examples(config)
if ins is not None:
# 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 = json.loads(ins_json)
ins_out_json = ins.model_dump_json(include_computed_fields=True)
ins_out_dict = json.loads(ins_out_json)
same_output = ins_out_dict == ins_dict
same_output_str = "/Output" if same_output else ""
table += f"#{heading_level} Example Input{same_output_str}\n\n"
table += "```{eval-rst}\n"
table += ".. code-block:: json\n\n"
input_dict = build_nested_structure(toplevel_keys, ins_dict)
table += textwrap.indent(json.dumps(input_dict, indent=4), " ")
table += "\n"
table += "```\n\n"
if not same_output:
table += f"#{heading_level} Example Output\n\n"
table += "```{eval-rst}\n"
table += ".. code-block:: json\n\n"
output_dict = build_nested_structure(toplevel_keys, ins_out_dict)
table += textwrap.indent(json.dumps(output_dict, indent=4), " ")
table += "\n"
table += "```\n\n"
while undocumented_types:
extra_config_type, extra_info = undocumented_types.popitem()
documented_types.add(extra_config_type)
table += generate_config_table_md(
extra_config_type, extra_info[1], extra_info[0], True, True
)
table = f"## {title}\n\n"
table += ":::{table} " + f"{title}\n:widths: 10 10 5 5 30\n:align: left\n\n"
table += "| Name | Type | Read-Only | Default | Description |\n"
table += "| ---- | ---- | --------- | ------- | ----------- |\n"
for name, config in sorted(configs.items()):
type_name = config["type"]
if type_name.startswith("typing."):
type_name = type_name[len("typing.") :]
table += f"| `{config['name']}` | `{type_name}` | `{config['read-only']}` | `{config['default']}` | {config['description']} |\n"
table += ":::\n\n" # Add an empty line after the table
return table
@ -79,57 +242,16 @@ def generate_config_md() -> str:
Returns:
str: The Markdown representation of the configuration spec.
"""
# FIXME: Support for nested
configs = {}
config_keys = config_eos.model_fields_set
# config_keys_read_only = config_eos.config_keys_read_only
config_keys_read_only: list[str] = []
for config_key in config_keys:
config = {}
config["name"] = config_key
config["value"] = getattr(config_eos, config_key)
if config_key in config_keys_read_only:
config["read-only"] = "ro"
computed_field_info = config_eos.__pydantic_decorators__.computed_fields[
config_key
].info
config["default"] = "N/A"
config["description"] = computed_field_info.description
config["type"] = str(computed_field_info.return_type)
else:
config["read-only"] = "rw"
field_info = config_eos.model_fields[config_key]
config["default"] = field_info.default
config["description"] = field_info.description
config["type"] = str(field_info.annotation)
configs[config_key] = config
# Generate markdown for the main table
markdown = "# Configuration Table\n\n"
# Generate table for general configuration names
general_configs = {k: v for k, v in configs.items() if k in GENERAL_CONFIGS}
for k in general_configs.keys():
del configs[k] # Remove general configs from the main configs dictionary
markdown += generate_config_table_md(general_configs, "General Configuration Values")
# Generate tables for each top level config
for field_name, field_info in config_eos.model_fields.items():
field_type = field_info.annotation
markdown += generate_config_table_md(
field_type, [field_name], f"EOS_{field_name.upper()}__", True
)
non_prefixed_configs = {k: v for k, v in configs.items()}
# Generate tables for each prefix (sorted by value) and remove prefixed configs from the main dictionary
sorted_prefixes = sorted(CONFIG_PREFIXES.items(), key=lambda item: item[1])
for prefix, title in sorted_prefixes:
prefixed_configs = {k: v for k, v in configs.items() if k.startswith(prefix)}
for k in prefixed_configs.keys():
del non_prefixed_configs[k]
markdown += generate_config_table_md(prefixed_configs, title)
# Generate markdown for the remaining non-prefixed configs if any
if non_prefixed_configs:
markdown += generate_config_table_md(non_prefixed_configs, "Other Configuration Values")
# Assure the is no double \n at end of file
# Assure there is no double \n at end of file
markdown = markdown.rstrip("\n")
markdown += "\n"

View File

@ -63,10 +63,13 @@ def get_absolute_path(
class ConfigCommonSettings(SettingsBaseModel):
"""Settings for common configuration."""
"""Settings for common configuration.
General configuration to set directories of cache and output files.
"""
data_folder_path: Optional[Path] = Field(
default=None, description="Path to EOS data directory."
default=None, description="Path to EOS data directory.", examples=[None, "/home/eos/data"]
)
data_output_subpath: Optional[Path] = Field(

View File

@ -14,10 +14,12 @@ from akkudoktoreos.core.logabc import logging_str_to_level
class LoggingCommonSettings(SettingsBaseModel):
"""Common settings for logging."""
"""Logging Configuration."""
logging_level_default: Optional[str] = Field(
default=None, description="EOS default logging level."
default=None,
description="EOS default logging level.",
examples=["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"],
)
# Validators

View File

@ -128,9 +128,16 @@ class PydanticBaseModel(BaseModel):
return value
# Override Pydantics serialization for all DateTime fields
def model_dump(self, *args: Any, **kwargs: Any) -> dict:
def model_dump(
self, *args: Any, include_computed_fields: bool = True, **kwargs: Any
) -> dict[str, Any]:
"""Custom dump method to handle serialization for DateTime fields."""
result = super().model_dump(*args, **kwargs)
if not include_computed_fields:
for computed_field_name in self.model_computed_fields:
result.pop(computed_field_name, None)
for key, value in result.items():
if isinstance(value, pendulum.DateTime):
result[key] = PydanticTypeAdapterDateTime.serialize(value)
@ -185,6 +192,10 @@ class PydanticBaseModel(BaseModel):
"""
return cls.model_validate(data)
def model_dump_json(self, *args: Any, indent: Optional[int] = None, **kwargs: Any) -> str:
data = self.model_dump(*args, **kwargs)
return json.dumps(data, indent=indent, default=str)
def to_json(self) -> str:
"""Convert the PydanticBaseModel instance to a JSON string.

View File

@ -25,15 +25,26 @@ def max_charging_power_field(description: Optional[str] = None) -> float:
def initial_soc_percentage_field(description: str) -> int:
return Field(default=0, ge=0, le=100, description=description)
return Field(default=0, ge=0, le=100, description=description, examples=[42])
def discharging_efficiency_field(default_value: float) -> float:
return Field(
default=default_value,
gt=0,
le=1,
description="A float representing the discharge efficiency of the battery.",
)
class BaseBatteryParameters(DeviceParameters):
"""Base class for battery parameters with fields for capacity, efficiency, and state of charge."""
"""Battery Device Simulation Configuration."""
device_id: str = Field(description="ID of battery")
device_id: str = Field(description="ID of battery", examples=["battery1"])
capacity_wh: int = Field(
gt=0, description="An integer representing the capacity of the battery in watt-hours."
gt=0,
description="An integer representing the capacity of the battery in watt-hours.",
examples=[8000],
)
charging_efficiency: float = Field(
default=0.88,
@ -41,12 +52,7 @@ class BaseBatteryParameters(DeviceParameters):
le=1,
description="A float representing the charging efficiency of the battery.",
)
discharging_efficiency: float = Field(
default=0.88,
gt=0,
le=1,
description="A float representing the discharge efficiency of the battery.",
)
discharging_efficiency: float = discharging_efficiency_field(0.88)
max_charge_power_w: Optional[float] = max_charging_power_field()
initial_soc_percentage: int = initial_soc_percentage_field(
"An integer representing the state of charge of the battery at the **start** of the current hour (not the current state)."
@ -56,6 +62,7 @@ class BaseBatteryParameters(DeviceParameters):
ge=0,
le=100,
description="An integer representing the minimum state of charge (SOC) of the battery in percentage.",
examples=[10],
)
max_soc_percentage: int = Field(
default=100,
@ -70,10 +77,10 @@ class SolarPanelBatteryParameters(BaseBatteryParameters):
class ElectricVehicleParameters(BaseBatteryParameters):
"""Parameters specific to an electric vehicle (EV)."""
"""Battery Electric Vehicle Device Simulation Configuration."""
device_id: str = Field(description="ID of electric vehicle")
discharging_efficiency: float = 1.0
device_id: str = Field(description="ID of electric vehicle", examples=["ev1"])
discharging_efficiency: float = discharging_efficiency_field(1.0)
initial_soc_percentage: int = initial_soc_percentage_field(
"An integer representing the current state of charge (SOC) of the battery in percentage."
)
@ -82,7 +89,7 @@ class ElectricVehicleParameters(BaseBatteryParameters):
class ElectricVehicleResult(DeviceOptimizeResult):
"""Result class containing information related to the electric vehicle's charging and discharging behavior."""
device_id: str = Field(description="ID of electric vehicle")
device_id: str = Field(description="ID of electric vehicle", examples=["ev1"])
charge_array: list[float] = Field(
description="Hourly charging status (0 for no charging, 1 for charging)."
)

View File

@ -19,20 +19,19 @@ from akkudoktoreos.utils.datetimeutil import to_duration
logger = get_logger(__name__)
# class DeviceParameters(PydanticBaseModel):
class DeviceParameters(ParametersBaseModel):
device_id: str = Field(description="ID of device")
device_id: str = Field(description="ID of device", examples="device1")
hours: Optional[int] = Field(
default=None,
gt=0,
description="Number of prediction hours. Defaults to global config prediction hours.",
examples=[None],
)
# class DeviceOptimizeResult(PydanticBaseModel):
class DeviceOptimizeResult(ParametersBaseModel):
device_id: str = Field(description="ID of device")
hours: int = Field(gt=0, description="Number of hours in the simulation.")
device_id: str = Field(description="ID of device", examples=["device1"])
hours: int = Field(gt=0, description="Number of hours in the simulation.", examples=[24])
class DeviceState(Enum):

View File

@ -10,14 +10,18 @@ logger = get_logger(__name__)
class HomeApplianceParameters(DeviceParameters):
device_id: str = Field(description="ID of home appliance")
"""Home Appliance Device Simulation Configuration."""
device_id: str = Field(description="ID of home appliance", examples=["dishwasher"])
consumption_wh: int = Field(
gt=0,
description="An integer representing the energy consumption of a household device in watt-hours.",
examples=[2000],
)
duration_h: int = Field(
gt=0,
description="An integer representing the usage duration of a household device in hours.",
examples=[3],
)

View File

@ -10,9 +10,13 @@ logger = get_logger(__name__)
class InverterParameters(DeviceParameters):
device_id: str = Field(description="ID of inverter")
max_power_wh: float = Field(gt=0)
battery: Optional[str] = Field(default=None, description="ID of battery")
"""Inverter Device Simulation Configuration."""
device_id: str = Field(description="ID of inverter", examples=["inverter1"])
max_power_wh: float = Field(gt=0, examples=[10000])
battery: Optional[str] = Field(
default=None, description="ID of battery", examples=[None, "battery1"]
)
class Inverter(DeviceBase):

View File

@ -15,11 +15,13 @@ class DevicesCommonSettings(SettingsBaseModel):
"""Base configuration for devices simulation settings."""
batteries: Optional[list[BaseBatteryParameters]] = Field(
default=None, description="List of battery/ev devices"
default=None,
description="List of battery/ev devices",
examples=[[{"device_id": "battery1", "capacity_wh": 8000}]],
)
inverters: Optional[list[InverterParameters]] = Field(
default=None, description="List of inverters"
default=None, description="List of inverters", examples=[[]]
)
home_appliances: Optional[list[HomeApplianceParameters]] = Field(
default=None, description="List of home appliances"
default=None, description="List of home appliances", examples=[[]]
)

View File

@ -23,20 +23,22 @@ logger = get_logger(__name__)
class MeasurementCommonSettings(SettingsBaseModel):
"""Measurement Configuration."""
measurement_load0_name: Optional[str] = Field(
default=None, description="Name of the load0 source (e.g. 'Household', 'Heat Pump')"
default=None, description="Name of the load0 source", examples=["Household", "Heat Pump"]
)
measurement_load1_name: Optional[str] = Field(
default=None, description="Name of the load1 source (e.g. 'Household', 'Heat Pump')"
default=None, description="Name of the load1 source", examples=[None]
)
measurement_load2_name: Optional[str] = Field(
default=None, description="Name of the load2 source (e.g. 'Household', 'Heat Pump')"
default=None, description="Name of the load2 source", examples=[None]
)
measurement_load3_name: Optional[str] = Field(
default=None, description="Name of the load3 source (e.g. 'Household', 'Heat Pump')"
default=None, description="Name of the load3 source", examples=[None]
)
measurement_load4_name: Optional[str] = Field(
default=None, description="Name of the load4 source (e.g. 'Household', 'Heat Pump')"
default=None, description="Name of the load4 source", examples=[None]
)
@ -49,29 +51,29 @@ class MeasurementDataRecord(DataRecord):
# Single loads, to be aggregated to total load
measurement_load0_mr: Optional[float] = Field(
default=None, ge=0, description="Load0 meter reading [kWh]"
default=None, ge=0, description="Load0 meter reading [kWh]", examples=[40421]
)
measurement_load1_mr: Optional[float] = Field(
default=None, ge=0, description="Load1 meter reading [kWh]"
default=None, ge=0, description="Load1 meter reading [kWh]", examples=[None]
)
measurement_load2_mr: Optional[float] = Field(
default=None, ge=0, description="Load2 meter reading [kWh]"
default=None, ge=0, description="Load2 meter reading [kWh]", examples=[None]
)
measurement_load3_mr: Optional[float] = Field(
default=None, ge=0, description="Load3 meter reading [kWh]"
default=None, ge=0, description="Load3 meter reading [kWh]", examples=[None]
)
measurement_load4_mr: Optional[float] = Field(
default=None, ge=0, description="Load4 meter reading [kWh]"
default=None, ge=0, description="Load4 meter reading [kWh]", examples=[None]
)
measurement_max_loads: ClassVar[int] = 5 # Maximum number of loads that can be set
measurement_grid_export_mr: Optional[float] = Field(
default=None, ge=0, description="Export to grid meter reading [kWh]"
default=None, ge=0, description="Export to grid meter reading [kWh]", examples=[1000]
)
measurement_grid_import_mr: Optional[float] = Field(
default=None, ge=0, description="Import from grid meter reading [kWh]"
default=None, ge=0, description="Import from grid meter reading [kWh]", examples=[1000]
)
# Computed fields

View File

@ -9,7 +9,7 @@ logger = get_logger(__name__)
class OptimizationCommonSettings(SettingsBaseModel):
"""Base configuration for optimization settings.
"""General Optimization Configuration.
Attributes:
optimization_hours (int): Number of hours for optimizations.

View File

@ -7,11 +7,17 @@ from akkudoktoreos.prediction.elecpriceimport import ElecPriceImportCommonSettin
class ElecPriceCommonSettings(SettingsBaseModel):
"""Electricity Price Prediction Configuration."""
elecprice_provider: Optional[str] = Field(
default=None, description="Electricity price provider id of provider to be used."
default=None,
description="Electricity price provider id of provider to be used.",
examples=["ElecPriceAkkudoktor"],
)
elecprice_charges_kwh: Optional[float] = Field(
default=None, ge=0, description="Electricity price charges (€/kWh)."
default=None, ge=0, description="Electricity price charges (€/kWh).", examples=[0.21]
)
provider_settings: Optional[ElecPriceImportCommonSettings] = None
provider_settings: Optional[ElecPriceImportCommonSettings] = Field(
default=None, description="Provider settings", examples=[None]
)

View File

@ -23,12 +23,15 @@ class ElecPriceImportCommonSettings(SettingsBaseModel):
"""Common settings for elecprice data import from file or JSON String."""
elecpriceimport_file_path: Optional[Union[str, Path]] = Field(
default=None, description="Path to the file to import elecprice data from."
default=None,
description="Path to the file to import elecprice data from.",
examples=[None, "/path/to/prices.json"],
)
elecpriceimport_json: Optional[str] = Field(
default=None,
description="JSON string, dictionary of electricity price forecast value lists.",
examples=['{"elecprice_marketprice_wh": [0.0003384, 0.0003318, 0.0003284]}'],
)
# Validators

View File

@ -13,12 +13,14 @@ logger = get_logger(__name__)
class LoadCommonSettings(SettingsBaseModel):
"""Common settings for loaod forecast providers."""
"""Load Prediction Configuration."""
load_provider: Optional[str] = Field(
default=None, description="Load provider id of provider to be used."
default=None,
description="Load provider id of provider to be used.",
examples=["LoadAkkudoktor"],
)
provider_settings: Optional[Union[LoadAkkudoktorCommonSettings, LoadImportCommonSettings]] = (
None
Field(default=None, description="Provider settings", examples=[None])
)

View File

@ -17,7 +17,7 @@ class LoadAkkudoktorCommonSettings(SettingsBaseModel):
"""Common settings for load data import from file."""
loadakkudoktor_year_energy: Optional[float] = Field(
default=None, description="Yearly energy consumption (kWh)."
default=None, description="Yearly energy consumption (kWh).", examples=[40421]
)

View File

@ -23,10 +23,14 @@ class LoadImportCommonSettings(SettingsBaseModel):
"""Common settings for load data import from file or JSON string."""
load_import_file_path: Optional[Union[str, Path]] = Field(
default=None, description="Path to the file to import load data from."
default=None,
description="Path to the file to import load data from.",
examples=[None, "/path/to/yearly_load.json"],
)
load_import_json: Optional[str] = Field(
default=None, description="JSON string, dictionary of load forecast value lists."
default=None,
description="JSON string, dictionary of load forecast value lists.",
examples=['{"load0_mean": [676.71, 876.19, 527.13]}'],
)
# Validators

View File

@ -45,7 +45,7 @@ from akkudoktoreos.utils.datetimeutil import to_timezone
class PredictionCommonSettings(SettingsBaseModel):
"""Base configuration for prediction settings, including forecast duration, geographic location, and time zone.
"""General Prediction Configuration.
This class provides configuration for prediction settings, allowing users to specify
parameters such as the forecast duration (in hours) and location (latitude and longitude).

View File

@ -12,29 +12,37 @@ logger = get_logger(__name__)
class PVForecastCommonSettings(SettingsBaseModel):
"""PV Forecast Configuration."""
# General plane parameters
# https://pvlib-python.readthedocs.io/en/stable/_modules/pvlib/iotools/pvgis.html
# Inverter Parameters
# https://pvlib-python.readthedocs.io/en/stable/_modules/pvlib/inverter.html
pvforecast_provider: Optional[str] = Field(
default=None, description="PVForecast provider id of provider to be used."
default=None,
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(
default=None, description="Tilt angle from horizontal plane. Ignored for two-axis tracking."
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."
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'."
@ -49,48 +57,60 @@ class PVForecastCommonSettings(SettingsBaseModel):
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."
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."
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]"
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."
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."
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."
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."
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'."
@ -105,262 +125,332 @@ class PVForecastCommonSettings(SettingsBaseModel):
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."
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."
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]"
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."
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."
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."
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."
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'."
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"
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."
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."
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]"
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."
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."
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."
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."
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'."
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"
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."
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."
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]"
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."
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."
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."
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."
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'."
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"
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."
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."
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]"
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."
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."
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."
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."
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'."
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"
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."
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."
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]"
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."
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."
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
provider_settings: Optional[PVForecastImportCommonSettings] = None
provider_settings: Optional[PVForecastImportCommonSettings] = Field(
default=None, description="Provider settings", examples=[None]
)
# Computed fields
@computed_field # type: ignore[prop-decorator]

View File

@ -23,12 +23,15 @@ class PVForecastImportCommonSettings(SettingsBaseModel):
"""Common settings for pvforecast data import from file or JSON string."""
pvforecastimport_file_path: Optional[Union[str, Path]] = Field(
default=None, description="Path to the file to import PV forecast data from."
default=None,
description="Path to the file to import PV forecast data from.",
examples=[None, "/path/to/pvforecast.json"],
)
pvforecastimport_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

View File

@ -9,8 +9,14 @@ from akkudoktoreos.prediction.weatherimport import WeatherImportCommonSettings
class WeatherCommonSettings(SettingsBaseModel):
"""Weather Forecast Configuration."""
weather_provider: Optional[str] = Field(
default=None, description="Weather provider id of provider to be used."
default=None,
description="Weather provider id of provider to be used.",
examples=["WeatherImport"],
)
provider_settings: Optional[WeatherImportCommonSettings] = None
provider_settings: Optional[WeatherImportCommonSettings] = Field(
default=None, description="Provider settings", examples=[None]
)

View File

@ -23,11 +23,15 @@ class WeatherImportCommonSettings(SettingsBaseModel):
"""Common settings for weather data import from file or JSON string."""
weatherimport_file_path: Optional[Union[str, Path]] = Field(
default=None, description="Path to the file to import weather data from."
default=None,
description="Path to the file to import weather data from.",
examples=[None, "/path/to/weather_data.json"],
)
weatherimport_json: Optional[str] = Field(
default=None, description="JSON string, dictionary of weather forecast value lists."
default=None,
description="JSON string, dictionary of weather forecast value lists.",
examples=['{"weather_temp_air": [18.3, 17.8, 16.9]}'],
)
# Validators

View File

@ -11,7 +11,7 @@ logger = get_logger(__name__)
class ServerCommonSettings(SettingsBaseModel):
"""Common server settings.
"""Server Configuration.
Attributes:
To be added

View File

@ -18,6 +18,8 @@ class classproperty(property):
class UtilsCommonSettings(SettingsBaseModel):
"""Utils Configuration."""
pass

View File

@ -419,7 +419,9 @@ def prepare_visualize(
start_hour: Optional[int] = 0,
) -> None:
report = VisualizationReport(filename)
next_full_hour_date = pendulum.now(report.config.prediction.timezone).start_of("hour").add(hours=1)
next_full_hour_date = (
pendulum.now(report.config.prediction.timezone).start_of("hour").add(hours=1)
)
# Group 1:
report.create_line_chart_date(
next_full_hour_date, # start_date

View File

@ -535,7 +535,7 @@ class TestDataSequence:
json_str = sequence.to_json()
assert isinstance(json_str, str)
assert "2023-11-06" in json_str
assert ":0.8" in json_str
assert ": 0.8" in json_str
def test_from_json(self, sequence, sequence2):
json_str = sequence2.to_json()