Add Documentation 2 (#334)

Add documentation that covers:

- configuration
- prediction

Add Python scripts that support automatic documentation generation for
configuration data defined with pydantic.

Adapt EOS configuration to provide more methods for REST API and
automatic documentation generation.

Adapt REST API to allow for EOS configuration file load and save.
Sort REST API on generation of openapi markdown for docs.

Move logutil to core/logging to allow configuration of logging by standard config.

Make Akkudoktor predictions always start extraction of prediction data at start of day.
Previously extraction started at actual hour. This is to support the code that assumes
prediction data to start at start of day.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
Bobby Noelte 2025-01-05 14:41:07 +01:00 committed by GitHub
parent 03ec729e50
commit d4e31d556a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 4517 additions and 462 deletions

View File

@ -0,0 +1,89 @@
% SPDX-License-Identifier: Apache-2.0
# Configuration
The configuration controls all aspects of EOS: optimization, prediction, measurement, and energy
management.
## Storing Configuration
EOS stores configuration data in a **key-value store**, where a `configuration key` refers to the
unique identifier used to store and retrieve specific configuration data. Note that the key-value
store is memory-based, meaning all stored data will be lost upon restarting the EOS REST server if
not saved to the `EOS configuration file`.
Some `configuration keys` are read-only and cannot be altered. These keys are either set up by other
means, such as environment variables, or determined from other information.
Several endpoints of the EOS REST server allow for the management and retrieval of configuration
data.
### Save Configuration File
Use endpoint `PUT /v1/config/file` to save the current configuration to the
`EOS configuration file`.
### Load Configuration File
Use endpoint `POST /v1/config/update` to update the configuration from the `EOS configuration file`.
## Configuration Sources and Priorities
The configuration sources and their priorities are as follows:
1. **Settings**: Provided during runtime by the REST interface
2. **Environment Variables**: Defined at startup of the REST server and during runtime
3. **EOS Configuration File**: Read at startup of the REST server and on request
4. **Default Values**
### Settings
Settings are sets of configuration data that take precedence over all other configuration data from
different sources. Note that settings are not persistent. To make the current configuration with the
current settings persistent, save the configuration to the `EOS configuration file`.
Use the following endpoints to change the current configuration settings:
- `PUT /v1/config`: Replaces the entire configuration settings.
- `PUT /v1/config/value`: Sets a specific configuration option.
### Environment Variables
All `configuration keys` can be set by environment variables with the same name. EOS recognizes the
following special environment variables:
- `EOS_CONFIG_DIR`: The directory to search for an EOS configuration file.
- `EOS_DIR`: The directory used by EOS for data, which will also be searched for an EOS
configuration file.
- `EOS_LOGGING_LEVEL`: The logging level to use in EOS.
### EOS Configuration File
The EOS configuration file provides persistent storage for configuration data. It can be modified
directly or through the REST interface.
If you do not have a configuration file, it will be automatically created on the first startup of
the REST server in a system-dependent location.
To determine the location of the configuration file used by EOS, ask the REST server. The endpoint
`GET /v1/config` provides the `config_file_path` configuration key.
EOS searches for the configuration file in the following order:
1. The directory specified by the `EOS_CONFIG_DIR` environment variable
2. The directory specified by the `EOS_DIR` environment variable
3. A platform-specific default directory for EOS
4. The current working directory
The first available configuration file found in these directories is loaded. If no configuration
file is found, a default configuration file is created in the platform-specific default directory,
and default settings are loaded into it.
### Default Values
Some of the `configuration keys` have default values by definition. For most of the
`configuration keys` the default value is just `None`, which means no default value.
```{eval-sh}
./scripts/generate_config_md.py | ./scripts/extract_markdown.py --input-stdin --heading-level 1
```

View File

@ -30,7 +30,7 @@ The measurement data must be or is provided in one of the following formats:
A dictionary with the following structure: A dictionary with the following structure:
```JSON ```python
{ {
"start_datetime": "2024-01-01 00:00:00", "start_datetime": "2024-01-01 00:00:00",
"interval": "1 Hour", "interval": "1 Hour",

View File

@ -21,7 +21,7 @@ data is lost on re-start of the EOS REST server.
Most predictions can be sourced from various providers. The specific provider to use is configured Most predictions can be sourced from various providers. The specific provider to use is configured
in the EOS configuration. For example: in the EOS configuration. For example:
```plaintext ```python
weather_provider = "ClearOutside" weather_provider = "ClearOutside"
``` ```
@ -43,7 +43,7 @@ The prediction data must be provided in one of the following formats:
A dictionary with the following structure: A dictionary with the following structure:
```JSON ```python
{ {
"start_datetime": "2024-01-01 00:00:00", "start_datetime": "2024-01-01 00:00:00",
"interval": "1 Hour", "interval": "1 Hour",
@ -214,6 +214,67 @@ Configuration options:
- `pvforecastimport_file_path`: Path to the file to import PV forecast data from. - `pvforecastimport_file_path`: Path to the file to import PV forecast data from.
- `pvforecastimport_json`: JSON string, dictionary of PV forecast value lists. - `pvforecastimport_json`: JSON string, dictionary of PV forecast value lists.
------
Some of the configuration options directly follow the [PVGIS](https://joint-research-centre.ec.europa.eu/photovoltaic-geographical-information-system-pvgis/getting-started-pvgis/pvgis-user-manual_en) nomenclature.
Detailed definitions taken from **PVGIS**:
- `pvforecast<0..5>_pvtechchoice`
The performance of PV modules depends on the temperature and on the solar irradiance, but the exact dependence varies between different types of PV modules. At the moment we can estimate the losses due to temperature and irradiance effects for the following types of modules: crystalline silicon cells; thin film modules made from CIS or CIGS and thin film modules made from Cadmium Telluride (CdTe).
For other technologies (especially various amorphous technologies), this correction cannot be calculated here. If you choose one of the first three options here the calculation of performance will take into account the temperature dependence of the performance of the chosen technology. If you choose the other option (other/unknown), the calculation will assume a loss of 8% of power due to temperature effects (a generic value which has found to be reasonable for temperate climates).
PV power output also depends on the spectrum of the solar radiation. PVGIS can calculate how the variations of the spectrum of sunlight affects the overall energy production from a PV system. At the moment this calculation can be done for crystalline silicon and CdTe modules. Note that this calculation is not yet available when using the NSRDB solar radiation database.
- `pvforecast<0..5>_peakpower`
This is the power that the manufacturer declares that the PV array can produce under standard test conditions (STC), which are a constant 1000W of solar irradiation per square meter in the plane of the array, at an array temperature of 25°C. The peak power should be entered in kilowatt-peak (kWp). If you do not know the declared peak power of your modules but instead know the area of the modules and the declared conversion efficiency (in percent), you can calculate the peak power as power = area * efficiency / 100.
Bifacial modules: PVGIS doesn't make specific calculations for bifacial modules at present. Users who wish to explore the possible benefits of this technology can input the power value for Bifacial Nameplate Irradiance. This can also be can also be estimated from the front side peak power P_STC value and the bifaciality factor, φ (if reported in the module data sheet) as: P_BNPI = P_STC * (1 + φ * 0.135). NB this bifacial approach is not appropriate for BAPV or BIPV installations or for modules mounting on a N-S axis i.e. facing E-W.
- `pvforecast<0..5>_loss`
The estimated system losses are all the losses in the system, which cause the power actually delivered to the electricity grid to be lower than the power produced by the PV modules. There are several causes for this loss, such as losses in cables, power inverters, dirt (sometimes snow) on the modules and so on. Over the years the modules also tend to lose a bit of their power, so the average yearly output over the lifetime of the system will be a few percent lower than the output in the first years.
We have given a default value of 14% for the overall losses. If you have a good idea that your value will be different (maybe due to a really high-efficiency inverter) you may reduce this value a little.
- `pvforecast<0..5>_mountingplace`
For fixed (non-tracking) systems, the way the modules are mounted will have an influence on the temperature of the module, which in turn affects the efficiency. Experiments have shown that if the movement of air behind the modules is restricted, the modules can get considerably hotter (up to 15°C at 1000W/m2 of sunlight).
In PVGIS there are two possibilities: free-standing, meaning that the modules are mounted on a rack with air flowing freely behind the modules; and building- integrated, which means that the modules are completely built into the structure of the wall or roof of a building, with no air movement behind the modules.
Some types of mounting are in between these two extremes, for instance if the modules are mounted on a roof with curved roof tiles, allowing air to move behind the modules. In such cases, the performance will be somewhere between the results of the two calculations that are possible here.
- `pvforecast<0..5>_userhorizon`
Elevation of horizon in degrees, at equally spaced azimuth clockwise from north. In the user horizon
data each number represents the horizon height in degrees in a certain compass direction around the
point of interest. The horizon heights should be given in a clockwise direction starting at North;
that is, from North, going to East, South, West, and back to North. The values are assumed to
represent equal angular distance around the horizon. For instance, if you have 36 values, the first
point is due north, the next is 10 degrees east of north, and so on, until the last point, 10
degrees west of north.
------
Most of the configuration options are in line with the [PVLib](https://pvlib-python.readthedocs.io/en/stable/_modules/pvlib/iotools/pvgis.html) definition for PVGIS data.
Detailed definitions from **PVLib** for PVGIS data.
- `pvforecast<0..5>_surface_tilt`:
Tilt angle from horizontal plane.
- `pvforecast<0..5>_surface_azimuth`
Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180,
west=270). This is offset 180 degrees from the convention used by PVGIS.
------
### PVForecastAkkudoktor Provider ### PVForecastAkkudoktor Provider
The `PVForecastAkkudoktor` provider retrieves the PV power forecast data directly from The `PVForecastAkkudoktor` provider retrieves the PV power forecast data directly from

View File

@ -14,6 +14,7 @@ welcome.md
akkudoktoreos/about.md akkudoktoreos/about.md
develop/getting_started.md develop/getting_started.md
develop/CONTRIBUTING.md develop/CONTRIBUTING.md
akkudoktoreos/configuration.md
akkudoktoreos/prediction.md akkudoktoreos/prediction.md
akkudoktoreos/measurement.md akkudoktoreos/measurement.md
akkudoktoreos/serverapi.md akkudoktoreos/serverapi.md

File diff suppressed because it is too large Load Diff

161
scripts/generate_config_md.py Executable file
View File

@ -0,0 +1,161 @@
#!.venv/bin/python
"""Utility functions for Configuration specification generation."""
import argparse
import sys
from akkudoktoreos.config.config import get_config
from akkudoktoreos.core.logging import get_logger
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",
]
def generate_config_table_md(configs, title):
"""Generate a markdown table for given configurations.
Args:
configs (dict): Configuration values with keys and their descriptions.
title (str): Title for the table.
Returns:
str: The markdown table as a string.
"""
if not configs:
return ""
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
def generate_config_md() -> str:
"""Generate configuration specification in Markdown with extra tables for prefixed values.
Returns:
str: The Markdown representation of the configuration spec.
"""
configs = {}
config_keys = config_eos.config_keys
config_keys_read_only = config_eos.config_keys_read_only
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")
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")
return markdown
def main():
"""Main function to run the generation of the Configuration specification as Markdown."""
parser = argparse.ArgumentParser(description="Generate Configuration Specification as Markdown")
parser.add_argument(
"--output-file",
type=str,
default=None,
help="File to write the Configuration Specification to",
)
args = parser.parse_args()
try:
config_md = generate_config_md()
if args.output_file:
# Write to file
with open(args.output_file, "w") as f:
f.write(config_md)
else:
# Write to std output
print(config_md)
except Exception as e:
print(f"Error during Configuration Specification generation: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -189,8 +189,10 @@ def openapi_to_markdown(openapi_json: dict) -> str:
markdown += "**Endpoints**:\n\n" markdown += "**Endpoints**:\n\n"
paths = openapi_json.get("paths", {}) paths = openapi_json.get("paths", {})
for path, methods in paths.items(): for path in sorted(paths):
for method, details in methods.items(): methods = paths[path]
for method in sorted(methods):
details = methods[method]
markdown += format_endpoint(path, method, details) markdown += format_endpoint(path, method, details)
# Assure the is no double \n at end of file # Assure the is no double \n at end of file

View File

@ -20,6 +20,8 @@ from pydantic import Field, ValidationError, computed_field
# settings # settings
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.coreabc import SingletonMixin from akkudoktoreos.core.coreabc import SingletonMixin
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.logsettings import LoggingCommonSettings
from akkudoktoreos.devices.devices import DevicesCommonSettings from akkudoktoreos.devices.devices import DevicesCommonSettings
from akkudoktoreos.measurement.measurement import MeasurementCommonSettings from akkudoktoreos.measurement.measurement import MeasurementCommonSettings
from akkudoktoreos.optimization.optimization import OptimizationCommonSettings from akkudoktoreos.optimization.optimization import OptimizationCommonSettings
@ -34,7 +36,6 @@ from akkudoktoreos.prediction.pvforecastimport import PVForecastImportCommonSett
from akkudoktoreos.prediction.weather import WeatherCommonSettings from akkudoktoreos.prediction.weather import WeatherCommonSettings
from akkudoktoreos.prediction.weatherimport import WeatherImportCommonSettings from akkudoktoreos.prediction.weatherimport import WeatherImportCommonSettings
from akkudoktoreos.server.server import ServerCommonSettings from akkudoktoreos.server.server import ServerCommonSettings
from akkudoktoreos.utils.logutil import get_logger
from akkudoktoreos.utils.utils import UtilsCommonSettings from akkudoktoreos.utils.utils import UtilsCommonSettings
logger = get_logger(__name__) logger = get_logger(__name__)
@ -90,6 +91,7 @@ class ConfigCommonSettings(SettingsBaseModel):
class SettingsEOS( class SettingsEOS(
ConfigCommonSettings, ConfigCommonSettings,
LoggingCommonSettings,
DevicesCommonSettings, DevicesCommonSettings,
MeasurementCommonSettings, MeasurementCommonSettings,
OptimizationCommonSettings, OptimizationCommonSettings,
@ -188,7 +190,13 @@ class ConfigEOS(SingletonMixin, SettingsEOS):
@property @property
def config_default_file_path(self) -> Path: def config_default_file_path(self) -> Path:
"""Compute the default config file path.""" """Compute the default config file path."""
return Path(__file__).parent.parent.joinpath("data/default.config.json") return self.package_root_path.joinpath("data/default.config.json")
@computed_field # type: ignore[prop-decorator]
@property
def package_root_path(self) -> Path:
"""Compute the package root path."""
return Path(__file__).parent.parent.resolve()
# Computed fields # Computed fields
@computed_field # type: ignore[prop-decorator] @computed_field # type: ignore[prop-decorator]
@ -200,6 +208,15 @@ class ConfigEOS(SingletonMixin, SettingsEOS):
key_list.extend(list(self.__pydantic_decorators__.computed_fields.keys())) key_list.extend(list(self.__pydantic_decorators__.computed_fields.keys()))
return key_list return key_list
# Computed fields
@computed_field # type: ignore[prop-decorator]
@property
def config_keys_read_only(self) -> List[str]:
"""Returns the keys of all read only fields in the configuration."""
key_list = []
key_list.extend(list(self.__pydantic_decorators__.computed_fields.keys()))
return key_list
def __init__(self) -> None: def __init__(self) -> None:
"""Initializes the singleton ConfigEOS instance. """Initializes the singleton ConfigEOS instance.
@ -239,7 +256,7 @@ class ConfigEOS(SingletonMixin, SettingsEOS):
settings (SettingsEOS): The settings to apply globally. settings (SettingsEOS): The settings to apply globally.
force (Optional[bool]): If True, overwrites the existing settings completely. force (Optional[bool]): If True, overwrites the existing settings completely.
If False, the new settings are merged to the existing ones with priority for If False, the new settings are merged to the existing ones with priority for
the new ones. the new ones. Defaults to False.
Raises: Raises:
ValueError: If settings are already set and `force` is not True or ValueError: If settings are already set and `force` is not True or
@ -349,14 +366,23 @@ class ConfigEOS(SingletonMixin, SettingsEOS):
return cfile, True return cfile, True
return config_dirs[0].joinpath(self.CONFIG_FILE_NAME), False return config_dirs[0].joinpath(self.CONFIG_FILE_NAME), False
def from_config_file(self) -> None: def settings_from_config_file(self) -> tuple[SettingsEOS, Path]:
"""Loads the configuration file settings for EOS. """Load settings from the configuration file.
If the config file does not exist, it will be created.
Returns:
tuple of settings and path
settings (SettingsEOS): The settings defined by the EOS configuration file.
path (pathlib.Path): The path of the configuration file.
Raises: Raises:
ValueError: If the configuration file is invalid or incomplete. ValueError: If the configuration file is invalid or incomplete.
""" """
config_file, exists = self._get_config_file_path() config_file, exists = self._get_config_file_path()
config_dir = config_file.parent config_dir = config_file.parent
# Create config directory and copy default config if file does not exist
if not exists: if not exists:
config_dir.mkdir(parents=True, exist_ok=True) config_dir.mkdir(parents=True, exist_ok=True)
try: try:
@ -366,18 +392,39 @@ class ConfigEOS(SingletonMixin, SettingsEOS):
config_file = self.config_default_file_path config_file = self.config_default_file_path
config_dir = config_file.parent config_dir = config_file.parent
# Load and validate the configuration file
with config_file.open("r", encoding=self.ENCODING) as f_in: with config_file.open("r", encoding=self.ENCODING) as f_in:
try: try:
json_str = f_in.read() json_str = f_in.read()
ConfigEOS._file_settings = SettingsEOS.model_validate_json(json_str) settings = SettingsEOS.model_validate_json(json_str)
except ValidationError as exc: except ValidationError as exc:
raise ValueError(f"Configuration '{config_file}' is incomplete or not valid: {exc}") raise ValueError(f"Configuration '{config_file}' is incomplete or not valid: {exc}")
return settings, config_file
def from_config_file(self) -> tuple[SettingsEOS, Path]:
"""Load the configuration file settings for EOS.
Returns:
tuple of settings and path
settings (SettingsEOS): The settings defined by the EOS configuration file.
path (pathlib.Path): The path of the configuration file.
Raises:
ValueError: If the configuration file is invalid or incomplete.
"""
# Load settings from config file
ConfigEOS._file_settings, config_file = self.settings_from_config_file()
# Update configuration in memory
self.update() self.update()
# Everthing worked, remember the values
self._config_folder_path = config_dir # Everything worked, remember the values
self._config_folder_path = config_file.parent
self._config_file_path = config_file self._config_file_path = config_file
return ConfigEOS._file_settings, config_file
def to_config_file(self) -> None: def to_config_file(self) -> None:
"""Saves the current configuration to the configuration file. """Saves the current configuration to the configuration file.

View File

@ -16,7 +16,7 @@ from typing import Any, ClassVar, Dict, Optional, Type
from pendulum import DateTime from pendulum import DateTime
from pydantic import computed_field from pydantic import computed_field
from akkudoktoreos.utils.logutil import get_logger from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -31,13 +31,13 @@ from pydantic import (
) )
from akkudoktoreos.core.coreabc import ConfigMixin, SingletonMixin, StartMixin from akkudoktoreos.core.coreabc import ConfigMixin, SingletonMixin, StartMixin
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import ( from akkudoktoreos.core.pydantic import (
PydanticBaseModel, PydanticBaseModel,
PydanticDateTimeData, PydanticDateTimeData,
PydanticDateTimeDataFrame, PydanticDateTimeDataFrame,
) )
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@ -583,20 +583,48 @@ class DataSequence(DataBase, MutableSequence):
# Sort the list by datetime after adding/updating # Sort the list by datetime after adding/updating
self.sort_by_datetime() self.sort_by_datetime()
def update_value(self, date: DateTime, key: str, value: Any) -> None: @overload
"""Updates a specific value in the data record for a given date. def update_value(self, date: DateTime, key: str, value: Any) -> None: ...
If a record for the date exists, updates the specified attribute with the new value. @overload
Otherwise, appends a new record with the given value and maintains chronological order. def update_value(self, date: DateTime, values: Dict[str, Any]) -> None: ...
def update_value(self, date: DateTime, *args: Any, **kwargs: Any) -> None:
"""Updates specific values in the data record for a given date.
If a record for the date exists, updates the specified attributes with the new values.
Otherwise, appends a new record with the given values and maintains chronological order.
Args: Args:
date (datetime): The date for which the weather value is to be added or updated. date (datetime): The date for which the values are to be added or updated.
key (str): The attribute name to be updated. key (str), value (Any): Single key-value pair to update
value: The new value to set for the specified attribute. OR
values (Dict[str, Any]): Dictionary of key-value pairs to update
OR
**kwargs: Key-value pairs as keyword arguments
Examples:
>>> update_value(date, 'temperature', 25.5)
>>> update_value(date, {'temperature': 25.5, 'humidity': 80})
>>> update_value(date, temperature=25.5, humidity=80)
""" """
self._validate_key_writable(key) # Process input arguments into a dictionary
values: Dict[str, Any] = {}
if len(args) == 2: # Single key-value pair
values[args[0]] = args[1]
elif len(args) == 1 and isinstance(args[0], dict): # Dictionary input
values.update(args[0])
elif len(args) > 0: # Invalid number of arguments
raise ValueError("Expected either 2 arguments (key, value) or 1 dictionary argument")
values.update(kwargs) # Add any keyword arguments
# Validate all keys are writable
for key in values:
self._validate_key_writable(key)
# Ensure datetime objects are normalized # Ensure datetime objects are normalized
date = to_datetime(date, to_maxtime=False) date = to_datetime(date, to_maxtime=False)
# Check if a record with the given date already exists # Check if a record with the given date already exists
for record in self.records: for record in self.records:
if not isinstance(record.date_time, DateTime): if not isinstance(record.date_time, DateTime):
@ -604,12 +632,13 @@ class DataSequence(DataBase, MutableSequence):
f"Record date '{record.date_time}' is not a datetime, but a `{type(record.date_time).__name__}`." f"Record date '{record.date_time}' is not a datetime, but a `{type(record.date_time).__name__}`."
) )
if compare_datetimes(record.date_time, date).equal: if compare_datetimes(record.date_time, date).equal:
# Update the DataRecord with the new value for the specified key # Update the DataRecord with all new values
setattr(record, key, value) for key, value in values.items():
setattr(record, key, value)
break break
else: else:
# Create a new record and append to the list # Create a new record and append to the list
record = self.record_class()(date_time=date, **{key: value}) record = self.record_class()(date_time=date, **values)
self.records.append(record) self.records.append(record)
# Sort the list by datetime after adding/updating # Sort the list by datetime after adding/updating
self.sort_by_datetime() self.sort_by_datetime()
@ -841,7 +870,7 @@ class DataSequence(DataBase, MutableSequence):
if start_index == 0: if start_index == 0:
# No value before start # No value before start
# Add dummy value # Add dummy value
dates.insert(0, dates[0] - interval) dates.insert(0, start_datetime - interval)
values.insert(0, values[0]) values.insert(0, values[0])
elif start_index > 1: elif start_index > 1:
# Truncate all values before latest value before start_datetime # Truncate all values before latest value before start_datetime

View File

@ -7,12 +7,12 @@ from pydantic import ConfigDict, Field, computed_field, field_validator, model_v
from typing_extensions import Self from typing_extensions import Self
from akkudoktoreos.core.coreabc import ConfigMixin, PredictionMixin, SingletonMixin from akkudoktoreos.core.coreabc import ConfigMixin, PredictionMixin, SingletonMixin
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import PydanticBaseModel from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.devices.battery import Battery from akkudoktoreos.devices.battery import Battery
from akkudoktoreos.devices.generic import HomeAppliance from akkudoktoreos.devices.generic import HomeAppliance
from akkudoktoreos.devices.inverter import Inverter from akkudoktoreos.devices.inverter import Inverter
from akkudoktoreos.utils.datetimeutil import to_datetime from akkudoktoreos.utils.datetimeutil import to_datetime
from akkudoktoreos.utils.logutil import get_logger
from akkudoktoreos.utils.utils import NumpyEncoder from akkudoktoreos.utils.utils import NumpyEncoder
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -0,0 +1,20 @@
"""Abstract and base classes for logging."""
import logging
def logging_str_to_level(level_str: str) -> int:
"""Convert log level string to logging level."""
if level_str == "DEBUG":
level = logging.DEBUG
elif level_str == "INFO":
level = logging.INFO
elif level_str == "WARNING":
level = logging.WARNING
elif level_str == "CRITICAL":
level = logging.CRITICAL
elif level_str == "ERROR":
level = logging.ERROR
else:
raise ValueError(f"Unknown loggin level: {level_str}")
return level

View File

@ -15,19 +15,21 @@ Notes:
- The logger supports rotating log files to prevent excessive log file size. - The logger supports rotating log files to prevent excessive log file size.
""" """
import logging import logging as pylogging
import os import os
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
from typing import Optional from typing import Optional
from akkudoktoreos.core.logabc import logging_str_to_level
def get_logger( def get_logger(
name: str, name: str,
log_file: Optional[str] = None, log_file: Optional[str] = None,
logging_level: Optional[str] = "INFO", logging_level: Optional[str] = None,
max_bytes: int = 5000000, max_bytes: int = 5000000,
backup_count: int = 5, backup_count: int = 5,
) -> logging.Logger: ) -> pylogging.Logger:
"""Creates and configures a logger with a given name. """Creates and configures a logger with a given name.
The logger supports logging to both the console and an optional log file. File logging is The logger supports logging to both the console and an optional log file. File logging is
@ -48,31 +50,22 @@ def get_logger(
logger.info("Application started") logger.info("Application started")
""" """
# Create a logger with the specified name # Create a logger with the specified name
logger = logging.getLogger(name) logger = pylogging.getLogger(name)
logger.propagate = True logger.propagate = True
if (env_level := os.getenv("EOS_LOGGING_LEVEL")) is not None: if logging_level is not None:
logging_level = env_level level = logging_str_to_level(logging_level)
if logging_level == "DEBUG": logger.setLevel(level)
level = logging.DEBUG
elif logging_level == "INFO":
level = logging.INFO
elif logging_level == "WARNING":
level = logging.WARNING
elif logging_level == "ERROR":
level = logging.ERROR
else:
level = logging.DEBUG
logger.setLevel(level)
# The log message format # The log message format
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") formatter = pylogging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
# Prevent loggers from being added multiple times # Prevent loggers from being added multiple times
# There may already be a logger from pytest # There may already be a logger from pytest
if not logger.handlers: if not logger.handlers:
# Create a console handler with a standard output stream # Create a console handler with a standard output stream
console_handler = logging.StreamHandler() console_handler = pylogging.StreamHandler()
console_handler.setLevel(level) if logging_level is not None:
console_handler.setLevel(level)
console_handler.setFormatter(formatter) console_handler.setFormatter(formatter)
# Add the console handler to the logger # Add the console handler to the logger
@ -88,7 +81,8 @@ def get_logger(
# Create a rotating file handler # Create a rotating file handler
file_handler = RotatingFileHandler(log_file, maxBytes=max_bytes, backupCount=backup_count) file_handler = RotatingFileHandler(log_file, maxBytes=max_bytes, backupCount=backup_count)
file_handler.setLevel(level) if logging_level is not None:
file_handler.setLevel(level)
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
# Add the file handler to the logger # Add the file handler to the logger

View File

@ -0,0 +1,45 @@
"""Settings for logging.
Kept in an extra module to avoid cyclic dependencies on package import.
"""
import logging
import os
from typing import Optional
from pydantic import Field, computed_field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logabc import logging_str_to_level
class LoggingCommonSettings(SettingsBaseModel):
"""Common settings for logging."""
logging_level_default: Optional[str] = Field(
default=None, description="EOS default logging level."
)
# Validators
@field_validator("logging_level_default", mode="after")
@classmethod
def set_default_logging_level(cls, value: Optional[str]) -> Optional[str]:
if isinstance(value, str) and value.upper() == "NONE":
value = None
if value is None and (env_level := os.getenv("EOS_LOGGING_LEVEL")) is not None:
# Take default logging level from special environment variable
value = env_level
if value is None:
return None
level = logging_str_to_level(value)
logging.getLogger().setLevel(level)
return value
# Computed fields
@computed_field # type: ignore[prop-decorator]
@property
def logging_level_root(self) -> str:
"""Root logger logging level."""
level = logging.getLogger().getEffectiveLevel()
level_name = logging.getLevelName(level)
return level_name

View File

@ -275,6 +275,7 @@ class PydanticDateTimeDataFrame(PydanticBaseModel):
) )
@field_validator("tz") @field_validator("tz")
@classmethod
def validate_timezone(cls, v: Optional[str]) -> Optional[str]: def validate_timezone(cls, v: Optional[str]) -> Optional[str]:
"""Validate that the timezone is valid.""" """Validate that the timezone is valid."""
if v is not None: if v is not None:

View File

@ -14,6 +14,7 @@
"load_name": null, "load_name": null,
"load_provider": null, "load_provider": null,
"loadakkudoktor_year_energy": null, "loadakkudoktor_year_energy": null,
"logging_level": "INFO",
"longitude": 13.4, "longitude": 13.4,
"optimization_ev_available_charge_rates_percent": null, "optimization_ev_available_charge_rates_percent": null,
"optimization_hours": 48, "optimization_hours": 48,

View File

@ -3,8 +3,8 @@ from typing import Any, Optional
import numpy as np import numpy as np
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.devices.devicesabc import DeviceBase from akkudoktoreos.devices.devicesabc import DeviceBase
from akkudoktoreos.utils.logutil import get_logger
from akkudoktoreos.utils.utils import NumpyEncoder from akkudoktoreos.utils.utils import NumpyEncoder
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -6,13 +6,13 @@ from pydantic import Field, computed_field
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.coreabc import SingletonMixin from akkudoktoreos.core.coreabc import SingletonMixin
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.devices.battery import Battery from akkudoktoreos.devices.battery import Battery
from akkudoktoreos.devices.devicesabc import DevicesBase from akkudoktoreos.devices.devicesabc import DevicesBase
from akkudoktoreos.devices.generic import HomeAppliance from akkudoktoreos.devices.generic import HomeAppliance
from akkudoktoreos.devices.inverter import Inverter from akkudoktoreos.devices.inverter import Inverter
from akkudoktoreos.prediction.interpolator import SelfConsumptionPropabilityInterpolator from akkudoktoreos.prediction.interpolator import SelfConsumptionPropabilityInterpolator
from akkudoktoreos.utils.datetimeutil import to_duration from akkudoktoreos.utils.datetimeutil import to_duration
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -10,9 +10,9 @@ from akkudoktoreos.core.coreabc import (
EnergyManagementSystemMixin, EnergyManagementSystemMixin,
PredictionMixin, PredictionMixin,
) )
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import PydanticBaseModel from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.utils.datetimeutil import to_duration from akkudoktoreos.utils.datetimeutil import to_duration
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -3,8 +3,8 @@ from typing import Optional
import numpy as np import numpy as np
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.devices.devicesabc import DeviceBase from akkudoktoreos.devices.devicesabc import DeviceBase
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -3,9 +3,9 @@ from typing import Optional
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from scipy.interpolate import RegularGridInterpolator from scipy.interpolate import RegularGridInterpolator
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.devices.battery import Battery from akkudoktoreos.devices.battery import Battery
from akkudoktoreos.devices.devicesabc import DeviceBase from akkudoktoreos.devices.devicesabc import DeviceBase
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -16,8 +16,8 @@ from pydantic import Field, computed_field
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.coreabc import SingletonMixin from akkudoktoreos.core.coreabc import SingletonMixin
from akkudoktoreos.core.dataabc import DataImportMixin, DataRecord, DataSequence from akkudoktoreos.core.dataabc import DataImportMixin, DataRecord, DataSequence
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.utils.datetimeutil import to_duration from akkudoktoreos.utils.datetimeutil import to_duration
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -3,7 +3,7 @@ from typing import List, Optional
from pydantic import Field from pydantic import Field
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.utils.logutil import get_logger from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -3,8 +3,8 @@
from pydantic import ConfigDict from pydantic import ConfigDict
from akkudoktoreos.core.coreabc import ConfigMixin, PredictionMixin from akkudoktoreos.core.coreabc import ConfigMixin, PredictionMixin
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import PydanticBaseModel from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -9,8 +9,8 @@ from typing import List, Optional
from pydantic import Field, computed_field from pydantic import Field, computed_field
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.predictionabc import PredictionProvider, PredictionRecord from akkudoktoreos.prediction.predictionabc import PredictionProvider, PredictionRecord
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -13,11 +13,11 @@ import requests
from numpydantic import NDArray, Shape from numpydantic import NDArray, Shape
from pydantic import Field, ValidationError from pydantic import Field, ValidationError
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import PydanticBaseModel from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.prediction.elecpriceabc import ElecPriceDataRecord, ElecPriceProvider from akkudoktoreos.prediction.elecpriceabc import ElecPriceDataRecord, ElecPriceProvider
from akkudoktoreos.utils.cacheutil import CacheFileStore, cache_in_file from akkudoktoreos.utils.cacheutil import CacheFileStore, cache_in_file
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@ -218,17 +218,14 @@ class ElecPriceAkkudoktor(ElecPriceProvider):
akkudoktor_value.marketpriceEurocentPerKWh / (100 * 1000) + charges_kwh / 1000 akkudoktor_value.marketpriceEurocentPerKWh / (100 * 1000) + charges_kwh / 1000
) )
if compare_datetimes(dt, self.start_datetime).lt: # We provide prediction starting at start of day, to be compatible to old system.
# forecast data is too old if compare_datetimes(dt, self.start_datetime.start_of("day")).lt:
# forecast data is too old - older than start_datetime with time set to 00:00:00
self.elecprice_8days[dt.hour, dt.day_of_week] = price_wh self.elecprice_8days[dt.hour, dt.day_of_week] = price_wh
continue continue
self.elecprice_8days[dt.hour, 7] = price_wh self.elecprice_8days[dt.hour, 7] = price_wh
record = ElecPriceDataRecord( self.update_value(dt, "elecprice_marketprice_wh", price_wh)
date_time=dt,
elecprice_marketprice_wh=price_wh,
)
self.append(record)
# Update 8day cache # Update 8day cache
elecprice_cache_file.seek(0) elecprice_cache_file.seek(0)

View File

@ -12,9 +12,9 @@ from typing import Optional, Union
from pydantic import Field, field_validator from pydantic import Field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider from akkudoktoreos.prediction.elecpriceabc import ElecPriceProvider
from akkudoktoreos.prediction.predictionabc import PredictionImportProvider from akkudoktoreos.prediction.predictionabc import PredictionImportProvider
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -5,12 +5,14 @@ from typing import Optional
from pydantic import Field from pydantic import Field
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.utils.logutil import get_logger from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
class LoadCommonSettings(SettingsBaseModel): class LoadCommonSettings(SettingsBaseModel):
"""Common settings for loaod forecast providers."""
load_provider: Optional[str] = Field( 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."
) )

View File

@ -9,8 +9,8 @@ from typing import List, Optional
from pydantic import Field from pydantic import Field
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.predictionabc import PredictionProvider, PredictionRecord from akkudoktoreos.prediction.predictionabc import PredictionProvider, PredictionRecord
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -1,15 +1,14 @@
"""Retrieves load forecast data from Akkudoktor load profiles.""" """Retrieves load forecast data from Akkudoktor load profiles."""
from pathlib import Path
from typing import Optional from typing import Optional
import numpy as np import numpy as np
from pydantic import Field from pydantic import Field
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.loadabc import LoadProvider from akkudoktoreos.prediction.loadabc import LoadProvider
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@ -84,7 +83,7 @@ class LoadAkkudoktor(LoadProvider):
def load_data(self) -> np.ndarray: def load_data(self) -> np.ndarray:
"""Loads data from the Akkudoktor load file.""" """Loads data from the Akkudoktor load file."""
load_file = Path(__file__).parent.parent.joinpath("data/load_profiles.npz") load_file = self.config.package_root_path.joinpath("data/load_profiles.npz")
data_year_energy = None data_year_energy = None
try: try:
file_data = np.load(load_file) file_data = np.load(load_file)
@ -107,23 +106,25 @@ class LoadAkkudoktor(LoadProvider):
"""Adds the load means and standard deviations.""" """Adds the load means and standard deviations."""
data_year_energy = self.load_data() data_year_energy = self.load_data()
weekday_adjust, weekend_adjust = self._calculate_adjustment(data_year_energy) weekday_adjust, weekend_adjust = self._calculate_adjustment(data_year_energy)
date = self.start_datetime # We provide prediction starting at start of day, to be compatible to old system.
for i in range(self.config.prediction_hours): # End date for prediction is prediction hours from now.
date = self.start_datetime.start_of("day")
end_date = self.start_datetime.add(hours=self.config.prediction_hours)
while compare_datetimes(date, end_date).lt:
# Extract mean (index 0) and standard deviation (index 1) for the given day and hour # Extract mean (index 0) and standard deviation (index 1) for the given day and hour
# Day indexing starts at 0, -1 because of that # Day indexing starts at 0, -1 because of that
hourly_stats = data_year_energy[date.day_of_year - 1, :, date.hour] hourly_stats = data_year_energy[date.day_of_year - 1, :, date.hour]
self.update_value(date, "load_mean", hourly_stats[0]) values = {
self.update_value(date, "load_std", hourly_stats[1]) "load_mean": hourly_stats[0],
"load_std": hourly_stats[1],
}
if date.day_of_week < 5: if date.day_of_week < 5:
# Monday to Friday (0..4) # Monday to Friday (0..4)
self.update_value( values["load_mean_adjusted"] = hourly_stats[0] + weekday_adjust[date.hour]
date, "load_mean_adjusted", hourly_stats[0] + weekday_adjust[date.hour]
)
else: else:
# Saturday, Sunday (5, 6) # Saturday, Sunday (5, 6)
self.update_value( values["load_mean_adjusted"] = hourly_stats[0] + weekend_adjust[date.hour]
date, "load_mean_adjusted", hourly_stats[0] + weekend_adjust[date.hour] self.update_value(date, values)
)
date += to_duration("1 hour") date += to_duration("1 hour")
# We are working on fresh data (no cache), report update time # We are working on fresh data (no cache), report update time
self.update_datetime = to_datetime(in_timezone=self.config.timezone) self.update_datetime = to_datetime(in_timezone=self.config.timezone)

View File

@ -12,9 +12,9 @@ from typing import Optional, Union
from pydantic import Field, field_validator from pydantic import Field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.loadabc import LoadProvider from akkudoktoreos.prediction.loadabc import LoadProvider
from akkudoktoreos.prediction.predictionabc import PredictionImportProvider from akkudoktoreos.prediction.predictionabc import PredictionImportProvider
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -22,8 +22,8 @@ from akkudoktoreos.core.dataabc import (
DataRecord, DataRecord,
DataSequence, DataSequence,
) )
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.utils.datetimeutil import to_duration from akkudoktoreos.utils.datetimeutil import to_duration
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -5,7 +5,7 @@ from typing import Any, ClassVar, List, Optional
from pydantic import Field, computed_field from pydantic import Field, computed_field
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.utils.logutil import get_logger from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@ -43,7 +43,7 @@ class PVForecastCommonSettings(SettingsBaseModel):
description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.", description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.",
) )
pvforecast0_loss: Optional[float] = Field( pvforecast0_loss: Optional[float] = Field(
default=None, description="Sum of PV system losses in percent" default=14.0, description="Sum of PV system losses in percent"
) )
pvforecast0_trackingtype: Optional[int] = Field( pvforecast0_trackingtype: Optional[int] = Field(
default=None, default=None,
@ -98,7 +98,9 @@ class PVForecastCommonSettings(SettingsBaseModel):
default="free", default="free",
description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.", description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.",
) )
pvforecast1_loss: Optional[float] = Field(0, description="Sum of PV system losses in percent") pvforecast1_loss: Optional[float] = Field(
default=14.0, description="Sum of PV system losses in percent"
)
pvforecast1_trackingtype: Optional[int] = Field( pvforecast1_trackingtype: Optional[int] = Field(
default=None, 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.", 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.",
@ -152,7 +154,9 @@ class PVForecastCommonSettings(SettingsBaseModel):
default="free", default="free",
description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.", description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.",
) )
pvforecast2_loss: Optional[float] = Field(0, description="Sum of PV system losses in percent") pvforecast2_loss: Optional[float] = Field(
default=14.0, description="Sum of PV system losses in percent"
)
pvforecast2_trackingtype: Optional[int] = Field( pvforecast2_trackingtype: Optional[int] = Field(
default=None, 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.", 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.",
@ -206,7 +210,9 @@ class PVForecastCommonSettings(SettingsBaseModel):
default="free", default="free",
description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.", description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.",
) )
pvforecast3_loss: Optional[float] = Field(0, description="Sum of PV system losses in percent") pvforecast3_loss: Optional[float] = Field(
default=14.0, description="Sum of PV system losses in percent"
)
pvforecast3_trackingtype: Optional[int] = Field( pvforecast3_trackingtype: Optional[int] = Field(
default=None, 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.", 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.",
@ -260,7 +266,9 @@ class PVForecastCommonSettings(SettingsBaseModel):
default="free", default="free",
description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.", description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.",
) )
pvforecast4_loss: Optional[float] = Field(0, description="Sum of PV system losses in percent") pvforecast4_loss: Optional[float] = Field(
default=14.0, description="Sum of PV system losses in percent"
)
pvforecast4_trackingtype: Optional[int] = Field( pvforecast4_trackingtype: Optional[int] = Field(
default=None, 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.", 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.",
@ -314,7 +322,9 @@ class PVForecastCommonSettings(SettingsBaseModel):
default="free", default="free",
description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.", description="Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.",
) )
pvforecast5_loss: Optional[float] = Field(0, description="Sum of PV system losses in percent") pvforecast5_loss: Optional[float] = Field(
default=14.0, description="Sum of PV system losses in percent"
)
pvforecast5_trackingtype: Optional[int] = Field( pvforecast5_trackingtype: Optional[int] = Field(
default=None, 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.", 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.",

View File

@ -9,8 +9,8 @@ from typing import List, Optional
from pydantic import Field from pydantic import Field
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.predictionabc import PredictionProvider, PredictionRecord from akkudoktoreos.prediction.predictionabc import PredictionProvider, PredictionRecord
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -68,6 +68,7 @@ from typing import Any, List, Optional, Union
import requests import requests
from pydantic import Field, ValidationError, computed_field from pydantic import Field, ValidationError, computed_field
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import PydanticBaseModel from akkudoktoreos.core.pydantic import PydanticBaseModel
from akkudoktoreos.prediction.pvforecastabc import ( from akkudoktoreos.prediction.pvforecastabc import (
PVForecastDataRecord, PVForecastDataRecord,
@ -75,7 +76,6 @@ from akkudoktoreos.prediction.pvforecastabc import (
) )
from akkudoktoreos.utils.cacheutil import cache_in_file from akkudoktoreos.utils.cacheutil import cache_in_file
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@ -283,27 +283,21 @@ class PVForecastAkkudoktor(PVForecastProvider):
original_datetime = akkudoktor_data.values[0][i].datetime original_datetime = akkudoktor_data.values[0][i].datetime
dt = to_datetime(original_datetime, in_timezone=self.config.timezone) dt = to_datetime(original_datetime, in_timezone=self.config.timezone)
# iso_datetime = parser.parse(original_datetime).isoformat() # Konvertiere zu ISO-Format # We provide prediction starting at start of day, to be compatible to old system.
# print() if compare_datetimes(dt, self.start_datetime.start_of("day")).lt:
# Optional: 2 Stunden abziehen, um die Zeitanpassung zu testen
# adjusted_datetime = parser.parse(original_datetime) - timedelta(hours=2)
# print(f"Angepasste Zeitstempel: {adjusted_datetime.isoformat()}")
if compare_datetimes(dt, self.start_datetime).lt:
# forecast data is too old # forecast data is too old
continue continue
sum_dc_power = sum(values[i].dcPower for values in akkudoktor_data.values) sum_dc_power = sum(values[i].dcPower for values in akkudoktor_data.values)
sum_ac_power = sum(values[i].power for values in akkudoktor_data.values) sum_ac_power = sum(values[i].power for values in akkudoktor_data.values)
record = PVForecastAkkudoktorDataRecord( data = {
date_time=dt, # Verwende angepassten Zeitstempel "pvforecast_dc_power": sum_dc_power,
pvforecast_dc_power=sum_dc_power, "pvforecast_ac_power": sum_ac_power,
pvforecast_ac_power=sum_ac_power, "pvforecastakkudoktor_wind_speed_10m": akkudoktor_data.values[0][i].windspeed_10m,
pvforecastakkudoktor_wind_speed_10m=akkudoktor_data.values[0][i].windspeed_10m, "pvforecastakkudoktor_temp_air": akkudoktor_data.values[0][i].temperature,
pvforecastakkudoktor_temp_air=akkudoktor_data.values[0][i].temperature, }
) self.update_value(dt, data)
self.append(record)
if len(self) < self.config.prediction_hours: if len(self) < self.config.prediction_hours:
raise ValueError( raise ValueError(

View File

@ -12,9 +12,9 @@ from typing import Optional, Union
from pydantic import Field, field_validator from pydantic import Field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.predictionabc import PredictionImportProvider from akkudoktoreos.prediction.predictionabc import PredictionImportProvider
from akkudoktoreos.prediction.pvforecastabc import PVForecastProvider from akkudoktoreos.prediction.pvforecastabc import PVForecastProvider
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -14,8 +14,8 @@ import pandas as pd
import pvlib import pvlib
from pydantic import Field from pydantic import Field
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.predictionabc import PredictionProvider, PredictionRecord from akkudoktoreos.prediction.predictionabc import PredictionProvider, PredictionRecord
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -13,10 +13,10 @@ import pandas as pd
import pvlib import pvlib
import requests import requests
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.weatherabc import WeatherDataRecord, WeatherProvider from akkudoktoreos.prediction.weatherabc import WeatherDataRecord, WeatherProvider
from akkudoktoreos.utils.cacheutil import cache_in_file from akkudoktoreos.utils.cacheutil import cache_in_file
from akkudoktoreos.utils.datetimeutil import to_datetime from akkudoktoreos.utils.datetimeutil import to_datetime
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -19,10 +19,10 @@ import pandas as pd
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.weatherabc import WeatherDataRecord, WeatherProvider from akkudoktoreos.prediction.weatherabc import WeatherDataRecord, WeatherProvider
from akkudoktoreos.utils.cacheutil import cache_in_file from akkudoktoreos.utils.cacheutil import cache_in_file
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration, to_timezone from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration, to_timezone
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -12,9 +12,9 @@ from typing import Optional, Union
from pydantic import Field, field_validator from pydantic import Field, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.prediction.predictionabc import PredictionImportProvider from akkudoktoreos.prediction.predictionabc import PredictionImportProvider
from akkudoktoreos.prediction.weatherabc import WeatherProvider from akkudoktoreos.prediction.weatherabc import WeatherProvider
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -7,7 +7,6 @@ from pathlib import Path
from typing import Annotated, Any, AsyncGenerator, Dict, List, Optional, Union from typing import Annotated, Any, AsyncGenerator, Dict, List, Optional, Union
import httpx import httpx
import pandas as pd
import uvicorn import uvicorn
from fastapi import FastAPI, Query, Request from fastapi import FastAPI, Query, Request
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
@ -15,6 +14,7 @@ from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse, Resp
from akkudoktoreos.config.config import ConfigEOS, SettingsEOS, get_config from akkudoktoreos.config.config import ConfigEOS, SettingsEOS, get_config
from akkudoktoreos.core.ems import get_ems from akkudoktoreos.core.ems import get_ems
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.core.pydantic import ( from akkudoktoreos.core.pydantic import (
PydanticBaseModel, PydanticBaseModel,
PydanticDateTimeData, PydanticDateTimeData,
@ -29,7 +29,6 @@ from akkudoktoreos.optimization.genetic import (
) )
from akkudoktoreos.prediction.prediction import get_prediction from akkudoktoreos.prediction.prediction import get_prediction
from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration from akkudoktoreos.utils.datetimeutil import to_datetime, to_duration
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
config_eos = get_config() config_eos = get_config()
@ -182,33 +181,112 @@ class PdfResponse(FileResponse):
media_type = "application/pdf" media_type = "application/pdf"
@app.put("/v1/config/value")
def fastapi_config_value_put(
key: Annotated[str, Query(description="configuration key")],
value: Annotated[Any, Query(description="configuration value")],
) -> ConfigEOS:
"""Set the configuration option in the settings.
Args:
key (str): configuration key
value (Any): configuration value
Returns:
configuration (ConfigEOS): The current configuration after the write.
"""
if key not in config_eos.config_keys:
raise HTTPException(status_code=404, detail=f"Key '{key}' is not available.")
if key in config_eos.config_keys_read_only:
raise HTTPException(status_code=404, detail=f"Key '{key}' is read only.")
try:
setattr(config_eos, key, value)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Error on update of configuration: {e}")
return config_eos
@app.post("/v1/config/update")
def fastapi_config_update_post() -> ConfigEOS:
"""Update the configuration from the EOS configuration file.
Returns:
configuration (ConfigEOS): The current configuration after update.
"""
try:
_, config_file_path = config_eos.from_config_file()
except:
raise HTTPException(
status_code=404,
detail=f"Cannot update configuration from file '{config_file_path}'.",
)
return config_eos
@app.get("/v1/config/file")
def fastapi_config_file_get() -> SettingsEOS:
"""Get the settings as defined by the EOS configuration file.
Returns:
settings (SettingsEOS): The settings defined by the EOS configuration file.
"""
try:
settings, config_file_path = config_eos.settings_from_config_file()
except:
raise HTTPException(
status_code=404,
detail=f"Cannot read configuration from file '{config_file_path}'.",
)
return settings
@app.put("/v1/config/file")
def fastapi_config_file_put() -> ConfigEOS:
"""Save the current configuration to the EOS configuration file.
Returns:
configuration (ConfigEOS): The current configuration that was saved.
"""
try:
config_eos.to_config_file()
except:
raise HTTPException(
status_code=404,
detail=f"Cannot save configuration to file '{config_eos.config_file_path}'.",
)
return config_eos
@app.get("/v1/config") @app.get("/v1/config")
def fastapi_config_get() -> ConfigEOS: def fastapi_config_get() -> ConfigEOS:
"""Get the current configuration.""" """Get the current configuration.
Returns:
configuration (ConfigEOS): The current configuration.
"""
return config_eos return config_eos
@app.put("/v1/config") @app.put("/v1/config")
def fastapi_config_put( def fastapi_config_put(
settings: SettingsEOS, settings: Annotated[SettingsEOS, Query(description="settings")],
save: Optional[bool] = None,
) -> ConfigEOS: ) -> ConfigEOS:
"""Merge settings into current configuration. """Write the provided settings into the current settings.
The existing settings are completely overwritten. Note that for any setting
value that is None, the configuration will fall back to values from other sources such as
environment variables, the EOS configuration file, or default values.
Args: Args:
settings (SettingsEOS): The settings to merge into the current configuration. settings (SettingsEOS): The settings to write into the current settings.
save (Optional[bool]): Save the resulting configuration to the configuration file.
Defaults to False. Returns:
configuration (ConfigEOS): The current configuration after the write.
""" """
config_eos.merge_settings(settings) try:
if save: config_eos.merge_settings(settings, force=True)
try: except Exception as e:
config_eos.to_config_file() raise HTTPException(status_code=400, detail=f"Error on update of configuration: {e}")
except:
raise HTTPException(
status_code=404,
detail=f"Cannot save configuration to file '{config_eos.config_file_path}'.",
)
return config_eos return config_eos
@ -226,10 +304,10 @@ def fastapi_measurement_load_mr_series_by_name_get(
key = measurement_eos.name_to_key(name=name, topic="measurement_load") key = measurement_eos.name_to_key(name=name, topic="measurement_load")
if key is None: if key is None:
raise HTTPException( raise HTTPException(
status_code=404, detail=f"Measurement load with name '{name}' not available." status_code=404, detail=f"Measurement load with name '{name}' is not available."
) )
if key not in measurement_eos.record_keys: if key not in measurement_eos.record_keys:
raise HTTPException(status_code=404, detail=f"Key '{key}' not available.") raise HTTPException(status_code=404, detail=f"Key '{key}' is not available.")
pdseries = measurement_eos.key_to_series(key=key) pdseries = measurement_eos.key_to_series(key=key)
return PydanticDateTimeSeries.from_series(pdseries) return PydanticDateTimeSeries.from_series(pdseries)
@ -244,10 +322,10 @@ def fastapi_measurement_load_mr_value_by_name_put(
key = measurement_eos.name_to_key(name=name, topic="measurement_load") key = measurement_eos.name_to_key(name=name, topic="measurement_load")
if key is None: if key is None:
raise HTTPException( raise HTTPException(
status_code=404, detail=f"Measurement load with name '{name}' not available." status_code=404, detail=f"Measurement load with name '{name}' is not available."
) )
if key not in measurement_eos.record_keys: if key not in measurement_eos.record_keys:
raise HTTPException(status_code=404, detail=f"Key '{key}' not available.") raise HTTPException(status_code=404, detail=f"Key '{key}' is not available.")
measurement_eos.update_value(datetime, key, value) measurement_eos.update_value(datetime, key, value)
pdseries = measurement_eos.key_to_series(key=key) pdseries = measurement_eos.key_to_series(key=key)
return PydanticDateTimeSeries.from_series(pdseries) return PydanticDateTimeSeries.from_series(pdseries)
@ -261,10 +339,10 @@ def fastapi_measurement_load_mr_series_by_name_put(
key = measurement_eos.name_to_key(name=name, topic="measurement_load") key = measurement_eos.name_to_key(name=name, topic="measurement_load")
if key is None: if key is None:
raise HTTPException( raise HTTPException(
status_code=404, detail=f"Measurement load with name '{name}' not available." status_code=404, detail=f"Measurement load with name '{name}' is not available."
) )
if key not in measurement_eos.record_keys: if key not in measurement_eos.record_keys:
raise HTTPException(status_code=404, detail=f"Key '{key}' not available.") raise HTTPException(status_code=404, detail=f"Key '{key}' is not available.")
pdseries = series.to_series() # make pandas series from PydanticDateTimeSeries pdseries = series.to_series() # make pandas series from PydanticDateTimeSeries
measurement_eos.key_from_series(key=key, series=pdseries) measurement_eos.key_from_series(key=key, series=pdseries)
pdseries = measurement_eos.key_to_series(key=key) pdseries = measurement_eos.key_to_series(key=key)
@ -277,7 +355,7 @@ def fastapi_measurement_series_get(
) -> PydanticDateTimeSeries: ) -> PydanticDateTimeSeries:
"""Get the measurements of given key as series.""" """Get the measurements of given key as series."""
if key not in measurement_eos.record_keys: if key not in measurement_eos.record_keys:
raise HTTPException(status_code=404, detail=f"Key '{key}' not available.") raise HTTPException(status_code=404, detail=f"Key '{key}' is not available.")
pdseries = measurement_eos.key_to_series(key=key) pdseries = measurement_eos.key_to_series(key=key)
return PydanticDateTimeSeries.from_series(pdseries) return PydanticDateTimeSeries.from_series(pdseries)
@ -290,7 +368,7 @@ def fastapi_measurement_value_put(
) -> PydanticDateTimeSeries: ) -> PydanticDateTimeSeries:
"""Merge the measurement of given key and value into EOS measurements at given datetime.""" """Merge the measurement of given key and value into EOS measurements at given datetime."""
if key not in measurement_eos.record_keys: if key not in measurement_eos.record_keys:
raise HTTPException(status_code=404, detail=f"Key '{key}' not available.") raise HTTPException(status_code=404, detail=f"Key '{key}' is not available.")
measurement_eos.update_value(datetime, key, value) measurement_eos.update_value(datetime, key, value)
pdseries = measurement_eos.key_to_series(key=key) pdseries = measurement_eos.key_to_series(key=key)
return PydanticDateTimeSeries.from_series(pdseries) return PydanticDateTimeSeries.from_series(pdseries)
@ -302,7 +380,7 @@ def fastapi_measurement_series_put(
) -> PydanticDateTimeSeries: ) -> PydanticDateTimeSeries:
"""Merge measurement given as series into given key.""" """Merge measurement given as series into given key."""
if key not in measurement_eos.record_keys: if key not in measurement_eos.record_keys:
raise HTTPException(status_code=404, detail=f"Key '{key}' not available.") raise HTTPException(status_code=404, detail=f"Key '{key}' is not available.")
pdseries = series.to_series() # make pandas series from PydanticDateTimeSeries pdseries = series.to_series() # make pandas series from PydanticDateTimeSeries
measurement_eos.key_from_series(key=key, series=pdseries) measurement_eos.key_from_series(key=key, series=pdseries)
pdseries = measurement_eos.key_to_series(key=key) pdseries = measurement_eos.key_to_series(key=key)
@ -351,7 +429,7 @@ def fastapi_prediction_series_get(
Defaults to end datetime of latest prediction. Defaults to end datetime of latest prediction.
""" """
if key not in prediction_eos.record_keys: if key not in prediction_eos.record_keys:
raise HTTPException(status_code=404, detail=f"Key '{key}' not available.") raise HTTPException(status_code=404, detail=f"Key '{key}' is not available.")
if start_datetime is None: if start_datetime is None:
start_datetime = prediction_eos.start_datetime start_datetime = prediction_eos.start_datetime
else: else:
@ -394,7 +472,7 @@ def fastapi_prediction_list_get(
Defaults to 1 hour. Defaults to 1 hour.
""" """
if key not in prediction_eos.record_keys: if key not in prediction_eos.record_keys:
raise HTTPException(status_code=404, detail=f"Key '{key}' not available.") raise HTTPException(status_code=404, detail=f"Key '{key}' is not available.")
if start_datetime is None: if start_datetime is None:
start_datetime = prediction_eos.start_datetime start_datetime = prediction_eos.start_datetime
else: else:
@ -429,7 +507,7 @@ def fastapi_prediction_update(force_update: bool = False, force_enable: bool = F
try: try:
prediction_eos.update_data(force_update=force_update, force_enable=force_enable) prediction_eos.update_data(force_update=force_update, force_enable=force_enable)
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"Error while trying to update provider: {e}") raise HTTPException(status_code=400, detail=f"Error on update of provider: {e}")
return Response() return Response()
@ -453,7 +531,7 @@ def fastapi_prediction_update_provider(
try: try:
provider.update_data(force_update=force_update, force_enable=force_enable) provider.update_data(force_update=force_update, force_enable=force_enable)
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"Error while trying to update provider: {e}") raise HTTPException(status_code=400, detail=f"Error on update of provider: {e}")
return Response() return Response()
@ -461,6 +539,13 @@ def fastapi_prediction_update_provider(
def fastapi_strompreis() -> list[float]: def fastapi_strompreis() -> list[float]:
"""Deprecated: Electricity Market Price Prediction per Wh (€/Wh). """Deprecated: Electricity Market Price Prediction per Wh (€/Wh).
Electricity prices start at 00.00.00 today and are provided for 48 hours.
If no prices are available the missing ones at the start of the series are
filled with the first available price.
Note:
Electricity price charges are added.
Note: Note:
Set ElecPriceAkkudoktor as elecprice_provider, then update data with Set ElecPriceAkkudoktor as elecprice_provider, then update data with
'/v1/prediction/update' '/v1/prediction/update'
@ -479,11 +564,21 @@ def fastapi_strompreis() -> list[float]:
# Get the current date and the end date based on prediction hours # Get the current date and the end date based on prediction hours
# Fetch prices for the specified date range # Fetch prices for the specified date range
return prediction_eos.key_to_array( start_datetime = to_datetime().start_of("day")
key="elecprice_marketprice_wh", end_datetime = start_datetime.add(days=2)
start_datetime=prediction_eos.start_datetime, try:
end_datetime=prediction_eos.end_datetime, elecprice = prediction_eos.key_to_array(
).tolist() key="elecprice_marketprice_wh",
start_datetime=start_datetime,
end_datetime=end_datetime,
).tolist()
except Exception as e:
raise HTTPException(
status_code=404,
detail=f"Can not get the electricity price forecast: {e}. Did you configure the electricity price forecast provider?",
)
return elecprice
class GesamtlastRequest(PydanticBaseModel): class GesamtlastRequest(PydanticBaseModel):
@ -498,6 +593,10 @@ def fastapi_gesamtlast(request: GesamtlastRequest) -> list[float]:
Endpoint to handle total load prediction adjusted by latest measured data. Endpoint to handle total load prediction adjusted by latest measured data.
Total load prediction starts at 00.00.00 today and is provided for 48 hours.
If no prediction values are available the missing ones at the start of the series are
filled with the first available prediction value.
Note: Note:
Use '/v1/prediction/list?key=load_mean_adjusted' instead. Use '/v1/prediction/list?key=load_mean_adjusted' instead.
Load energy meter readings to be added to EOS measurement by: Load energy meter readings to be added to EOS measurement by:
@ -534,11 +633,21 @@ def fastapi_gesamtlast(request: GesamtlastRequest) -> list[float]:
# Create load forecast # Create load forecast
prediction_eos.update_data(force_update=True) prediction_eos.update_data(force_update=True)
prediction_list = prediction_eos.key_to_array( # Get the forcast starting at start of day
key="load_mean_adjusted", start_datetime = to_datetime().start_of("day")
start_datetime=prediction_eos.start_datetime, end_datetime = start_datetime.add(days=2)
end_datetime=prediction_eos.end_datetime, try:
).tolist() prediction_list = prediction_eos.key_to_array(
key="load_mean_adjusted",
start_datetime=start_datetime,
end_datetime=end_datetime,
).tolist()
except Exception as e:
raise HTTPException(
status_code=404,
detail=f"Can not get the total load forecast: {e}. Did you configure the load forecast provider?",
)
return prediction_list return prediction_list
@ -548,6 +657,10 @@ def fastapi_gesamtlast_simple(year_energy: float) -> list[float]:
Endpoint to handle total load prediction. Endpoint to handle total load prediction.
Total load prediction starts at 00.00.00 today and is provided for 48 hours.
If no prediction values are available the missing ones at the start of the series are
filled with the first available prediction value.
Note: Note:
Set LoadAkkudoktor as load_provider, then update data with Set LoadAkkudoktor as load_provider, then update data with
'/v1/prediction/update' '/v1/prediction/update'
@ -564,11 +677,21 @@ def fastapi_gesamtlast_simple(year_energy: float) -> list[float]:
# Create load forecast # Create load forecast
prediction_eos.update_data(force_update=True) prediction_eos.update_data(force_update=True)
prediction_list = prediction_eos.key_to_array( # Get the forcast starting at start of day
key="load_mean", start_datetime = to_datetime().start_of("day")
start_datetime=prediction_eos.start_datetime, end_datetime = start_datetime.add(days=2)
end_datetime=prediction_eos.end_datetime, try:
).tolist() prediction_list = prediction_eos.key_to_array(
key="load_mean",
start_datetime=start_datetime,
end_datetime=end_datetime,
).tolist()
except Exception as e:
raise HTTPException(
status_code=404,
detail=f"Can not get the total load forecast: {e}. Did you configure the load forecast provider?",
)
return prediction_list return prediction_list
@ -583,6 +706,10 @@ def fastapi_pvforecast() -> ForecastResponse:
Endpoint to handle PV forecast prediction. Endpoint to handle PV forecast prediction.
PVForecast starts at 00.00.00 today and is provided for 48 hours.
If no forecast values are available the missing ones at the start of the series are
filled with the first available forecast value.
Note: Note:
Set PVForecastAkkudoktor as pvforecast_provider, then update data with Set PVForecastAkkudoktor as pvforecast_provider, then update data with
'/v1/prediction/update' '/v1/prediction/update'
@ -590,41 +717,38 @@ def fastapi_pvforecast() -> ForecastResponse:
'/v1/prediction/list?key=pvforecast_ac_power' and '/v1/prediction/list?key=pvforecast_ac_power' and
'/v1/prediction/list?key=pvforecastakkudoktor_temp_air' instead. '/v1/prediction/list?key=pvforecastakkudoktor_temp_air' instead.
""" """
############### settings = SettingsEOS(
# PV Forecast elecprice_provider="PVForecastAkkudoktor",
############### )
prediction_key = "pvforecast_ac_power" config_eos.merge_settings(settings=settings)
pvforecast_ac_power = prediction_eos.get(prediction_key)
if pvforecast_ac_power is None:
raise HTTPException(status_code=404, detail=f"Prediction not available: {prediction_key}")
# On empty Series.loc TypeError: Cannot compare tz-naive and tz-aware datetime-like objects ems_eos.set_start_datetime() # Set energy management start datetime to current hour.
if len(pvforecast_ac_power) == 0:
pvforecast_ac_power = pd.Series()
else:
# Fetch prices for the specified date range
pvforecast_ac_power = pvforecast_ac_power.loc[
prediction_eos.start_datetime : prediction_eos.end_datetime
]
prediction_key = "pvforecastakkudoktor_temp_air" # Create PV forecast
pvforecastakkudoktor_temp_air = prediction_eos.get(prediction_key) prediction_eos.update_data(force_update=True)
if pvforecastakkudoktor_temp_air is None:
raise HTTPException(status_code=404, detail=f"Prediction not available: {prediction_key}")
# On empty Series.loc TypeError: Cannot compare tz-naive and tz-aware datetime-like objects # Get the forcast starting at start of day
if len(pvforecastakkudoktor_temp_air) == 0: start_datetime = to_datetime().start_of("day")
pvforecastakkudoktor_temp_air = pd.Series() end_datetime = start_datetime.add(days=2)
else: try:
# Fetch prices for the specified date range ac_power = prediction_eos.key_to_array(
pvforecastakkudoktor_temp_air = pvforecastakkudoktor_temp_air.loc[ key="pvforecast_ac_power",
prediction_eos.start_datetime : prediction_eos.end_datetime start_datetime=start_datetime,
] end_datetime=end_datetime,
).tolist()
temp_air = prediction_eos.key_to_array(
key="pvforecastakkudoktor_temp_air",
start_datetime=start_datetime,
end_datetime=end_datetime,
).tolist()
except Exception as e:
raise HTTPException(
status_code=404,
detail=f"Can not get the PV forecast: {e}. Did you configure the PV forecast provider?",
)
# Return both forecasts as a JSON response # Return both forecasts as a JSON response
return ForecastResponse( return ForecastResponse(temperature=temp_air, pvpower=ac_power)
temperature=pvforecastakkudoktor_temp_air.tolist(), pvpower=pvforecast_ac_power.tolist()
)
@app.post("/optimize") @app.post("/optimize")

View File

@ -2,7 +2,7 @@ import uvicorn
from fasthtml.common import H1, FastHTML, Table, Td, Th, Thead, Titled, Tr from fasthtml.common import H1, FastHTML, Table, Td, Th, Thead, Titled, Tr
from akkudoktoreos.config.config import get_config from akkudoktoreos.config.config import get_config
from akkudoktoreos.utils.logutil import get_logger from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -5,7 +5,7 @@ from typing import Optional
from pydantic import Field, IPvAnyAddress, field_validator from pydantic import Field, IPvAnyAddress, field_validator
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.utils.logutil import get_logger from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -48,8 +48,8 @@ from pendulum import DateTime, Duration
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from akkudoktoreos.core.coreabc import ConfigMixin from akkudoktoreos.core.coreabc import ConfigMixin
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration from akkudoktoreos.utils.datetimeutil import compare_datetimes, to_datetime, to_duration
from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -31,7 +31,7 @@ from pendulum import Date, DateTime, Duration
from pendulum.tz.timezone import Timezone from pendulum.tz.timezone import Timezone
from timezonefinder import TimezoneFinder from timezonefinder import TimezoneFinder
from akkudoktoreos.utils.logutil import get_logger from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -4,7 +4,7 @@ from typing import Any
import numpy as np import numpy as np
from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.config.configabc import SettingsBaseModel
from akkudoktoreos.utils.logutil import get_logger from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -12,7 +12,7 @@ import pytest
from xprocess import ProcessStarter from xprocess import ProcessStarter
from akkudoktoreos.config.config import ConfigEOS, get_config from akkudoktoreos.config.config import ConfigEOS, get_config
from akkudoktoreos.utils.logutil import get_logger from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@ -160,7 +160,7 @@ def server(xprocess, config_eos, config_default_dirs):
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
) )
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
project_dir = Path(__file__).parent.parent.parent project_dir = config_eos.package_root_path
subprocess.run( subprocess.run(
[sys.executable, "-m", "pip", "install", "-e", project_dir], [sys.executable, "-m", "pip", "install", "-e", project_dir],
check=True, check=True,

View File

@ -5,7 +5,7 @@ from unittest.mock import patch
import pytest import pytest
from akkudoktoreos.config.config import ConfigEOS from akkudoktoreos.config.config import ConfigEOS
from akkudoktoreos.utils.logutil import get_logger from akkudoktoreos.core.logging import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -77,13 +77,9 @@ def test_loadakkudoktor_provider_id(load_provider):
assert load_provider.provider_id() == "LoadAkkudoktor" assert load_provider.provider_id() == "LoadAkkudoktor"
@patch("akkudoktoreos.prediction.loadakkudoktor.Path")
@patch("akkudoktoreos.prediction.loadakkudoktor.np.load") @patch("akkudoktoreos.prediction.loadakkudoktor.np.load")
def test_load_data_from_mock(mock_np_load, mock_path, mock_load_profiles_file, load_provider): def test_load_data_from_mock(mock_np_load, mock_load_profiles_file, load_provider):
"""Test the `load_data` method.""" """Test the `load_data` method."""
# Mock path behavior to return the test file
mock_path.return_value.parent.parent.joinpath.return_value = mock_load_profiles_file
# Mock numpy load to return data similar to what would be in the file # Mock numpy load to return data similar to what would be in the file
mock_np_load.return_value = { mock_np_load.return_value = {
"yearly_profiles": np.ones((365, 24)), "yearly_profiles": np.ones((365, 24)),

View File

@ -1,4 +1,4 @@
"""Test Module for logutil Module.""" """Test Module for logging Module."""
import logging import logging
import os import os
@ -6,7 +6,7 @@ from logging.handlers import RotatingFileHandler
import pytest import pytest
from akkudoktoreos.utils.logutil import get_logger from akkudoktoreos.core.logging import get_logger
# ----------------------------- # -----------------------------
# get_logger # get_logger
@ -73,10 +73,5 @@ def test_get_logger_no_file_logging(clean_up_log_file):
def test_get_logger_with_invalid_level(clean_up_log_file): def test_get_logger_with_invalid_level(clean_up_log_file):
"""Test logger creation with an invalid logging level.""" """Test logger creation with an invalid logging level."""
logger = get_logger("test_logger", logging_level="INVALID") with pytest.raises(ValueError, match="Unknown loggin level: INVALID"):
logger = get_logger("test_logger", logging_level="INVALID")
# Check logger name
assert logger.name == "test_logger"
# Check default logging level is DEBUG
assert logger.level == logging.DEBUG

View File

@ -9,5 +9,5 @@ def test_server(server, config_eos):
assert config_eos.data_folder_path is not None assert config_eos.data_folder_path is not None
assert config_eos.data_folder_path.is_dir() assert config_eos.data_folder_path.is_dir()
result = requests.get(f"{server}/v1/config?") result = requests.get(f"{server}/v1/config")
assert result.status_code == HTTPStatus.OK assert result.status_code == HTTPStatus.OK

View File

@ -8,6 +8,159 @@
**Endpoints**: **Endpoints**:
## POST /gesamtlast
**Links**: [local](http://localhost:8503/docs#/default/fastapi_gesamtlast_gesamtlast_post), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_gesamtlast_gesamtlast_post)
Fastapi Gesamtlast
```
Deprecated: Total Load Prediction with adjustment.
Endpoint to handle total load prediction adjusted by latest measured data.
Total load prediction starts at 00.00.00 today and is provided for 48 hours.
If no prediction values are available the missing ones at the start of the series are
filled with the first available prediction value.
Note:
Use '/v1/prediction/list?key=load_mean_adjusted' instead.
Load energy meter readings to be added to EOS measurement by:
'/v1/measurement/load-mr/value/by-name' or
'/v1/measurement/value'
```
**Request Body**:
- `application/json`: {
"$ref": "#/components/schemas/GesamtlastRequest"
}
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## GET /gesamtlast_simple
**Links**: [local](http://localhost:8503/docs#/default/fastapi_gesamtlast_simple_gesamtlast_simple_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_gesamtlast_simple_gesamtlast_simple_get)
Fastapi Gesamtlast Simple
```
Deprecated: Total Load Prediction.
Endpoint to handle total load prediction.
Total load prediction starts at 00.00.00 today and is provided for 48 hours.
If no prediction values are available the missing ones at the start of the series are
filled with the first available prediction value.
Note:
Set LoadAkkudoktor as load_provider, then update data with
'/v1/prediction/update'
and then request data with
'/v1/prediction/list?key=load_mean' instead.
```
**Parameters**:
- `year_energy` (query, required): No description provided.
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## POST /optimize
**Links**: [local](http://localhost:8503/docs#/default/fastapi_optimize_optimize_post), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_optimize_optimize_post)
Fastapi Optimize
**Parameters**:
- `start_hour` (query, optional): Defaults to current hour of the day.
**Request Body**:
- `application/json`: {
"$ref": "#/components/schemas/OptimizationParameters"
}
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## GET /pvforecast
**Links**: [local](http://localhost:8503/docs#/default/fastapi_pvforecast_pvforecast_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_pvforecast_pvforecast_get)
Fastapi Pvforecast
```
Deprecated: PV Forecast Prediction.
Endpoint to handle PV forecast prediction.
PVForecast starts at 00.00.00 today and is provided for 48 hours.
If no forecast values are available the missing ones at the start of the series are
filled with the first available forecast value.
Note:
Set PVForecastAkkudoktor as pvforecast_provider, then update data with
'/v1/prediction/update'
and then request data with
'/v1/prediction/list?key=pvforecast_ac_power' and
'/v1/prediction/list?key=pvforecastakkudoktor_temp_air' instead.
```
**Responses**:
- **200**: Successful Response
---
## GET /strompreis
**Links**: [local](http://localhost:8503/docs#/default/fastapi_strompreis_strompreis_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_strompreis_strompreis_get)
Fastapi Strompreis
```
Deprecated: Electricity Market Price Prediction per Wh (€/Wh).
Electricity prices start at 00.00.00 today and are provided for 48 hours.
If no prices are available the missing ones at the start of the series are
filled with the first available price.
Note:
Electricity price charges are added.
Note:
Set ElecPriceAkkudoktor as elecprice_provider, then update data with
'/v1/prediction/update'
and then request data with
'/v1/prediction/list?key=elecprice_marketprice_wh' or
'/v1/prediction/list?key=elecprice_marketprice_kwh' instead.
```
**Responses**:
- **200**: Successful Response
---
## GET /v1/config ## GET /v1/config
**Links**: [local](http://localhost:8503/docs#/default/fastapi_config_get_v1_config_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_config_get_v1_config_get) **Links**: [local](http://localhost:8503/docs#/default/fastapi_config_get_v1_config_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_config_get_v1_config_get)
@ -16,6 +169,9 @@ Fastapi Config Get
``` ```
Get the current configuration. Get the current configuration.
Returns:
configuration (ConfigEOS): The current configuration.
``` ```
**Responses**: **Responses**:
@ -31,22 +187,459 @@ Get the current configuration.
Fastapi Config Put Fastapi Config Put
``` ```
Merge settings into current configuration. Write the provided settings into the current settings.
The existing settings are completely overwritten. Note that for any setting
value that is None, the configuration will fall back to values from other sources such as
environment variables, the EOS configuration file, or default values.
Args: Args:
settings (SettingsEOS): The settings to merge into the current configuration. settings (SettingsEOS): The settings to write into the current settings.
save (Optional[bool]): Save the resulting configuration to the configuration file.
Defaults to False. Returns:
configuration (ConfigEOS): The current configuration after the write.
``` ```
**Parameters**: **Parameters**:
- `save` (query, optional): No description provided. - `server_fastapi_host` (query, optional): FastAPI server IP address.
- `server_fastapi_port` (query, optional): FastAPI server IP port number.
- `server_fastapi_verbose` (query, optional): Enable debug output
- `server_fastapi_startup_server_fasthtml` (query, optional): FastAPI server to startup application FastHTML server.
- `server_fasthtml_host` (query, optional): FastHTML server IP address.
- `server_fasthtml_port` (query, optional): FastHTML server IP port number.
- `weatherimport_file_path` (query, optional): Path to the file to import weather data from.
- `weatherimport_json` (query, optional): JSON string, dictionary of weather forecast value lists.
- `weather_provider` (query, optional): Weather provider id of provider to be used.
- `pvforecastimport_file_path` (query, optional): Path to the file to import PV forecast data from.
- `pvforecastimport_json` (query, optional): JSON string, dictionary of PV forecast value lists.
- `pvforecast_provider` (query, optional): PVForecast provider id of provider to be used.
- `pvforecast0_surface_tilt` (query, optional): Tilt angle from horizontal plane. Ignored for two-axis tracking.
- `pvforecast0_surface_azimuth` (query, optional): Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).
- `pvforecast0_userhorizon` (query, optional): Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.
- `pvforecast0_peakpower` (query, optional): Nominal power of PV system in kW.
- `pvforecast0_pvtechchoice` (query, optional): PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'.
- `pvforecast0_mountingplace` (query, optional): Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.
- `pvforecast0_loss` (query, optional): Sum of PV system losses in percent
- `pvforecast0_trackingtype` (query, optional): 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` (query, optional): Calculate the optimum tilt angle. Ignored for two-axis tracking.
- `pvforecast0_optimalangles` (query, optional): Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking.
- `pvforecast0_albedo` (query, optional): Proportion of the light hitting the ground that it reflects back.
- `pvforecast0_module_model` (query, optional): Model of the PV modules of this plane.
- `pvforecast0_inverter_model` (query, optional): Model of the inverter of this plane.
- `pvforecast0_inverter_paco` (query, optional): AC power rating of the inverter. [W]
- `pvforecast0_modules_per_string` (query, optional): Number of the PV modules of the strings of this plane.
- `pvforecast0_strings_per_inverter` (query, optional): Number of the strings of the inverter of this plane.
- `pvforecast1_surface_tilt` (query, optional): Tilt angle from horizontal plane. Ignored for two-axis tracking.
- `pvforecast1_surface_azimuth` (query, optional): Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).
- `pvforecast1_userhorizon` (query, optional): Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.
- `pvforecast1_peakpower` (query, optional): Nominal power of PV system in kW.
- `pvforecast1_pvtechchoice` (query, optional): PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'.
- `pvforecast1_mountingplace` (query, optional): Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.
- `pvforecast1_loss` (query, optional): Sum of PV system losses in percent
- `pvforecast1_trackingtype` (query, optional): 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` (query, optional): Calculate the optimum tilt angle. Ignored for two-axis tracking.
- `pvforecast1_optimalangles` (query, optional): Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking.
- `pvforecast1_albedo` (query, optional): Proportion of the light hitting the ground that it reflects back.
- `pvforecast1_module_model` (query, optional): Model of the PV modules of this plane.
- `pvforecast1_inverter_model` (query, optional): Model of the inverter of this plane.
- `pvforecast1_inverter_paco` (query, optional): AC power rating of the inverter. [W]
- `pvforecast1_modules_per_string` (query, optional): Number of the PV modules of the strings of this plane.
- `pvforecast1_strings_per_inverter` (query, optional): Number of the strings of the inverter of this plane.
- `pvforecast2_surface_tilt` (query, optional): Tilt angle from horizontal plane. Ignored for two-axis tracking.
- `pvforecast2_surface_azimuth` (query, optional): Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).
- `pvforecast2_userhorizon` (query, optional): Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.
- `pvforecast2_peakpower` (query, optional): Nominal power of PV system in kW.
- `pvforecast2_pvtechchoice` (query, optional): PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'.
- `pvforecast2_mountingplace` (query, optional): Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.
- `pvforecast2_loss` (query, optional): Sum of PV system losses in percent
- `pvforecast2_trackingtype` (query, optional): 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` (query, optional): Calculate the optimum tilt angle. Ignored for two-axis tracking.
- `pvforecast2_optimalangles` (query, optional): Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking.
- `pvforecast2_albedo` (query, optional): Proportion of the light hitting the ground that it reflects back.
- `pvforecast2_module_model` (query, optional): Model of the PV modules of this plane.
- `pvforecast2_inverter_model` (query, optional): Model of the inverter of this plane.
- `pvforecast2_inverter_paco` (query, optional): AC power rating of the inverter. [W]
- `pvforecast2_modules_per_string` (query, optional): Number of the PV modules of the strings of this plane.
- `pvforecast2_strings_per_inverter` (query, optional): Number of the strings of the inverter of this plane.
- `pvforecast3_surface_tilt` (query, optional): Tilt angle from horizontal plane. Ignored for two-axis tracking.
- `pvforecast3_surface_azimuth` (query, optional): Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).
- `pvforecast3_userhorizon` (query, optional): Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.
- `pvforecast3_peakpower` (query, optional): Nominal power of PV system in kW.
- `pvforecast3_pvtechchoice` (query, optional): PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'.
- `pvforecast3_mountingplace` (query, optional): Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.
- `pvforecast3_loss` (query, optional): Sum of PV system losses in percent
- `pvforecast3_trackingtype` (query, optional): 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` (query, optional): Calculate the optimum tilt angle. Ignored for two-axis tracking.
- `pvforecast3_optimalangles` (query, optional): Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking.
- `pvforecast3_albedo` (query, optional): Proportion of the light hitting the ground that it reflects back.
- `pvforecast3_module_model` (query, optional): Model of the PV modules of this plane.
- `pvforecast3_inverter_model` (query, optional): Model of the inverter of this plane.
- `pvforecast3_inverter_paco` (query, optional): AC power rating of the inverter. [W]
- `pvforecast3_modules_per_string` (query, optional): Number of the PV modules of the strings of this plane.
- `pvforecast3_strings_per_inverter` (query, optional): Number of the strings of the inverter of this plane.
- `pvforecast4_surface_tilt` (query, optional): Tilt angle from horizontal plane. Ignored for two-axis tracking.
- `pvforecast4_surface_azimuth` (query, optional): Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).
- `pvforecast4_userhorizon` (query, optional): Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.
- `pvforecast4_peakpower` (query, optional): Nominal power of PV system in kW.
- `pvforecast4_pvtechchoice` (query, optional): PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'.
- `pvforecast4_mountingplace` (query, optional): Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.
- `pvforecast4_loss` (query, optional): Sum of PV system losses in percent
- `pvforecast4_trackingtype` (query, optional): 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` (query, optional): Calculate the optimum tilt angle. Ignored for two-axis tracking.
- `pvforecast4_optimalangles` (query, optional): Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking.
- `pvforecast4_albedo` (query, optional): Proportion of the light hitting the ground that it reflects back.
- `pvforecast4_module_model` (query, optional): Model of the PV modules of this plane.
- `pvforecast4_inverter_model` (query, optional): Model of the inverter of this plane.
- `pvforecast4_inverter_paco` (query, optional): AC power rating of the inverter. [W]
- `pvforecast4_modules_per_string` (query, optional): Number of the PV modules of the strings of this plane.
- `pvforecast4_strings_per_inverter` (query, optional): Number of the strings of the inverter of this plane.
- `pvforecast5_surface_tilt` (query, optional): Tilt angle from horizontal plane. Ignored for two-axis tracking.
- `pvforecast5_surface_azimuth` (query, optional): Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).
- `pvforecast5_userhorizon` (query, optional): Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.
- `pvforecast5_peakpower` (query, optional): Nominal power of PV system in kW.
- `pvforecast5_pvtechchoice` (query, optional): PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'.
- `pvforecast5_mountingplace` (query, optional): Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.
- `pvforecast5_loss` (query, optional): Sum of PV system losses in percent
- `pvforecast5_trackingtype` (query, optional): 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` (query, optional): Calculate the optimum tilt angle. Ignored for two-axis tracking.
- `pvforecast5_optimalangles` (query, optional): Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking.
- `pvforecast5_albedo` (query, optional): Proportion of the light hitting the ground that it reflects back.
- `pvforecast5_module_model` (query, optional): Model of the PV modules of this plane.
- `pvforecast5_inverter_model` (query, optional): Model of the inverter of this plane.
- `pvforecast5_inverter_paco` (query, optional): AC power rating of the inverter. [W]
- `pvforecast5_modules_per_string` (query, optional): Number of the PV modules of the strings of this plane.
- `pvforecast5_strings_per_inverter` (query, optional): Number of the strings of the inverter of this plane.
- `load_import_file_path` (query, optional): Path to the file to import load data from.
- `load_import_json` (query, optional): JSON string, dictionary of load forecast value lists.
- `loadakkudoktor_year_energy` (query, optional): Yearly energy consumption (kWh).
- `load_provider` (query, optional): Load provider id of provider to be used.
- `elecpriceimport_file_path` (query, optional): Path to the file to import elecprice data from.
- `elecpriceimport_json` (query, optional): JSON string, dictionary of electricity price forecast value lists.
- `elecprice_provider` (query, optional): Electricity price provider id of provider to be used.
- `elecprice_charges_kwh` (query, optional): Electricity price charges (€/kWh).
- `prediction_hours` (query, optional): Number of hours into the future for predictions
- `prediction_historic_hours` (query, optional): Number of hours into the past for historical predictions data
- `latitude` (query, optional): Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°)
- `longitude` (query, optional): Longitude in decimal degrees, within -180 to 180 (°)
- `optimization_hours` (query, optional): Number of hours into the future for optimizations.
- `optimization_penalty` (query, optional): Penalty factor used in optimization.
- `optimization_ev_available_charge_rates_percent` (query, optional): Charge rates available for the EV in percent of maximum charge.
- `measurement_load0_name` (query, optional): Name of the load0 source (e.g. 'Household', 'Heat Pump')
- `measurement_load1_name` (query, optional): Name of the load1 source (e.g. 'Household', 'Heat Pump')
- `measurement_load2_name` (query, optional): Name of the load2 source (e.g. 'Household', 'Heat Pump')
- `measurement_load3_name` (query, optional): Name of the load3 source (e.g. 'Household', 'Heat Pump')
- `measurement_load4_name` (query, optional): Name of the load4 source (e.g. 'Household', 'Heat Pump')
- `battery_provider` (query, optional): Id of Battery simulation provider.
- `battery_capacity` (query, optional): Battery capacity [Wh].
- `battery_initial_soc` (query, optional): Battery initial state of charge [%].
- `battery_soc_min` (query, optional): Battery minimum state of charge [%].
- `battery_soc_max` (query, optional): Battery maximum state of charge [%].
- `battery_charging_efficiency` (query, optional): Battery charging efficiency [%].
- `battery_discharging_efficiency` (query, optional): Battery discharging efficiency [%].
- `battery_max_charging_power` (query, optional): Battery maximum charge power [W].
- `bev_provider` (query, optional): Id of Battery Electric Vehicle simulation provider.
- `bev_capacity` (query, optional): Battery Electric Vehicle capacity [Wh].
- `bev_initial_soc` (query, optional): Battery Electric Vehicle initial state of charge [%].
- `bev_soc_max` (query, optional): Battery Electric Vehicle maximum state of charge [%].
- `bev_charging_efficiency` (query, optional): Battery Electric Vehicle charging efficiency [%].
- `bev_discharging_efficiency` (query, optional): Battery Electric Vehicle discharging efficiency [%].
- `bev_max_charging_power` (query, optional): Battery Electric Vehicle maximum charge power [W].
- `dishwasher_provider` (query, optional): Id of Dish Washer simulation provider.
- `dishwasher_consumption` (query, optional): Dish Washer energy consumption [Wh].
- `dishwasher_duration` (query, optional): Dish Washer usage duration [h].
- `inverter_provider` (query, optional): Id of PV Inverter simulation provider.
- `inverter_power_max` (query, optional): Inverter maximum power [W].
- `logging_level_default` (query, optional): EOS default logging level.
- `data_folder_path` (query, optional): Path to EOS data directory.
- `data_output_subpath` (query, optional): Sub-path for the EOS output data directory.
- `data_cache_subpath` (query, optional): Sub-path for the EOS cache data directory.
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## GET /v1/config/file
**Links**: [local](http://localhost:8503/docs#/default/fastapi_config_file_get_v1_config_file_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_config_file_get_v1_config_file_get)
Fastapi Config File Get
```
Get the settings as defined by the EOS configuration file.
Returns:
settings (SettingsEOS): The settings defined by the EOS configuration file.
```
**Responses**:
- **200**: Successful Response
---
## PUT /v1/config/file
**Links**: [local](http://localhost:8503/docs#/default/fastapi_config_file_put_v1_config_file_put), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_config_file_put_v1_config_file_put)
Fastapi Config File Put
```
Save the current configuration to the EOS configuration file.
Returns:
configuration (ConfigEOS): The current configuration that was saved.
```
**Responses**:
- **200**: Successful Response
---
## POST /v1/config/update
**Links**: [local](http://localhost:8503/docs#/default/fastapi_config_update_post_v1_config_update_post), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_config_update_post_v1_config_update_post)
Fastapi Config Update Post
```
Update the configuration from the EOS configuration file.
Returns:
configuration (ConfigEOS): The current configuration after update.
```
**Responses**:
- **200**: Successful Response
---
## PUT /v1/config/value
**Links**: [local](http://localhost:8503/docs#/default/fastapi_config_value_put_v1_config_value_put), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_config_value_put_v1_config_value_put)
Fastapi Config Value Put
```
Set the configuration option in the settings.
Args:
key (str): configuration key
value (Any): configuration value
Returns:
configuration (ConfigEOS): The current configuration after the write.
```
**Parameters**:
- `key` (query, required): configuration key
- `value` (query, required): configuration value
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## PUT /v1/measurement/data
**Links**: [local](http://localhost:8503/docs#/default/fastapi_measurement_data_put_v1_measurement_data_put), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_measurement_data_put_v1_measurement_data_put)
Fastapi Measurement Data Put
```
Merge the measurement data given as datetime data into EOS measurements.
```
**Request Body**: **Request Body**:
- `application/json`: { - `application/json`: {
"$ref": "#/components/schemas/SettingsEOS" "$ref": "#/components/schemas/PydanticDateTimeData"
}
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## PUT /v1/measurement/dataframe
**Links**: [local](http://localhost:8503/docs#/default/fastapi_measurement_dataframe_put_v1_measurement_dataframe_put), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_measurement_dataframe_put_v1_measurement_dataframe_put)
Fastapi Measurement Dataframe Put
```
Merge the measurement data given as dataframe into EOS measurements.
```
**Request Body**:
- `application/json`: {
"$ref": "#/components/schemas/PydanticDateTimeDataFrame"
} }
**Responses**: **Responses**:
@ -225,54 +818,6 @@ Merge the measurement of given key and value into EOS measurements at given date
--- ---
## PUT /v1/measurement/dataframe
**Links**: [local](http://localhost:8503/docs#/default/fastapi_measurement_dataframe_put_v1_measurement_dataframe_put), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_measurement_dataframe_put_v1_measurement_dataframe_put)
Fastapi Measurement Dataframe Put
```
Merge the measurement data given as dataframe into EOS measurements.
```
**Request Body**:
- `application/json`: {
"$ref": "#/components/schemas/PydanticDateTimeDataFrame"
}
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## PUT /v1/measurement/data
**Links**: [local](http://localhost:8503/docs#/default/fastapi_measurement_data_put_v1_measurement_data_put), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_measurement_data_put_v1_measurement_data_put)
Fastapi Measurement Data Put
```
Merge the measurement data given as datetime data into EOS measurements.
```
**Request Body**:
- `application/json`: {
"$ref": "#/components/schemas/PydanticDateTimeData"
}
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## GET /v1/prediction/keys ## GET /v1/prediction/keys
**Links**: [local](http://localhost:8503/docs#/default/fastapi_prediction_keys_get_v1_prediction_keys_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_prediction_keys_get_v1_prediction_keys_get) **Links**: [local](http://localhost:8503/docs#/default/fastapi_prediction_keys_get_v1_prediction_keys_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_prediction_keys_get_v1_prediction_keys_get)
@ -289,39 +834,6 @@ Get a list of available prediction keys.
--- ---
## GET /v1/prediction/series
**Links**: [local](http://localhost:8503/docs#/default/fastapi_prediction_series_get_v1_prediction_series_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_prediction_series_get_v1_prediction_series_get)
Fastapi Prediction Series Get
```
Get prediction for given key within given date range as series.
Args:
key (str): Prediction key
start_datetime (Optional[str]): Starting datetime (inclusive).
Defaults to start datetime of latest prediction.
end_datetime (Optional[str]: Ending datetime (exclusive).
Defaults to end datetime of latest prediction.
```
**Parameters**:
- `key` (query, required): Prediction key.
- `start_datetime` (query, optional): Starting datetime (inclusive).
- `end_datetime` (query, optional): Ending datetime (exclusive).
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## GET /v1/prediction/list ## GET /v1/prediction/list
**Links**: [local](http://localhost:8503/docs#/default/fastapi_prediction_list_get_v1_prediction_list_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_prediction_list_get_v1_prediction_list_get) **Links**: [local](http://localhost:8503/docs#/default/fastapi_prediction_list_get_v1_prediction_list_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_prediction_list_get_v1_prediction_list_get)
@ -359,6 +871,39 @@ Args:
--- ---
## GET /v1/prediction/series
**Links**: [local](http://localhost:8503/docs#/default/fastapi_prediction_series_get_v1_prediction_series_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_prediction_series_get_v1_prediction_series_get)
Fastapi Prediction Series Get
```
Get prediction for given key within given date range as series.
Args:
key (str): Prediction key
start_datetime (Optional[str]): Starting datetime (inclusive).
Defaults to start datetime of latest prediction.
end_datetime (Optional[str]: Ending datetime (exclusive).
Defaults to end datetime of latest prediction.
```
**Parameters**:
- `key` (query, required): Prediction key.
- `start_datetime` (query, optional): Starting datetime (inclusive).
- `end_datetime` (query, optional): Ending datetime (exclusive).
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## POST /v1/prediction/update ## POST /v1/prediction/update
**Links**: [local](http://localhost:8503/docs#/default/fastapi_prediction_update_v1_prediction_update_post), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_prediction_update_v1_prediction_update_post) **Links**: [local](http://localhost:8503/docs#/default/fastapi_prediction_update_v1_prediction_update_post), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_prediction_update_v1_prediction_update_post)
@ -422,140 +967,6 @@ Args:
--- ---
## GET /strompreis
**Links**: [local](http://localhost:8503/docs#/default/fastapi_strompreis_strompreis_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_strompreis_strompreis_get)
Fastapi Strompreis
```
Deprecated: Electricity Market Price Prediction per Wh (€/Wh).
Note:
Set ElecPriceAkkudoktor as elecprice_provider, then update data with
'/v1/prediction/update'
and then request data with
'/v1/prediction/list?key=elecprice_marketprice_wh' or
'/v1/prediction/list?key=elecprice_marketprice_kwh' instead.
```
**Responses**:
- **200**: Successful Response
---
## POST /gesamtlast
**Links**: [local](http://localhost:8503/docs#/default/fastapi_gesamtlast_gesamtlast_post), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_gesamtlast_gesamtlast_post)
Fastapi Gesamtlast
```
Deprecated: Total Load Prediction with adjustment.
Endpoint to handle total load prediction adjusted by latest measured data.
Note:
Use '/v1/prediction/list?key=load_mean_adjusted' instead.
Load energy meter readings to be added to EOS measurement by:
'/v1/measurement/load-mr/value/by-name' or
'/v1/measurement/value'
```
**Request Body**:
- `application/json`: {
"$ref": "#/components/schemas/GesamtlastRequest"
}
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## GET /gesamtlast_simple
**Links**: [local](http://localhost:8503/docs#/default/fastapi_gesamtlast_simple_gesamtlast_simple_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_gesamtlast_simple_gesamtlast_simple_get)
Fastapi Gesamtlast Simple
```
Deprecated: Total Load Prediction.
Endpoint to handle total load prediction.
Note:
Set LoadAkkudoktor as load_provider, then update data with
'/v1/prediction/update'
and then request data with
'/v1/prediction/list?key=load_mean' instead.
```
**Parameters**:
- `year_energy` (query, required): No description provided.
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## GET /pvforecast
**Links**: [local](http://localhost:8503/docs#/default/fastapi_pvforecast_pvforecast_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_pvforecast_pvforecast_get)
Fastapi Pvforecast
```
Deprecated: PV Forecast Prediction.
Endpoint to handle PV forecast prediction.
Note:
Set PVForecastAkkudoktor as pvforecast_provider, then update data with
'/v1/prediction/update'
and then request data with
'/v1/prediction/list?key=pvforecast_ac_power' and
'/v1/prediction/list?key=pvforecastakkudoktor_temp_air' instead.
```
**Responses**:
- **200**: Successful Response
---
## POST /optimize
**Links**: [local](http://localhost:8503/docs#/default/fastapi_optimize_optimize_post), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_optimize_optimize_post)
Fastapi Optimize
**Parameters**:
- `start_hour` (query, optional): Defaults to current hour of the day.
**Request Body**:
- `application/json`: {
"$ref": "#/components/schemas/OptimizationParameters"
}
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## GET /visualization_results.pdf ## GET /visualization_results.pdf
**Links**: [local](http://localhost:8503/docs#/default/get_pdf_visualization_results_pdf_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/get_pdf_visualization_results_pdf_get) **Links**: [local](http://localhost:8503/docs#/default/get_pdf_visualization_results_pdf_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/get_pdf_visualization_results_pdf_get)