Add documentation. (#321)

Add documentation that covers:

- Prediction
- Measuremnt
- REST API

Add Python scripts that support automatic documentation generation using the Sphinx
sphinxcontrib.eval extension.

Add automatic update/ test for REST API documentation.

Filter proxy endpoints from REST API documentation.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
Bobby Noelte 2025-01-03 00:31:20 +01:00 committed by GitHub
parent 4cb6dc7270
commit 1866055478
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 7565 additions and 6131 deletions

5
.gitignore vendored
View File

@ -1,8 +1,6 @@
cache/ cache/
output/ output/
EOS.config.json EOS.config.json
docs/develop/CONTRIBUTING.md
docs/develop/getting_started.md
# Default ignore folders and files for VS Code, Python # Default ignore folders and files for VS Code, Python
@ -258,5 +256,6 @@ visualize_output_*.pdf
*_pdf.png *_pdf.png
# Test files # Test files
openapi-new.json
tests/testdata/new_optimize_result* tests/testdata/new_optimize_result*
tests/testdata/openapi-new.json
tests/testdata/openapi-new.md

View File

@ -17,6 +17,7 @@ help:
@echo " docker-build - Rebuild docker image" @echo " docker-build - Rebuild docker image"
@echo " docs - Generate HTML documentation (in build/docs/html/)." @echo " docs - Generate HTML documentation (in build/docs/html/)."
@echo " read-docs - Read HTML documentation in your browser." @echo " read-docs - Read HTML documentation in your browser."
@echo " clean-docs - Remove generated documentation.""
@echo " run - Run FastAPI production server in the virtual environment." @echo " run - Run FastAPI production server in the virtual environment."
@echo " run-dev - Run FastAPI development server in the virtual environment (automatically reloads)." @echo " run-dev - Run FastAPI development server in the virtual environment (automatically reloads)."
@echo " dist - Create distribution (in dist/)." @echo " dist - Create distribution (in dist/)."
@ -52,13 +53,6 @@ dist: pip
# Target to generate HTML documentation # Target to generate HTML documentation
docs: pip-dev docs: pip-dev
mkdir -p docs/develop
cp README.md docs/develop/getting_started.md
# remove top level header and coresponding description
sed -i '/^##[^#]/,$$!d' docs/develop/getting_started.md
sed -i "1i\# Getting Started\n" docs/develop/getting_started.md
cp CONTRIBUTING.md docs/develop
sed -i "s/README.md/getting_started.md/g" docs/develop/CONTRIBUTING.md
.venv/bin/sphinx-build -M html docs build/docs .venv/bin/sphinx-build -M html docs build/docs
@echo "Documentation generated to build/docs/html/." @echo "Documentation generated to build/docs/html/."
@ -67,12 +61,17 @@ read-docs: docs
@echo "Read the documentation in your browser" @echo "Read the documentation in your browser"
.venv/bin/python -m webbrowser build/docs/html/index.html .venv/bin/python -m webbrowser build/docs/html/index.html
# Clean target to remove generated documentation, distribution and virtual environment # Clean target to remove generated documentation and documentation artefacts
clean: clean-docs:
@echo "Cleaning virtual env, distribution and build directories"
rm -rf dist build .venv
@echo "Searching and deleting all '_autosum' directories in docs..." @echo "Searching and deleting all '_autosum' directories in docs..."
@find docs -type d -name '_autosummary' -exec rm -rf {} +; @find docs -type d -name '_autosummary' -exec rm -rf {} +;
@echo "Cleaning docs build directories"
rm -rf build/docs
# Clean target to remove generated documentation, distribution and virtual environment
clean: clean-docs
@echo "Cleaning virtual env, distribution and build directories"
rm -rf build .venv
@echo "Deletion complete." @echo "Deletion complete."
run: run:

View File

@ -93,7 +93,7 @@ Each class is designed to be easily customized and extended to integrate additio
## Server API ## Server API
See the Swagger API documentation for detailed information: [EOS OpenAPI Spec](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/docs/akkudoktoreos/openapi.json) See the Swagger API documentation for detailed information: [EOS OpenAPI Spec](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json)
## Further resources ## Further resources

View File

@ -0,0 +1,93 @@
% SPDX-License-Identifier: Apache-2.0
# Measurements
Measurements are utilized to refine predictions using real data from your system, thereby enhancing
accuracy.
- **Household Load Measurement**
- **Grid Export Measurement**
- **Grid Import Measurement**
## Storing Measurements
EOS stores measurements in a **key-value store**, where the term `measurement key` refers to the
unique identifier used to store and retrieve specific measurement data. Note that the key-value
store is memory-based, meaning that all stored data will be lost upon restarting the EOS REST
server.
:::{admonition} Todo
:class: note
Ensure that measurement data persists across server restarts.
:::
Several endpoints of the EOS REST server allow for the management and retrieval of these
measurements.
The measurement data must be or is provided in one of the following formats:
### 1. DateTimeData
A dictionary with the following structure:
```JSON
{
"start_datetime": "2024-01-01 00:00:00",
"interval": "1 Hour",
"<measurement key>": [value, value, ...],
"<measurement key>": [value, value, ...],
...
}
```
### 2. DateTimeDataFrame
A JSON string created from a [pandas](https://pandas.pydata.org/docs/index.html) dataframe with a
`DatetimeIndex`. Use [pandas.DataFrame.to_json(orient="index")](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_json.html#pandas.DataFrame.to_json).
The column name of the data must be the same as the names of the `measurement key`s.
### 3. DateTimeSeries
A JSON string created from a [pandas](https://pandas.pydata.org/docs/index.html) series with a
`DatetimeIndex`. Use [pandas.Series.to_json(orient="index")](https://pandas.pydata.org/docs/reference/api/pandas.Series.to_json.html#pandas.Series.to_json).
## Load Measurement
The EOS measurement store provides for storing meter readings of loads. There are currently five loads
foreseen. The associated `measurement key`s are:
- `measurement_load0_mr`: Load0 meter reading [kWh]
- `measurement_load1_mr`: Load1 meter reading [kWh]
- `measurement_load2_mr`: Load2 meter reading [kWh]
- `measurement_load3_mr`: Load3 meter reading [kWh]
- `measurement_load4_mr`: Load4 meter reading [kWh]
For ease of use, you can assign descriptive names to the `measurement key`s to represent your
system's load sources. Use the following `configuration options` to set these names
(e.g., 'Dish Washer', 'Heat Pump'):
- `measurement_load0_name`: Name of the load0 source
- `measurement_load1_name`: Name of the load1 source
- `measurement_load2_name`: Name of the load2 source
- `measurement_load3_name`: Name of the load3 source
- `measurement_load4_name`: Name of the load4 source
Load measurements can be stored for any datetime. The values between different meter readings are
linearly approximated. Since optimization occurs on the hour, storing values between hours is
generally not useful.
The EOS measurement store automatically sums all given loads to create a total load value series
for specified intervals, usually one hour. This aggregated data can be used for load predictions.
## Grid Export/ Import Measurement
The EOS measurement store also allows for the storage of meter readings for grid import and export.
The associated `measurement key`s are:
- `measurement_grid_export_mr`: Export to grid meter reading [kWh]
- `measurement_grid_import_mr`: Import from grid meter reading [kWh]
:::{admonition} Todo
:class: note
Currently not used. Integrate grid meter readings into the respective predictions.
:::

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,401 @@
% SPDX-License-Identifier: Apache-2.0
# Predictions
Predictions, along with simulations and measurements, form the foundation upon which energy
optimization is executed. In EOS, a standard set of predictions is managed, including:
- **Household Load Prediction**
- **Electricity Price Prediction**
- **PV Power Prediction**
- **Weather Prediction**
## Storing Predictions
EOS stores predictions in a **key-value store**, where the term `prediction key` refers to the
unique key used to retrieve specific prediction data. The key-value store is in memory. Stored
data is lost on re-start of the EOS REST server.
## Prediction Providers
Most predictions can be sourced from various providers. The specific provider to use is configured
in the EOS configuration. For example:
```plaintext
weather_provider = "ClearOutside"
```
Some providers offer multiple prediction keys. For instance, a weather provider might provide data
to prediction keys like:
- `weather_temp_air` (air temperature)
- `weather_wind_speed` (wind speed)
### Prediction Import Providers
The prediction import providers are designed to import prediction data from a file or a JSON
string. An external entity should update the file or JSON string whenever new prediction data
becomes available.
The prediction data must be provided in one of the following formats:
#### 1. DateTimeData
A dictionary with the following structure:
```JSON
{
"start_datetime": "2024-01-01 00:00:00",
"interval": "1 Hour",
"<prediction key>": [value, value, ...],
"<prediction key>": [value, value, ...],
...
}
```
#### 2. DateTimeDataFrame
A JSON string created from a [pandas](https://pandas.pydata.org/docs/index.html) dataframe with a
`DatetimeIndex`. Use [pandas.DataFrame.to_json(orient="index")](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_json.html#pandas.DataFrame.to_json).
The column name of the data must be the same as the names of the `prediction key`s.
#### 3. DateTimeSeries
A JSON string created from a [pandas](https://pandas.pydata.org/docs/index.html) series with a
`DatetimeIndex`. Use [pandas.Series.to_json(orient="index")](https://pandas.pydata.org/docs/reference/api/pandas.Series.to_json.html#pandas.Series.to_json).
## Adjusted Predictions
Certain prediction keys include an `_adjusted` suffix, such as `load_total_adjusted`. These
predictions are adjusted by real data from your system's measurements if given to enhance accuracy.
For example, the load prediction provider `LoadAkkudoktor` takes generic load data assembled by
Akkudoktor.net, maps that to the yearly energy consumption given in the configuration option
`loadakkudoktor_year_energy`, and finally adjusts the predicted load by the `measurement_loads`
of your system.
## Prediction Updates
Predictions are updated at the start of each energy management run, i.e., when EOS performs
optimization. Key considerations for updates include:
- Predictions sourced from online providers are usually rate-limited to one retrieval per hour.
- Only predictions with a configured provider are updated.
- Some providers may not support all generic prediction keys, leading to potential gaps
in updated predictions even after update.
## Accessing Predictions
Prediction data can be accessed using the EOS **REST API** via the `/v1/prediction/<...>` endpoints.
In a standard configuration, the [**REST API**](http://0.0.0.0:8503/docs) of a running EOS instance
is available at [http://0.0.0.0:8503/docs](http://0.0.0.0:8503/docs). This link provides access to
the API documentation and allows you to explore available endpoints interactively.
To view all available prediction keys, use the **GET** `/v1/prediction/keys` endpoint.
If no keys are displayed, or if the ones you need are missing, it indicates that your configuration
lacks the necessary prediction provider settings. You can configure prediction providers by using
the **PUT** `/v1/config` endpoint. You may save your configuration to the EOS configuration file.
## Electricity Price Prediction
Prediction keys:
- `elecprice_marketprice_wh`: Electricity market price per Wh (€/Wh).
- `elecprice_marketprice_kwh`: Electricity market price per kWh (€/kWh).
Configuration options:
- `elecprice_provider`: Electricity price provider id of provider to be used.
- `ElecPriceAkkudoktor`: Retrieves from Akkudoktor.net.
- `ElecPriceImport`: Imports from a file or JSON string.
- `elecprice_charges_kwh`: Electricity price charges (€/kWh).
- `elecpriceimport_file_path`: Path to the file to import electricity price forecast data from.
- `elecpriceimport_json`: JSON string, dictionary of electricity price forecast value lists.
### ElecPriceAkkudoktor Provider
The `ElecPriceAkkudoktor` provider retrieves electricity prices directly from **Akkudoktor.net**,
which supplies price data for the next 24 hours. For periods beyond 24 hours, the provider generates
prices by extrapolating historical price data combined with the most recent actual prices obtained
from Akkudoktor.net. Electricity price charges given in the `elecprice_charges_kwh` configuration
option are added.
### ElecPriceImport Provider
The `ElecPriceImport` provider is designed to import electricity prices from a file or a JSON
string. An external entity should update the file or JSON string whenever new prediction data
becomes available.
The prediction key for the electricity price forecast data is:
- `elecprice_marketprice_wh`: Electricity market price per Wh (€/Wh).
The electricity proce forecast data must be provided in one of the formats described in
<project:#prediction-import-providers>. The data source must be given in the
`elecpriceimport_file_path` or `elecpriceimport_json` configuration option.
## Load Prediction
Prediction keys:
- `load_mean`: Predicted load mean value (W).
- `load_std`: Predicted load standard deviation (W).
- `load_mean_adjusted`: Predicted load mean value adjusted by load measurement (W).
Configuration options:
- `load_provider`: Load provider id of provider to be used.
- `LoadAkkudoktor`: Retrieves from local database.
- `LoadImport`: Imports from a file or JSON string.
- `loadakkudoktor_year_energy`: Yearly energy consumption (kWh).
- `loadimport_file_path`: Path to the file to import load forecast data from.
- `loadimport_json`: JSON string, dictionary of load forecast value lists.
### LoadAkkudoktor Provider
The `LoadAkkudoktor` provider retrieves generic load data from a local database and tailors it to
align with the annual energy consumption specified in the `loadakkudoktor_year_energy` configuration
option.
### LoadImport Provider
The `LoadImport` provider is designed to import load forecast data from a file or a JSON
string. An external entity should update the file or JSON string whenever new prediction data
becomes available.
The prediction keys for the load forecast data are:
- `load_mean`: Predicted load mean value (W).
- `load_std`: Predicted load standard deviation (W).
- `load_mean_adjusted`: Predicted load mean value adjusted by load measurement (W).
The load forecast data must be provided in one of the formats described in
<project:#prediction-import-providers>. The data source must be given in the `loadimport_file_path`
or `loadimport_json` configuration option.
## PV Power Prediction
Prediction keys:
- `pvforecast_ac_power`: Total DC power (W).
- `pvforecast_dc_power`: Total AC power (W).
Configuration options:
- `pvforecast_provider`: PVForecast provider id of provider to be used.
- `PVForecastAkkudoktor`: Retrieves from Akkudoktor.net.
- `PVForecastImport`: Imports from a file or JSON string.
- `latitude`: Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°)"
- `longitude`: Longitude in decimal degrees, within -180 to 180 (°)
- `pvforecast<0..5>_surface_tilt`: Tilt angle from horizontal plane. Ignored for two-axis tracking.
- `pvforecast<0..5>_surface_azimuth`: Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).
- `pvforecast<0..5>_userhorizon`: Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.
- `pvforecast<0..5>_peakpower`: Nominal power of PV system in kW.
- `pvforecast<0..5>_pvtechchoice`: PV technology. One of 'crystSi', 'CIS', 'CdTe', 'Unknown'.
- `pvforecast<0..5>_mountingplace`: Type of mounting for PV system. Options are 'free' for free-standing and 'building' for building-integrated.
- `pvforecast<0..5>_loss`: Sum of PV system losses in percent
- `pvforecast<0..5>_trackingtype`: 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.
- `pvforecast<0..5>_optimal_surface_tilt`: Calculate the optimum tilt angle. Ignored for two-axis tracking.
- `pvforecast<0..5>_optimalangles`: Calculate the optimum tilt and azimuth angles. Ignored for two-axis tracking.
- `pvforecast<0..5>_albedo`: Proportion of the light hitting the ground that it reflects back.
- `pvforecast<0..5>_module_model`: Model of the PV modules of this plane.
- `pvforecast<0..5>_inverter_model`: Model of the inverter of this plane.
- `pvforecast<0..5>_inverter_paco`: AC power rating of the inverter. [W]
- `pvforecast<0..5>_modules_per_string`: Number of the PV modules of the strings of this plane.
- `pvforecast<0..5>_strings_per_inverter`: Number of the strings of the inverter of this plane.
- `pvforecastimport_file_path`: Path to the file to import PV forecast data from.
- `pvforecastimport_json`: JSON string, dictionary of PV forecast value lists.
### PVForecastAkkudoktor Provider
The `PVForecastAkkudoktor` provider retrieves the PV power forecast data directly from
**Akkudoktor.net**.
The following general configuration options of the PV system must be set:
- `latitude`: Latitude in decimal degrees, between -90 and 90, north is positive (ISO 19115) (°)"
- `longitude`: Longitude in decimal degrees, within -180 to 180 (°)
For each plane `<0..5>` of the PV system the following configuration options must be set:
- `pvforecast<0..5>_surface_tilt`: Tilt angle from horizontal plane. Ignored for two-axis tracking.
- `pvforecast<0..5>_surface_azimuth`: Orientation (azimuth angle) of the (fixed) plane. Clockwise from north (north=0, east=90, south=180, west=270).
- `pvforecast<0..5>_userhorizon`: Elevation of horizon in degrees, at equally spaced azimuth clockwise from north.
- `pvforecast<0..5>_inverter_paco`: AC power rating of the inverter. [W]
- `pvforecast<0..5>_peakpower`: Nominal power of PV system in kW.
Example:
```Python
{
"latitude": 50.1234,
"longitude": 9.7654,
"pvforecast_provider": "PVForecastAkkudoktor",
"pvforecast0_peakpower": 5.0,
"pvforecast0_surface_azimuth": -10,
"pvforecast0_surface_tilt": 7,
"pvforecast0_userhorizon": [20, 27, 22, 20],
"pvforecast0_inverter_paco": 10000,
"pvforecast1_peakpower": 4.8,
"pvforecast1_surface_azimuth": -90,
"pvforecast1_surface_tilt": 7,
"pvforecast1_userhorizon": [30, 30, 30, 50],
"pvforecast1_inverter_paco": 10000,
"pvforecast2_peakpower": 1.4,
"pvforecast2_surface_azimuth": -40,
"pvforecast2_surface_tilt": 60,
"pvforecast2_userhorizon": [60, 30, 0, 30],
"pvforecast2_inverter_paco": 2000,
"pvforecast3_peakpower": 1.6,
"pvforecast3_surface_azimuth": 5,
"pvforecast3_surface_tilt": 45,
"pvforecast3_userhorizon": [45, 25, 30, 60],
"pvforecast3_inverter_paco": 1400,
"pvforecast4_peakpower": None,
}
```
### PVForecastImport Provider
The `PVForecastImport` provider is designed to import PV forecast data from a file or a JSON
string. An external entity should update the file or JSON string whenever new prediction data
becomes available.
The prediction keys for the PV forecast data are:
- `pvforecast_ac_power`: Total DC power (W).
- `pvforecast_dc_power`: Total AC power (W).
The PV forecast data must be provided in one of the formats described in
<project:#prediction-import-providers>. The data source must be given in the
`pvforecastimport_file_path` or `pvforecastimport_json` configuration option.
## Weather Prediction
Prediction keys:
- `weather_dew_point`: Dew Point (°C)
- `weather_dhi`: Diffuse Horizontal Irradiance (W/m2)
- `weather_dni`: Direct Normal Irradiance (W/m2)
- `weather_feels_like`: Feels Like (°C)
- `weather_fog`: Fog (%)
- `weather_frost_chance`: Chance of Frost
- `weather_ghi`: Global Horizontal Irradiance (W/m2)
- `weather_high_clouds`: High Clouds (% Sky Obscured)
- `weather_low_clouds`: Low Clouds (% Sky Obscured)
- `weather_medium_clouds`: Medium Clouds (% Sky Obscured)
- `weather_ozone`: Ozone (du)
- `weather_precip_amt`: Precipitation Amount (mm)
- `weather_precip_prob`: Precipitation Probability (%)
- `weather_preciptable_water`: Precipitable Water (cm)
- `weather_precip_type`: Precipitation Type
- `weather_pressure`: Pressure (mb)
- `weather_relative_humidity`: Relative Humidity (%)
- `weather_temp_air`: Temperature (°C)
- `weather_total_clouds`: Total Clouds (% Sky Obscured)
- `weather_visibility`: Visibility (m)
- `weather_wind_direction`: "Wind Direction (°)
- `weather_wind_speed`: Wind Speed (kmph)
Configuration options:
- `weather_provider`: Load provider id of provider to be used.
- `BrightSky`: Retrieves from https://api.brightsky.dev.
- `ClearOutside`: Retrieves from https://clearoutside.com/forecast.
- `LoadImport`: Imports from a file or JSON string.
- `weatherimport_file_path`: Path to the file to import weatherforecast data from.
- `weatherimport_json`: JSON string, dictionary of weather forecast value lists.
### BrightSky Provider
The `BrightSky` provider retrieves the PV power forecast data directly from
[**BrightSky**](https://api.brightsky.dev).
The provider provides forecast data for the following prediction keys:
- `weather_dew_point`: Dew Point (°C)
- `weather_ghi`: Global Horizontal Irradiance (W/m2)
- `weather_precip_amt`: Precipitation Amount (mm)
- `weather_precip_prob`: Precipitation Probability (%)
- `weather_pressure`: Pressure (mb)
- `weather_relative_humidity`: Relative Humidity (%)
- `weather_temp_air`: Temperature (°C)
- `weather_total_clouds`: Total Clouds (% Sky Obscured)
- `weather_visibility`: Visibility (m)
- `weather_wind_direction`: "Wind Direction (°)
- `weather_wind_speed`: Wind Speed (kmph)
### ClearOutside Provider
The `ClearOutside` provider retrieves the PV power forecast data directly from
[**ClearOutside**](https://clearoutside.com/forecast).
The provider provides forecast data for the following prediction keys:
- `weather_dew_point`: Dew Point (°C)
- `weather_dhi`: Diffuse Horizontal Irradiance (W/m2)
- `weather_dni`: Direct Normal Irradiance (W/m2)
- `weather_feels_like`: Feels Like (°C)
- `weather_fog`: Fog (%)
- `weather_frost_chance`: Chance of Frost
- `weather_ghi`: Global Horizontal Irradiance (W/m2)
- `weather_high_clouds`: High Clouds (% Sky Obscured)
- `weather_low_clouds`: Low Clouds (% Sky Obscured)
- `weather_medium_clouds`: Medium Clouds (% Sky Obscured)
- `weather_ozone`: Ozone (du)
- `weather_precip_amt`: Precipitation Amount (mm)
- `weather_precip_prob`: Precipitation Probability (%)
- `weather_preciptable_water`: Precipitable Water (cm)
- `weather_precip_type`: Precipitation Type
- `weather_pressure`: Pressure (mb)
- `weather_relative_humidity`: Relative Humidity (%)
- `weather_temp_air`: Temperature (°C)
- `weather_total_clouds`: Total Clouds (% Sky Obscured)
- `weather_visibility`: Visibility (m)
- `weather_wind_direction`: "Wind Direction (°)
- `weather_wind_speed`: Wind Speed (kmph)
### WeatherImport Provider
The `WeatherImport` provider is designed to import weather forecast data from a file or a JSON
string. An external entity should update the file or JSON string whenever new prediction data
becomes available.
The prediction keys for the PV forecast data are:
- `weather_dew_point`: Dew Point (°C)
- `weather_dhi`: Diffuse Horizontal Irradiance (W/m2)
- `weather_dni`: Direct Normal Irradiance (W/m2)
- `weather_feels_like`: Feels Like (°C)
- `weather_fog`: Fog (%)
- `weather_frost_chance`: Chance of Frost
- `weather_ghi`: Global Horizontal Irradiance (W/m2)
- `weather_high_clouds`: High Clouds (% Sky Obscured)
- `weather_low_clouds`: Low Clouds (% Sky Obscured)
- `weather_medium_clouds`: Medium Clouds (% Sky Obscured)
- `weather_ozone`: Ozone (du)
- `weather_precip_amt`: Precipitation Amount (mm)
- `weather_precip_prob`: Precipitation Probability (%)
- `weather_preciptable_water`: Precipitable Water (cm)
- `weather_precip_type`: Precipitation Type
- `weather_pressure`: Pressure (mb)
- `weather_relative_humidity`: Relative Humidity (%)
- `weather_temp_air`: Temperature (°C)
- `weather_total_clouds`: Total Clouds (% Sky Obscured)
- `weather_visibility`: Visibility (m)
- `weather_wind_direction`: "Wind Direction (°)
- `weather_wind_speed`: Wind Speed (kmph)
The PV forecast data must be provided in one of the formats described in
<project:#prediction-import-providers>. The data source must be given in the
`weatherimport_file_path` or `pvforecastimport_json` configuration option.

View File

@ -0,0 +1,7 @@
% SPDX-License-Identifier: Apache-2.0
# Server API
```{eval-sh}
./scripts/generate_openapi_md.py | ./scripts/extract_markdown.py --input-stdin --start-line "**Version**:"
```

View File

@ -1,17 +0,0 @@
..
SPDX-License-Identifier: Apache-2.0
File has to be of RST format to make openapi directive work correctly
.. _akkudoktoreos_server_api:
Server API
##########
For a more detailed documentation see the Swagger interface: `EOS OpenAPI Spec <https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/docs/akkudoktoreos/openapi.json>`_
.. openapi:: openapi.json
:examples:
..
Due to bugs in sphinxcontrib-openapi referenced request/response objects fail to render and anyOf is broken too.
:request:

View File

@ -23,8 +23,8 @@ extensions = [
"sphinx.ext.autosummary", "sphinx.ext.autosummary",
"sphinx.ext.napoleon", "sphinx.ext.napoleon",
"sphinx_rtd_theme", "sphinx_rtd_theme",
# "sphinxcontrib.openapi", buggy
"myst_parser", "myst_parser",
"sphinxcontrib.eval",
] ]
templates_path = ["_templates"] templates_path = ["_templates"]
@ -80,7 +80,7 @@ myst_url_schemes = {
}, },
} }
myst_number_code_blocks = ["typescript"] myst_number_code_blocks = ["typescript"]
myst_heading_anchors = 2 myst_heading_anchors = 3
myst_footnote_transition = True myst_footnote_transition = True
myst_dmath_double_inline = True myst_dmath_double_inline = True
myst_enable_checkboxes = True myst_enable_checkboxes = True
@ -117,9 +117,6 @@ autodoc_default_options = {
# -- Options for autosummary ------------------------------------------------- # -- Options for autosummary -------------------------------------------------
autosummary_generate = True autosummary_generate = True
# -- Options for openapi -----------------------------------------------------
# openapi_default_renderer = "httpdomain:old" buggy
# -- Options for napoleon ------------------------------------------------- # -- Options for napoleon -------------------------------------------------
napoleon_google_docstring = True napoleon_google_docstring = True
napoleon_numpy_docstring = False napoleon_numpy_docstring = False

View File

@ -0,0 +1,3 @@
```{eval-sh}
./scripts/extract_markdown.py --input-file CONTRIBUTING.md
```

View File

@ -0,0 +1,5 @@
# Getting Started
```{eval-sh}
./scripts/extract_markdown.py --input-file README.md --start-line "## Getting Involved"
```

View File

@ -14,7 +14,9 @@ welcome.md
akkudoktoreos/about.md akkudoktoreos/about.md
develop/getting_started.md develop/getting_started.md
develop/CONTRIBUTING.md develop/CONTRIBUTING.md
akkudoktoreos/serverapi.rst akkudoktoreos/prediction.md
akkudoktoreos/measurement.md
akkudoktoreos/serverapi.md
akkudoktoreos/api.rst akkudoktoreos/api.rst
``` ```

5887
openapi.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ linkify-it-py==2.0.3
myst-parser==4.0.0 myst-parser==4.0.0
sphinx==8.1.3 sphinx==8.1.3
sphinx_rtd_theme==3.0.2 sphinx_rtd_theme==3.0.2
sphinxcontrib-openapi==0.8.4 sphinxcontrib-eval==0.0.3
pytest==8.3.4 pytest==8.3.4
pytest-cov==6.0.0 pytest-cov==6.0.0
pytest-xprocess==1.0.2 pytest-xprocess==1.0.2

0
scripts/__init__.py Normal file
View File

179
scripts/extract_markdown.py Executable file
View File

@ -0,0 +1,179 @@
#!.venv/bin/python
r"""This module extracts a part of a markdown string from an input file or a given input string.
The extraction starts at a line that contains the content specified by the `--start-line` parameter
and ends at a line that contains the content specified by the `--end-line` parameter.
If `--start-line` is not specified, extraction starts from the beginning of the file or string.
If `--end-line` is not specified, extraction goes to the end of the file or string.
The extracted markdown string is written either to stdout or to the specified output file.
Additionally, the heading levels can be adjusted by specifying the `--heading-level` parameter.
Usage:
scripts/extract_markdown.py [--input-file INPUT_FILE | --input INPUT_STRING] [--start-line START_LINE] [--end-line END_LINE] [--output-file OUTPUT_FILE] [--heading-level HEADING_LEVEL]
Arguments:
--input-file : The file path to read the markdown content from.
--input : The markdown content as a string.
--start-line : Optional. The string content of the start line from where extraction begins.
--end-line : Optional. The string content of the end line where extraction ends.
--output-file : Optional. The file path to write the extracted markdown content to.
--heading-level: Optional. The number of additional `#` to add to markdown headings or to remove
from markdown headings if negative.
Example:
scripts/extract_markdown.py --input-file input.md --start-line "# Start" --end-line "# End" --output-file output.md --heading-level 1
scripts/extract_markdown.py --input "# Start\n\nSome content here\n\n# End" --start-line "# Start" --end-line "# End" --output-file output.md --heading-level 1
"""
"""
This module extracts a part of a markdown string from an input file or a given input string.
The extraction starts at a line that contains the content specified by the `--start-line` parameter
and ends at a line that contains the content specified by the `--end-line` parameter.
If `--start-line` is not specified, extraction starts from the beginning of the file or string.
If `--end-line` is not specified, extraction goes to the end of the file or string.
The extracted markdown string is written either to stdout or to the specified output file.
Additionally, the heading levels can be adjusted by specifying the `--heading-level` parameter.
Usage:
python extract_markdown.py [--input-file INPUT_FILE | --input INPUT_STRING | --input-stdin] [--start-line START_LINE] [--end-line END_LINE] [--output-file OUTPUT_FILE] [--heading-level HEADING_LEVEL]
Arguments:
--input-file : The file path to read the markdown content from.
--input : The markdown content as a string.
--input-stdin : Read markdown content from stdin.
--start-line : Optional. The string content of the start line from where extraction begins.
--end-line : Optional. The string content of the end line where extraction ends.
--output-file : Optional. The file path to write the extracted markdown content to.
--heading-level: Optional. The number of additional `#` to add to markdown headings or to remove from markdown headings if negative.
Example:
python extract_markdown.py --input-file input.md --start-line "# Start" --end-line "# End" --output-file output.md --heading-level 1
python extract_markdown.py --input "# Start\n\nSome content here\n\n# End" --start-line "# Start" --end-line "# End" --output-file output.md --heading-level 1
"""
import argparse
import re
import sys
def adjust_heading_levels(line: str, heading_level: int) -> str:
"""Adjust the heading levels in a markdown line.
Args:
line (str): The markdown line.
heading_level (int): The number of levels to adjust the headings by.
Returns:
adjusted_line (str): The line with adjusted heading levels.
"""
heading_pattern = re.compile(r"^(#+)\s")
match = heading_pattern.match(line)
if match:
current_level = len(match.group(1))
new_level = current_level + heading_level
if new_level > 0:
adjusted_line = "#" * new_level + line[current_level:]
else:
adjusted_line = line[current_level:]
else:
adjusted_line = line
return adjusted_line
def extract_markdown(content: str, start_line: str, end_line: str, heading_level: int) -> str:
"""Extract a part of a markdown string from given content.
Args:
content (str): The markdown content.
start_line (str): The string content of the start line from where extraction begins.
end_line (str): The string content of the end line where extraction ends.
heading_level (int): The number of levels to adjust the headings by.
Returns:
extracted_content (str): Extracted markdown content as a string.
"""
extracted_content = []
lines = content.splitlines(True)
extracting = start_line is None
for line in lines:
if not extracting and start_line and start_line in line:
extracting = True
extracted_content.append(
adjust_heading_levels(line, heading_level)
) # Include start line in output
continue
if extracting and end_line and end_line in line:
extracting = False
break
if extracting:
extracted_content.append(adjust_heading_levels(line, heading_level))
return "".join(extracted_content)
def main():
"""Main function to run the extraction of the markdown content."""
parser = argparse.ArgumentParser(
description="Extract a part of a markdown string from an input file"
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--input-file", type=str, help="File to read the markdown content from")
group.add_argument("--input", type=str, help="Markdown content as a string")
group.add_argument(
"--input-stdin", action="store_true", help="Read markdown content from stdin"
)
parser.add_argument(
"--start-line",
type=str,
default=None,
help="Optional. The string content of the start line",
)
parser.add_argument(
"--end-line", type=str, default=None, help="Optional. The string content of the end line"
)
parser.add_argument(
"--output-file",
type=str,
default=None,
help="File to write the extracted markdown content to",
)
parser.add_argument(
"--heading-level",
type=int,
default=0,
help="The number of additional `#` to add to markdown headings or to remove from markdown headings if negative",
)
args = parser.parse_args()
try:
if args.input_file:
with open(args.input_file, "r") as f:
content = f.read()
elif args.input:
content = args.input
elif args.input_stdin:
content = sys.stdin.read()
else:
raise ValueError("No valid input source provided.")
extracted_content = extract_markdown(
content, args.start_line, args.end_line, args.heading_level
)
if args.output_file:
# Write to file
with open(args.output_file, "w") as f:
f.write(extracted_content)
else:
# Write to std output
print(extracted_content)
except Exception as e:
print(f"Error during markdown extraction: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

69
scripts/generate_openapi.py Executable file
View File

@ -0,0 +1,69 @@
#!.venv/bin/python
"""This module generates the OpenAPI specification for the FastAPI application defined in `akkudoktoreos.server.fastapi_server`.
The script can be executed directly to generate the OpenAPI specification
either to the standard output or to a specified file.
Usage:
scripts/generate_openapi.py [--output-file OUTPUT_FILE]
Arguments:
--output-file : Optional. The file path to write the OpenAPI specification to.
Example:
scripts/generate_openapi.py --output-file openapi.json
"""
import argparse
import json
import sys
from fastapi.openapi.utils import get_openapi
from akkudoktoreos.server.fastapi_server import app
def generate_openapi() -> dict:
"""Generate the OpenAPI specification.
Returns:
openapi_spec (dict): OpenAPI specification.
"""
openapi_spec = get_openapi(
title=app.title,
version=app.version,
openapi_version=app.openapi_version,
description=app.description,
routes=app.routes,
)
return openapi_spec
def main():
"""Main function to run the generation of the OpenAPI specification."""
parser = argparse.ArgumentParser(description="Generate OpenAPI Specification")
parser.add_argument(
"--output-file", type=str, default=None, help="File to write the OpenAPI Specification to"
)
args = parser.parse_args()
try:
openapi_spec = generate_openapi()
openapi_spec_str = json.dumps(openapi_spec, indent=2)
if args.output_file:
# Write to file
with open(args.output_file, "w") as f:
f.write(openapi_spec_str)
else:
# Write to std output
print(openapi_spec_str)
except Exception as e:
print(f"Error during OpenAPI specification generation: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

239
scripts/generate_openapi_md.py Executable file
View File

@ -0,0 +1,239 @@
#!.venv/bin/python
"""Utility functions for OpenAPI specification conversion tasks."""
import argparse
import json
import sys
if __package__ is None or __package__ == "":
# uses current directory visibility
import generate_openapi
else:
# uses current package visibility
from . import generate_openapi
def extract_info(openapi_json: dict) -> dict:
"""Extract basic information from OpenAPI JSON.
Args:
openapi_json (dict): The OpenAPI specification as a Python dictionary.
Returns:
dict: A dictionary containing the title, version, description, and base_url.
"""
info = openapi_json.get("info", {})
servers = openapi_json.get("servers", [{}])
return {
"title": info.get("title", "API Documentation"),
"version": info.get("version", "1.0.0"),
"description": info.get("description", "No description provided."),
"base_url": servers[0].get("url", "No base URL provided."),
}
def format_authentication(security_schemes: dict) -> str:
"""Format the authentication section for the Markdown.
Args:
security_schemes (dict): The security schemes from the OpenAPI spec.
Returns:
str: The formatted authentication section in Markdown.
"""
if not security_schemes:
return ""
markdown = "## Authentication\n\n"
for scheme, details in security_schemes.items():
auth_type = details.get("type", "unknown")
markdown += f"- **{scheme}**: {auth_type}\n\n"
return markdown
def format_parameters(parameters: list) -> str:
"""Format the parameters section for the Markdown.
Args:
parameters (list): The list of parameters from an endpoint.
Returns:
str: The formatted parameters section in Markdown.
"""
if not parameters:
return ""
markdown = "**Parameters**:\n\n"
for param in parameters:
name = param.get("name", "unknown")
location = param.get("in", "unknown")
required = param.get("required", False)
description = param.get("description", "No description provided.")
markdown += (
f"- `{name}` ({location}, {'required' if required else 'optional'}): {description}\n\n"
)
return markdown
def format_request_body(request_body: dict) -> str:
"""Format the request body section for the Markdown.
Args:
request_body (dict): The request body content from an endpoint.
Returns:
str: The formatted request body section in Markdown.
"""
if not request_body:
return ""
markdown = "**Request Body**:\n\n"
for content_type, schema in request_body.items():
markdown += f"- `{content_type}`: {json.dumps(schema.get('schema', {}), indent=2)}\n\n"
return markdown
def format_responses(responses: dict) -> str:
"""Format the responses section for the Markdown.
Args:
responses (dict): The responses from an endpoint.
Returns:
str: The formatted responses section in Markdown.
"""
if not responses:
return ""
markdown = "**Responses**:\n\n"
for status, response in responses.items():
desc = response.get("description", "No description provided.")
markdown += f"- **{status}**: {desc}\n\n"
return markdown
def format_endpoint(path: str, method: str, details: dict) -> str:
"""Format a single endpoint's details for the Markdown.
Args:
path (str): The endpoint path.
method (str): The HTTP method.
details (dict): The details of the endpoint.
Returns:
str: The formatted endpoint section in Markdown.
"""
link_summary = (
details.get("summary", "<summary missing>")
.lower()
.strip()
.replace(" ", "_")
.replace("-", "_")
)
link_path = (
path.lower().strip().replace("/", "_").replace(".", "_").replace("{", "_").replace("}", "_")
)
link_method = f"_{method.lower()})"
# [local](http://localhost:8503/docs#/default/fastapi_config_get_v1_config_get)
local_path = (
"[local](http://localhost:8503/docs#/default/" + link_summary + link_path + link_method
)
# [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_strompreis_strompreis_get)
swagger_path = (
"[swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/"
+ link_summary
+ link_path
+ link_method
)
markdown = f"## {method.upper()} {path}\n\n"
markdown += f"**Links**: {local_path}, {swagger_path}\n\n"
summary = details.get("summary", None)
if summary:
markdown += f"{summary}\n\n"
description = details.get("description", None)
if description:
markdown += "```\n"
markdown += f"{description}"
markdown += "\n```\n\n"
markdown += format_parameters(details.get("parameters", []))
markdown += format_request_body(details.get("requestBody", {}).get("content", {}))
markdown += format_responses(details.get("responses", {}))
markdown += "---\n\n"
return markdown
def openapi_to_markdown(openapi_json: dict) -> str:
"""Convert OpenAPI JSON specification to a Markdown representation.
Args:
openapi_json (dict): The OpenAPI specification as a Python dictionary.
Returns:
str: The Markdown representation of the OpenAPI spec.
"""
info = extract_info(openapi_json)
markdown = f"# {info['title']}\n\n"
markdown += f"**Version**: `{info['version']}`\n\n"
markdown += f"**Description**: {info['description']}\n\n"
markdown += f"**Base URL**: `{info['base_url']}`\n\n"
security_schemes = openapi_json.get("components", {}).get("securitySchemes", {})
markdown += format_authentication(security_schemes)
markdown += "**Endpoints**:\n\n"
paths = openapi_json.get("paths", {})
for path, methods in paths.items():
for method, details in methods.items():
markdown += format_endpoint(path, method, details)
# Assure the is no double \n at end of file
markdown = markdown.rstrip("\n")
markdown += "\n"
return markdown
def generate_openapi_md() -> str:
"""Generate OpenAPI specification in Markdown.
Returns:
str: The Markdown representation of the OpenAPI spec.
"""
openapi_spec = generate_openapi.generate_openapi()
openapi_md = openapi_to_markdown(openapi_spec)
return openapi_md
def main():
"""Main function to run the generation of the OpenAPI specification as Markdown."""
parser = argparse.ArgumentParser(description="Generate OpenAPI Specification as Markdown")
parser.add_argument(
"--output-file", type=str, default=None, help="File to write the OpenAPI Specification to"
)
args = parser.parse_args()
try:
openapi_md = generate_openapi_md()
if args.output_file:
# Write to file
with open(args.output_file, "w") as f:
f.write(openapi_md)
else:
# Write to std output
print(openapi_md)
except Exception as e:
print(f"Error during OpenAPI specification generation: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -11,8 +11,6 @@ logger = get_logger(__name__)
class LoadCommonSettings(SettingsBaseModel): class LoadCommonSettings(SettingsBaseModel):
# Load 0
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."
) )
load_name: Optional[str] = Field(default=None, description="Name of the load source.")

View File

@ -18,13 +18,12 @@ logger = get_logger(__name__)
class LoadDataRecord(PredictionRecord): class LoadDataRecord(PredictionRecord):
"""Represents a load data record containing various load attributes at a specific datetime.""" """Represents a load data record containing various load attributes at a specific datetime."""
load_mean: Optional[float] = Field(default=None, description="Predicted load mean value (W)") load_mean: Optional[float] = Field(default=None, description="Predicted load mean value (W).")
load_std: Optional[float] = Field( load_std: Optional[float] = Field(
default=None, description="Predicted load standard deviation (W)" default=None, description="Predicted load standard deviation (W)."
) )
load_mean_adjusted: Optional[float] = Field( load_mean_adjusted: Optional[float] = Field(
default=None, description="Predicted load mean value adjusted by load measurement (W)" default=None, description="Predicted load mean value adjusted by load measurement (W)."
) )

View File

@ -18,8 +18,8 @@ logger = get_logger(__name__)
class PVForecastDataRecord(PredictionRecord): class PVForecastDataRecord(PredictionRecord):
"""Represents a pvforecast data record containing various pvforecast attributes at a specific datetime.""" """Represents a pvforecast data record containing various pvforecast attributes at a specific datetime."""
pvforecast_dc_power: Optional[float] = Field(default=None, description="Total DC power (W)") pvforecast_dc_power: Optional[float] = Field(default=None, description="Total DC power (W).")
pvforecast_ac_power: Optional[float] = Field(default=None, description="Total AC power (W)") pvforecast_ac_power: Optional[float] = Field(default=None, description="Total AC power (W).")
class PVForecastProvider(PredictionProvider): class PVForecastProvider(PredictionProvider):

View File

@ -23,13 +23,12 @@ class PVForecastImportCommonSettings(SettingsBaseModel):
"""Common settings for pvforecast data import from file or JSON string.""" """Common settings for pvforecast data import from file or JSON string."""
pvforecastimport_file_path: Optional[Union[str, Path]] = Field( pvforecastimport_file_path: Optional[Union[str, Path]] = Field(
default=None, description="Path to the file to import pvforecast data from." default=None, description="Path to the file to import PV forecast data from."
) )
pvforecastimport_json: Optional[str] = Field( pvforecastimport_json: Optional[str] = Field(
default=None, default=None,
description="JSON string, dictionary of PV forecast float value lists." description="JSON string, dictionary of PV forecast value lists.",
"Keys are 'pvforecast_dc_power', 'pvforecast_ac_power'.",
) )
# Validators # Validators

View File

@ -56,7 +56,7 @@ class WeatherDataRecord(PredictionRecord):
default=None, description="Low Clouds (% Sky Obscured)" default=None, description="Low Clouds (% Sky Obscured)"
) )
weather_medium_clouds: Optional[float] = Field( weather_medium_clouds: Optional[float] = Field(
None, description="Medium Clouds (% Sky Obscured)" default=None, description="Medium Clouds (% Sky Obscured)"
) )
weather_high_clouds: Optional[float] = Field( weather_high_clouds: Optional[float] = Field(
default=None, description="High Clouds (% Sky Obscured)" default=None, description="High Clouds (% Sky Obscured)"

View File

@ -219,7 +219,9 @@ def fastapi_measurement_keys_get() -> list[str]:
@app.get("/v1/measurement/load-mr/series/by-name") @app.get("/v1/measurement/load-mr/series/by-name")
def fastapi_measurement_load_mr_series_by_name_get(name: str) -> PydanticDateTimeSeries: def fastapi_measurement_load_mr_series_by_name_get(
name: Annotated[str, Query(description="Load name.")],
) -> PydanticDateTimeSeries:
"""Get the meter reading of given load name as series.""" """Get the meter reading of given load name as series."""
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:
@ -234,7 +236,9 @@ def fastapi_measurement_load_mr_series_by_name_get(name: str) -> PydanticDateTim
@app.put("/v1/measurement/load-mr/value/by-name") @app.put("/v1/measurement/load-mr/value/by-name")
def fastapi_measurement_load_mr_value_by_name_put( def fastapi_measurement_load_mr_value_by_name_put(
datetime: Any, name: str, value: Union[float | str] datetime: Annotated[str, Query(description="Datetime.")],
name: Annotated[str, Query(description="Load name.")],
value: Union[float | str],
) -> PydanticDateTimeSeries: ) -> PydanticDateTimeSeries:
"""Merge the meter reading of given load name and value into EOS measurements at given datetime.""" """Merge the meter reading of given load name and value into EOS measurements at given datetime."""
key = measurement_eos.name_to_key(name=name, topic="measurement_load") key = measurement_eos.name_to_key(name=name, topic="measurement_load")
@ -251,7 +255,7 @@ def fastapi_measurement_load_mr_value_by_name_put(
@app.put("/v1/measurement/load-mr/series/by-name") @app.put("/v1/measurement/load-mr/series/by-name")
def fastapi_measurement_load_mr_series_by_name_put( def fastapi_measurement_load_mr_series_by_name_put(
name: str, series: PydanticDateTimeSeries name: Annotated[str, Query(description="Load name.")], series: PydanticDateTimeSeries
) -> PydanticDateTimeSeries: ) -> PydanticDateTimeSeries:
"""Merge the meter readings series of given load name into EOS measurements at given datetime.""" """Merge the meter readings series of given load name into EOS measurements at given datetime."""
key = measurement_eos.name_to_key(name=name, topic="measurement_load") key = measurement_eos.name_to_key(name=name, topic="measurement_load")
@ -268,7 +272,9 @@ def fastapi_measurement_load_mr_series_by_name_put(
@app.get("/v1/measurement/series") @app.get("/v1/measurement/series")
def fastapi_measurement_series_get(key: str) -> PydanticDateTimeSeries: def fastapi_measurement_series_get(
key: Annotated[str, Query(description="Prediction key.")],
) -> 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}' not available.")
@ -278,7 +284,9 @@ def fastapi_measurement_series_get(key: str) -> PydanticDateTimeSeries:
@app.put("/v1/measurement/value") @app.put("/v1/measurement/value")
def fastapi_measurement_value_put( def fastapi_measurement_value_put(
datetime: Any, key: str, value: Union[float | str] datetime: Annotated[str, Query(description="Datetime.")],
key: Annotated[str, Query(description="Prediction key.")],
value: Union[float | str],
) -> 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:
@ -290,7 +298,7 @@ def fastapi_measurement_value_put(
@app.put("/v1/measurement/series") @app.put("/v1/measurement/series")
def fastapi_measurement_series_put( def fastapi_measurement_series_put(
key: str, series: PydanticDateTimeSeries key: Annotated[str, Query(description="Prediction key.")], series: PydanticDateTimeSeries
) -> 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:
@ -323,16 +331,23 @@ def fastapi_prediction_keys_get() -> list[str]:
@app.get("/v1/prediction/series") @app.get("/v1/prediction/series")
def fastapi_prediction_series_get( def fastapi_prediction_series_get(
key: str, key: Annotated[str, Query(description="Prediction key.")],
start_datetime: Optional[str] = None, start_datetime: Annotated[
end_datetime: Optional[str] = None, Optional[str],
Query(description="Starting datetime (inclusive)."),
] = None,
end_datetime: Annotated[
Optional[str],
Query(description="Ending datetime (exclusive)."),
] = None,
) -> PydanticDateTimeSeries: ) -> PydanticDateTimeSeries:
"""Get prediction for given key within given date range as series. """Get prediction for given key within given date range as series.
Args: Args:
start_datetime: Starting datetime (inclusive). key (str): Prediction key
start_datetime (Optional[str]): Starting datetime (inclusive).
Defaults to start datetime of latest prediction. Defaults to start datetime of latest prediction.
end_datetime: Ending datetime (exclusive). end_datetime (Optional[str]: Ending datetime (exclusive).
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:
@ -353,19 +368,29 @@ def fastapi_prediction_series_get(
@app.get("/v1/prediction/list") @app.get("/v1/prediction/list")
def fastapi_prediction_list_get( def fastapi_prediction_list_get(
key: str, key: Annotated[str, Query(description="Prediction key.")],
start_datetime: Optional[str] = None, start_datetime: Annotated[
end_datetime: Optional[str] = None, Optional[str],
interval: Optional[str] = None, Query(description="Starting datetime (inclusive)."),
] = None,
end_datetime: Annotated[
Optional[str],
Query(description="Ending datetime (exclusive)."),
] = None,
interval: Annotated[
Optional[str],
Query(description="Time duration for each interval."),
] = None,
) -> List[Any]: ) -> List[Any]:
"""Get prediction for given key within given date range as value list. """Get prediction for given key within given date range as value list.
Args: Args:
start_datetime: Starting datetime (inclusive). key (str): Prediction key
start_datetime (Optional[str]): Starting datetime (inclusive).
Defaults to start datetime of latest prediction. Defaults to start datetime of latest prediction.
end_datetime: Ending datetime (exclusive). end_datetime (Optional[str]: Ending datetime (exclusive).
Defaults to end datetime of latest prediction. Defaults to end datetime of latest prediction.
interval: Time duration for each interval interval (Optional[str]): Time duration for each interval.
Defaults to 1 hour. Defaults to 1 hour.
""" """
if key not in prediction_eos.record_keys: if key not in prediction_eos.record_keys:
@ -640,26 +665,24 @@ def site_map() -> RedirectResponse:
# Keep the proxy last to handle all requests that are not taken by the Rest API. # Keep the proxy last to handle all requests that are not taken by the Rest API.
# Also keep the single endpoints for delete, get, post, put to assure openapi.json is always build
# the same way for testing.
@app.delete("/{path:path}") @app.delete("/{path:path}", include_in_schema=False)
async def proxy_delete(request: Request, path: str) -> Response: async def proxy_delete(request: Request, path: str) -> Response:
return await proxy(request, path) return await proxy(request, path)
@app.get("/{path:path}") @app.get("/{path:path}", include_in_schema=False)
async def proxy_get(request: Request, path: str) -> Response: async def proxy_get(request: Request, path: str) -> Response:
return await proxy(request, path) return await proxy(request, path)
@app.post("/{path:path}") @app.post("/{path:path}", include_in_schema=False)
async def proxy_post(request: Request, path: str) -> Response: async def proxy_post(request: Request, path: str) -> Response:
return await proxy(request, path) return await proxy(request, path)
@app.put("/{path:path}") @app.put("/{path:path}", include_in_schema=False)
async def proxy_put(request: Request, path: str) -> Response: async def proxy_put(request: Request, path: str) -> Response:
return await proxy(request, path) return await proxy(request, path)

View File

@ -1,25 +0,0 @@
import json
from pathlib import Path
from fastapi.openapi.utils import get_openapi
from akkudoktoreos.server.fastapi_server import app
def generate_openapi(filename: str | Path = "openapi.json"):
with open(filename, "w") as f:
json.dump(
get_openapi(
title=app.title,
version=app.version,
openapi_version=app.openapi_version,
description=app.description,
routes=app.routes,
),
f,
indent=2,
)
if __name__ == "__main__":
generate_openapi()

View File

@ -1,27 +1,56 @@
import json import json
import sys
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
import pytest
DIR_PROJECT_ROOT = Path(__file__).parent.parent DIR_PROJECT_ROOT = Path(__file__).parent.parent
DIR_TESTDATA = Path(__file__).parent / "testdata" DIR_TESTDATA = Path(__file__).parent / "testdata"
def test_openapi_spec_current(config_eos): def test_openapi_spec_current(config_eos):
"""Verify the openapi spec hasn´t changed.""" """Verify the openapi spec hasn´t changed."""
old_spec_path = DIR_PROJECT_ROOT / "docs" / "akkudoktoreos" / "openapi.json" expected_spec_path = DIR_PROJECT_ROOT / "openapi.json"
new_spec_path = DIR_TESTDATA / "openapi-new.json" new_spec_path = DIR_TESTDATA / "openapi-new.json"
expected_spec_md_path = DIR_TESTDATA / "openapi.md"
new_spec_md_path = DIR_TESTDATA / "openapi-new.md"
with open(expected_spec_path) as f_expected:
expected_spec = json.load(f_expected)
with open(expected_spec_md_path) as f_expected:
expected_spec_md = f_expected.read()
# Patch get_config and import within guard to patch global variables within the fastapi_server module. # Patch get_config and import within guard to patch global variables within the fastapi_server module.
with patch("akkudoktoreos.config.config.get_config", return_value=config_eos): with patch("akkudoktoreos.config.config.get_config", return_value=config_eos):
from generate_openapi import generate_openapi # Ensure the script works correctly as part of a package
root_dir = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(root_dir))
from scripts import generate_openapi, generate_openapi_md
generate_openapi(new_spec_path) spec = generate_openapi.generate_openapi()
with open(new_spec_path) as f_new: spec_md = generate_openapi_md.generate_openapi_md()
new_spec = json.load(f_new)
with open(old_spec_path) as f_old: with open(new_spec_path, "w") as f_new:
old_spec = json.load(f_old) json.dump(spec, f_new, indent=4, sort_keys=True)
with open(new_spec_md_path, "w") as f_new:
f_new.write(spec_md)
# Serialize to ensure comparison is consistent # Serialize to ensure comparison is consistent
new_spec = json.dumps(new_spec, indent=4, sort_keys=True) spec_str = json.dumps(spec, indent=4, sort_keys=True)
old_spec = json.dumps(old_spec, indent=4, sort_keys=True) expected_spec_str = json.dumps(expected_spec, indent=4, sort_keys=True)
assert new_spec == old_spec try:
assert spec_str == expected_spec_str
except AssertionError as e:
pytest.fail(
f"Expected {new_spec_path} to equal {expected_spec_path}.\n"
+ f"If ok: cp {new_spec_path} {expected_spec_path}\n"
)
try:
assert spec_md == expected_spec_md
except AssertionError as e:
pytest.fail(
f"Expected {new_spec_md_path} to equal {expected_spec_md_path}.\n"
+ f"If ok: cp {new_spec_md_path} {expected_spec_md_path}\n"
)

569
tests/testdata/openapi.md vendored Normal file
View File

@ -0,0 +1,569 @@
# Akkudoktor-EOS
**Version**: `0.0.1`
**Description**: This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period.
**Base URL**: `No base URL provided.`
**Endpoints**:
## 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)
Fastapi Config Get
```
Get the current configuration.
```
**Responses**:
- **200**: Successful Response
---
## PUT /v1/config
**Links**: [local](http://localhost:8503/docs#/default/fastapi_config_put_v1_config_put), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_config_put_v1_config_put)
Fastapi Config Put
```
Merge settings into current configuration.
Args:
settings (SettingsEOS): The settings to merge into the current configuration.
save (Optional[bool]): Save the resulting configuration to the configuration file.
Defaults to False.
```
**Parameters**:
- `save` (query, optional): No description provided.
**Request Body**:
- `application/json`: {
"$ref": "#/components/schemas/SettingsEOS"
}
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## GET /v1/measurement/keys
**Links**: [local](http://localhost:8503/docs#/default/fastapi_measurement_keys_get_v1_measurement_keys_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_measurement_keys_get_v1_measurement_keys_get)
Fastapi Measurement Keys Get
```
Get a list of available measurement keys.
```
**Responses**:
- **200**: Successful Response
---
## GET /v1/measurement/load-mr/series/by-name
**Links**: [local](http://localhost:8503/docs#/default/fastapi_measurement_load_mr_series_by_name_get_v1_measurement_load-mr_series_by-name_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_measurement_load_mr_series_by_name_get_v1_measurement_load-mr_series_by-name_get)
Fastapi Measurement Load Mr Series By Name Get
```
Get the meter reading of given load name as series.
```
**Parameters**:
- `name` (query, required): Load name.
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## PUT /v1/measurement/load-mr/series/by-name
**Links**: [local](http://localhost:8503/docs#/default/fastapi_measurement_load_mr_series_by_name_put_v1_measurement_load-mr_series_by-name_put), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_measurement_load_mr_series_by_name_put_v1_measurement_load-mr_series_by-name_put)
Fastapi Measurement Load Mr Series By Name Put
```
Merge the meter readings series of given load name into EOS measurements at given datetime.
```
**Parameters**:
- `name` (query, required): Load name.
**Request Body**:
- `application/json`: {
"$ref": "#/components/schemas/PydanticDateTimeSeries"
}
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## PUT /v1/measurement/load-mr/value/by-name
**Links**: [local](http://localhost:8503/docs#/default/fastapi_measurement_load_mr_value_by_name_put_v1_measurement_load-mr_value_by-name_put), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_measurement_load_mr_value_by_name_put_v1_measurement_load-mr_value_by-name_put)
Fastapi Measurement Load Mr Value By Name Put
```
Merge the meter reading of given load name and value into EOS measurements at given datetime.
```
**Parameters**:
- `datetime` (query, required): Datetime.
- `name` (query, required): Load name.
- `value` (query, required): No description provided.
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## GET /v1/measurement/series
**Links**: [local](http://localhost:8503/docs#/default/fastapi_measurement_series_get_v1_measurement_series_get), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_measurement_series_get_v1_measurement_series_get)
Fastapi Measurement Series Get
```
Get the measurements of given key as series.
```
**Parameters**:
- `key` (query, required): Prediction key.
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## PUT /v1/measurement/series
**Links**: [local](http://localhost:8503/docs#/default/fastapi_measurement_series_put_v1_measurement_series_put), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_measurement_series_put_v1_measurement_series_put)
Fastapi Measurement Series Put
```
Merge measurement given as series into given key.
```
**Parameters**:
- `key` (query, required): Prediction key.
**Request Body**:
- `application/json`: {
"$ref": "#/components/schemas/PydanticDateTimeSeries"
}
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## PUT /v1/measurement/value
**Links**: [local](http://localhost:8503/docs#/default/fastapi_measurement_value_put_v1_measurement_value_put), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_measurement_value_put_v1_measurement_value_put)
Fastapi Measurement Value Put
```
Merge the measurement of given key and value into EOS measurements at given datetime.
```
**Parameters**:
- `datetime` (query, required): Datetime.
- `key` (query, required): Prediction key.
- `value` (query, required): No description provided.
**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**:
- **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
**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)
Fastapi Prediction Keys Get
```
Get a list of available prediction keys.
```
**Responses**:
- **200**: Successful Response
---
## 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
**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)
Fastapi Prediction List Get
```
Get prediction for given key within given date range as value list.
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.
interval (Optional[str]): Time duration for each interval.
Defaults to 1 hour.
```
**Parameters**:
- `key` (query, required): Prediction key.
- `start_datetime` (query, optional): Starting datetime (inclusive).
- `end_datetime` (query, optional): Ending datetime (exclusive).
- `interval` (query, optional): Time duration for each interval.
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## 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)
Fastapi Prediction Update
```
Update predictions for all providers.
Args:
force_update: Update data even if it is already cached.
Defaults to False.
force_enable: Update data even if provider is disabled.
Defaults to False.
```
**Parameters**:
- `force_update` (query, optional): No description provided.
- `force_enable` (query, optional): No description provided.
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## POST /v1/prediction/update/{provider_id}
**Links**: [local](http://localhost:8503/docs#/default/fastapi_prediction_update_provider_v1_prediction_update__provider_id__post), [swagger](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/openapi.json#/default/fastapi_prediction_update_provider_v1_prediction_update__provider_id__post)
Fastapi Prediction Update Provider
```
Update predictions for given provider ID.
Args:
provider_id: ID of provider to update.
force_update: Update data even if it is already cached.
Defaults to False.
force_enable: Update data even if provider is disabled.
Defaults to False.
```
**Parameters**:
- `provider_id` (path, required): No description provided.
- `force_update` (query, optional): No description provided.
- `force_enable` (query, optional): No description provided.
**Responses**:
- **200**: Successful Response
- **422**: Validation Error
---
## 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
**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)
Get Pdf
**Responses**:
- **200**: Successful Response
---