Migrate from Flask to FastAPI (#163)

* Migrate from Flask to FastAPI

 * FastAPI migration:
    - Use pydantic model classes as input parameters to the
      data/calculation classes.
    - Interface field names changed to constructor parameter names (for
      simplicity only during transition, should be updated in a followup
      PR).
    - Add basic interface requirements (e.g. some values > 0, etc.).
 * Update tests for new data format.
 * Python requirement down to 3.9 (TypeGuard no longer needed)
 * Makefile: Add helpful targets (e.g. development server with reload)

* Move API doc from README to pydantic model classes (swagger)

 * Link to swagger.io with own openapi.yml.
 * Commit openapi.json and check with pytest for changes so the
   documentation is always up-to-date.

* Streamline docker

* FastAPI: Run startup action on dev server

 * Fix config for /strompreis, endpoint still broken however.

* test_openapi: Compare against docs/.../openapi.json

* Move fastapi to server/ submodule

 * See #187 for new repository structure.
This commit is contained in:
Dominique Lasserre 2024-11-15 22:27:25 +01:00 committed by GitHub
parent ed3226e522
commit f61665669f
38 changed files with 997 additions and 1331 deletions

6
.env
View File

@ -1,10 +1,4 @@
EOS_VERSION=latest
EOS_PORT=8503
MARIADB_VERSION=11.1.6
PYTHON_VERSION=3.12.6
MARIADB_ROOT_PASSWORD=ruth
MARIADB_DATABASE=eos
MARIADB_USER=eos
MARIADB_PASSWORD=eos

3
.gitignore vendored
View File

@ -254,3 +254,6 @@ visualize_output_*.pdf
# Test images
*_pdf.png
# Test files
openapi-new.json

View File

@ -30,6 +30,8 @@ COPY src .
USER eos
ENTRYPOINT []
CMD ["python", "-m", "akkudoktoreosserver.flask_server"]
EXPOSE 8503
CMD ["python", "-m", "akkudoktoreos.server.fastapi_server"]
VOLUME ["${MPLCONFIGDIR}", "${EOS_CACHE_DIR}", "${EOS_OUTPUT_DIR}"]

View File

@ -1,5 +1,5 @@
# Define the targets
.PHONY: help venv pip install dist test test-full docker-run docs read-docs clean
.PHONY: help venv pip install dist test test-full docker-run docker-build docs read-docs clean format run run-dev
# Default target
all: help
@ -10,12 +10,14 @@ help:
@echo " venv - Set up a Python 3 virtual environment."
@echo " pip - Install dependencies from requirements.txt."
@echo " pip-dev - Install dependencies from requirements-dev.txt."
@echo " format - Format source code."
@echo " install - Install EOS in editable form (development mode) into virtual environment."
@echo " docker-run - Run entire setup on docker"
@echo " docker-build - Rebuild docker image"
@echo " docs - Generate HTML documentation (in build/docs/html/)."
@echo " read-docs - Read HTML documentation in your browser."
@echo " run - Run flask_server in the virtual environment (needs install before)."
@echo " run - Run FastAPI server in the virtual environment (needs install before)."
@echo " run-dev - Run FastAPI development server in the virtual environment (automatically reloads)."
@echo " dist - Create distribution (in dist/)."
@echo " clean - Remove generated documentation, distribution and virtual environment."
@ -66,8 +68,12 @@ clean:
@echo "Deletion complete."
run:
@echo "Starting flask server, please wait..."
.venv/bin/python -m akkudoktoreosserver.flask_server
@echo "Starting FastAPI server, please wait..."
.venv/bin/python -m akkudoktoreos.server.fastapi_server
run-dev:
@echo "Starting FastAPI development server, please wait..."
.venv/bin/fastapi dev src/akkudoktoreos/server/fastapi_server.py
# Target to setup tests.
test-setup: pip-dev
@ -83,9 +89,13 @@ test-full:
@echo "Running all tests..."
.venv/bin/pytest --full-run
# Target to format code.
format:
pre-commit run --all-files
# Run entire setup on docker
docker-run:
@docker compose up --remove-orphans
docker-build:
@docker compose build
@docker compose build --pull

2
NOTICE
View File

@ -17,7 +17,7 @@ This product may utilize technologies covered under international patents and/or
ADDITIONAL ATTRIBUTIONS:
The following is a list of licensors and other acknowledgements for third-party software that may be contained within this system:
- Flask, licensed under the BSD License, see https://flask.palletsprojects.com/
- FastAPI, licensed under the MIT License, see https://fastapi.tiangolo.com/
- NumPy, licensed under the BSD License, see https://numpy.org/
- Requests, licensed under the Apache License 2.0, see https://requests.readthedocs.io/
- matplotlib, licensed under the matplotlib License (a variant of the Python Software Foundation License), see https://matplotlib.org/

260
README.md
View File

@ -83,10 +83,18 @@ source .venv/bin/activate
## Usage
To use the system, run `flask_server.py`, which starts the server:
To use the system, run `fastapi_server.py`, which starts the server:
```bash
./flask_server.py
./src/akkudoktoreos/server/fastapi_server.py
```
### Docker
To run the system with Docker:
```bash
docker compose up --build
```
## Classes and Functionalities
@ -111,250 +119,6 @@ These classes work together to enable a detailed simulation and optimization of
Each class is designed to be easily customized and extended to integrate additional functions or improvements. For example, new methods can be added for more accurate modeling of PV system or battery behavior. Developers are invited to modify and extend the system according to their needs.
# Input for the Flask Server (as of 30.07.2024)
## Server API
Describes the structure and data types of the JSON object sent to the Flask server, with a forecast period of 48 hours.
## JSON Object Fields
### `strompreis_euro_pro_wh`
- **Description**: An array of floats representing the electricity price in euros per watt-hour for different time intervals.
- **Type**: Array
- **Element Type**: Float
- **Length**: 48
### `gesamtlast`
- **Description**: An array of floats representing the total load (consumption) in watts for different time intervals.
- **Type**: Array
- **Element Type**: Float
- **Length**: 48
### `pv_forecast`
- **Description**: An array of floats representing the forecasted photovoltaic output in watts for different time intervals.
- **Type**: Array
- **Element Type**: Float
- **Length**: 48
### `temperature_forecast`
- **Description**: An array of floats representing the temperature forecast in degrees Celsius for different time intervals.
- **Type**: Array
- **Element Type**: Float
- **Length**: 48
### `pv_soc`
- **Description**: An integer representing the state of charge of the PV battery at the **start** of the current hour (not the current state).
- **Type**: Integer
### `pv_akku_cap`
- **Description**: An integer representing the capacity of the photovoltaic battery in watt-hours.
- **Type**: Integer
### `einspeiseverguetung_euro_pro_wh`
- **Description**: A float representing the feed-in compensation in euros per watt-hour.
- **Type**: Float
### `eauto_min_soc`
- **Description**: An integer representing the minimum state of charge (SOC) of the electric vehicle in percentage.
- **Type**: Integer
### `eauto_cap`
- **Description**: An integer representing the capacity of the electric vehicle battery in watt-hours.
- **Type**: Integer
### `eauto_charge_efficiency`
- **Description**: A float representing the charging efficiency of the electric vehicle.
- **Type**: Float
### `eauto_charge_power`
- **Description**: An integer representing the charging power of the electric vehicle in watts.
- **Type**: Integer
### `eauto_soc`
- **Description**: An integer representing the current state of charge (SOC) of the electric vehicle in percentage.
- **Type**: Integer
### `start_solution`
- **Description**: Can be `null` or contain a previous solution (if available).
- **Type**: `null` or object
### `haushaltsgeraet_wh`
- **Description**: An integer representing the energy consumption of a household device in watt-hours.
- **Type**: Integer
### `haushaltsgeraet_dauer`
- **Description**: An integer representing the usage duration of a household device in hours.
- **Type**: Integer
# JSON Output Description
This document describes the structure and data types of the JSON output returned by the Flask server, with a forecast period of 48 hours.
**Note**: The first value of "Last_Wh_pro_Stunde", "Netzeinspeisung_Wh_pro_Stunde" and "Netzbezug_Wh_pro_Stunde", will be set to null in the JSON output and represented as NaN or None in the corresponding classes' data returns. This approach is adopted to ensure that the current hour's processing remains unchanged.
## JSON Output Fields (as of 30.7.2024)
### discharge_hours_bin
An array that indicates for each hour of the forecast period (in this example, 48 hours) whether energy is discharged from the battery or not. The values are either `0` (no discharge) or `1` (discharge).
### eauto_obj
This object contains information related to the electric vehicle and its charging and discharging behavior:
- **charge_array**: Indicates for each hour whether the EV is charging (`0` for no charging, `1` for charging).
- **Type**: Array
- **Element Type**: Integer (0 or 1)
- **Length**: 48
- **discharge_array**: Indicates for each hour whether the EV is discharging (`0` for no discharging, `1` for discharging).
- **Type**: Array
- **Element Type**: Integer (0 or 1)
- **Length**: 48
- **entlade_effizienz**: The discharge efficiency as a float.
- **Type**: Float
- **hours**: Amount of hours the simulation is done for.
- **Type**: Integer
- **kapazitaet_wh**: The capacity of the EVs battery in watt-hours.
- **Type**: Integer
- **lade_effizienz**: The charging efficiency as a float.
- **Type**: Float
- **max_ladeleistung_w**: The maximum charging power of the EV in watts.
- **Type**: Float
- **max_ladeleistung_w**: Max charging power of the EV in Watts.
- **Type**: Integer
- **soc_wh**: The state of charge of the battery in watt-hours at the start of the simulation.
- **Type**: Integer
- **start_soc_prozent**: The state of charge of the battery in percentage at the start of the simulation.
- **Type**: Integer
### eautocharge_hours_float
An array of binary values (0 or 1) that indicates whether the EV will be charged in a certain hour.
- **Type**: Array
- **Element Type**: Integer (0 or 1)
- **Length**: 48
### result
This object contains the results of the simulation and provides insights into various parameters over the entire forecast period:
- **E-Auto_SoC_pro_Stunde**: The state of charge of the EV for each hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
- **Eigenverbrauch_Wh_pro_Stunde**: The self-consumption of the system in watt-hours per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
- **Einnahmen_Euro_pro_Stunde**: The revenue from grid feed-in or other sources in euros per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
- **Gesamt_Verluste**: The total losses in watt-hours over the entire period.
- **Type**: Float
- **Gesamtbilanz_Euro**: The total balance of revenues minus costs in euros.
- **Type**: Float
- **Gesamteinnahmen_Euro**: The total revenues in euros.
- **Type**: Float
- **Gesamtkosten_Euro**: The total costs in euros.
- **Type**: Float
- **Haushaltsgeraet_wh_pro_stunde**: The energy consumption of a household appliance in watt-hours per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
- **Kosten_Euro_pro_Stunde**: The costs in euros per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
- **Netzbezug_Wh_pro_Stunde**: The grid energy drawn in watt-hours per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
- **Netzeinspeisung_Wh_pro_Stunde**: The energy fed into the grid in watt-hours per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
- **Verluste_Pro_Stunde**: The losses in watt-hours per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
- **akku_soc_pro_stunde**: The state of charge of the battery (not the EV) in percentage per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
### simulation_data
An object containing the simulated data.
- **E-Auto_SoC_pro_Stunde**: An array of floats representing the simulated state of charge of the electric car per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
- **Eigenverbrauch_Wh_pro_Stunde**: An array of floats representing the simulated self-consumption in watt-hours per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
- **Einnahmen_Euro_pro_Stunde**: An array of floats representing the simulated income in euros per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
- **Gesamt_Verluste**: The total simulated losses in watt-hours.
- **Type**: Float
- **Gesamtbilanz_Euro**: The total simulated balance in euros.
- **Type**: Float
- **Gesamteinnahmen_Euro**: The total simulated income in euros.
- **Type**: Float
- **Gesamtkosten_Euro**: The total simulated costs in euros.
- **Type**: Float
- **Haushaltsgeraet_wh_pro_stunde**: An array of floats representing the simulated energy consumption of a household appliance in watt-hours per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
- **Kosten_Euro_pro_Stunde**: An array of floats representing the simulated costs in euros per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
- **Netzbezug_Wh_pro_Stunde**: An array of floats representing the simulated grid consumption in watt-hours per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
- **Netzeinspeisung_Wh_pro_Stunde**: An array of floats representing the simulated grid feed-in in watt-hours per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
- **Verluste_Pro_Stunde**: An array of floats representing the simulated losses per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
- **akku_soc_pro_stunde**: An array of floats representing the simulated state of charge of the battery in percentage per hour.
- **Type**: Array
- **Element Type**: Float
- **Length**: 35
### spuelstart
- **Description**: Can be `null` or contain an object representing the start of washing (if applicable).
- **Type**: null or object
### start_solution
- **Description**: An array of binary values (0 or 1) representing a possible starting solution for the simulation.
- **Type**: Array
- **Element Type**: Integer (0 or 1)
- **Length**: 48
See the Swagger 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)

View File

@ -1,6 +1,6 @@
---
networks:
eos:
default:
name: "eos"
services:
eos:
@ -11,11 +11,6 @@ services:
dockerfile: "Dockerfile"
args:
PYTHON_VERSION: "${PYTHON_VERSION}"
init: true
environment:
FLASK_RUN_PORT: "${EOS_PORT}"
networks:
- "eos"
volumes:
- ./src/akkudoktoreos/default.config.json:/opt/eos/EOS.config.json:ro
ports:

View File

@ -13,4 +13,3 @@ EOS API
:recursive:
akkudoktoreos
akkudoktoreosserver

View File

@ -31,7 +31,7 @@
"/gesamtlast": {
"post": {
"summary": "Fastapi Gesamtlast",
"description": "Endpoint to handle total load calculation based on the latest measured data",
"description": "Endpoint to handle total load calculation based on the latest measured data.",
"operationId": "fastapi_gesamtlast_gesamtlast_post",
"parameters": [
{
@ -260,8 +260,10 @@
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
"application/pdf": {
"schema": {
"type": "string"
}
}
}
}
@ -399,7 +401,7 @@
"start_soc_prozent"
],
"title": "EAutoResult",
"description": "\"This object contains information related to the electric vehicle and its charging and discharging behavior"
"description": "This object contains information related to the electric vehicle and its charging and discharging behavior."
},
"EnergieManagementSystemParameters": {
"properties": {
@ -420,9 +422,19 @@
"description": "An array of floats representing the electricity price in euros per watt-hour for different time intervals."
},
"einspeiseverguetung_euro_pro_wh": {
"type": "number",
"anyOf": [
{
"items": {
"type": "number"
},
"type": "array"
},
{
"type": "number"
}
],
"title": "Einspeiseverguetung Euro Pro Wh",
"description": "A float representing the feed-in compensation in euros per watt-hour."
"description": "A float or array of floats representing the feed-in compensation in euros per watt-hour."
},
"preis_euro_pro_wh_akku": {
"type": "number",
@ -567,21 +579,29 @@
},
"OptimizeResponse": {
"properties": {
"discharge_hours_bin": {
"items": {
"type": "integer"
},
"type": "array",
"title": "Discharge Hours Bin",
"description": "An array that indicates for each hour of the forecast period whether energy is discharged from the battery or not. The values are either `0` (no discharge) or `1` (discharge)."
},
"eautocharge_hours_float": {
"ac_charge": {
"items": {
"type": "number"
},
"type": "array",
"title": "Eautocharge Hours Float",
"description": "An array of binary values (0 or 1) that indicates whether the EV will be charged in a certain hour."
"title": "Ac Charge",
"description": "Array with AC charging values as relative power (0-1), other values set to 0."
},
"dc_charge": {
"items": {
"type": "number"
},
"type": "array",
"title": "Dc Charge",
"description": "Array with DC charging values as relative power (0-1), other values set to 0."
},
"discharge_allowed": {
"items": {
"type": "integer"
},
"type": "array",
"title": "Discharge Allowed",
"description": "Array with discharge values (1 for discharge, 0 otherwise)."
},
"result": {
"$ref": "#/components/schemas/SimulationResult"
@ -619,8 +639,9 @@
},
"type": "object",
"required": [
"discharge_hours_bin",
"eautocharge_hours_float",
"ac_charge",
"dc_charge",
"discharge_allowed",
"result",
"eauto_obj"
],
@ -869,7 +890,7 @@
"akku_soc_pro_stunde"
],
"title": "SimulationResult",
"description": "This object contains the results of the simulation and provides insights into various parameters over the entire forecast period"
"description": "This object contains the results of the simulation and provides insights into various parameters over the entire forecast period."
},
"ValidationError": {
"properties": {

View File

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

View File

@ -7,7 +7,7 @@ authors = [
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."
readme = "README.md"
license = {file = "LICENSE"}
requires-python = ">=3.10"
requires-python = ">=3.9"
classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",
@ -28,11 +28,10 @@ optional-dependencies = {dev = { file = ["requirements-dev.txt"] }}
[tool.setuptools.packages.find]
where = ["src/"]
include = ["akkudoktoreos", "akkudoktoreosserver", ]
include = ["akkudoktoreos"]
[tool.setuptools.package-data]
akkudoktoreos = ["*.json", ]
akkudoktoreosserver = ["data/*.npz", ]
akkudoktoreos = ["*.json", "data/*.npz", ]
[tool.pyright]
# used in Pylance extension for language server

View File

@ -1,6 +1,8 @@
numpy==2.1.3
matplotlib==3.9.2
flask==3.0.3
fastapi[standard]==0.115.0
uvicorn==0.31.1
pydantic==2.9.2
scikit-learn==1.5.2
timezonefinder==6.5.4
deap==1.4.1

View File

@ -5,7 +5,11 @@ import time
import numpy as np
from akkudoktoreos.class_numpy_encoder import NumpyEncoder
from akkudoktoreos.class_optimize import optimization_problem
from akkudoktoreos.class_optimize import (
OptimizationParameters,
OptimizeResponse,
optimization_problem,
)
from akkudoktoreos.config import get_working_dir, load_config
from akkudoktoreos.visualize import visualisiere_ergebnisse
@ -229,48 +233,52 @@ gesamtlast = [
start_solution = None
# Define parameters for the optimization problem
parameter = {
# Value of energy in battery (per Wh)
"preis_euro_pro_wh_akku": 0e-05,
# Initial state of charge (SOC) of PV battery (%)
"pv_soc": 15,
# Battery capacity (in Wh)
"pv_akku_cap": 26400,
# Yearly energy consumption (in Wh)
"year_energy": 4100000,
# Feed-in tariff for exporting electricity (per Wh)
"einspeiseverguetung_euro_pro_wh": 7e-05,
# Maximum heating power (in W)
"max_heizleistung": 1000,
# Overall load on the system
"gesamtlast": gesamtlast,
# PV generation forecast (48 hours)
"pv_forecast": pv_forecast,
# Temperature forecast (48 hours)
"temperature_forecast": temperature_forecast,
# Electricity price forecast (48 hours)
"strompreis_euro_pro_wh": strompreis_euro_pro_wh,
# Minimum SOC for electric car
"eauto_min_soc": 50,
# Electric car battery capacity (Wh)
"eauto_cap": 60000,
# Charging efficiency of the electric car
"eauto_charge_efficiency": 0.95,
# Charging power of the electric car (W)
"eauto_charge_power": 11040,
# Current SOC of the electric car (%)
"eauto_soc": 15,
# Current PV power generation (W)
"pvpowernow": 211.137503624,
# Initial solution for the optimization
"start_solution": start_solution,
# Household appliance consumption (Wh)
"haushaltsgeraet_wh": 5000,
# Duration of appliance usage (hours)
"haushaltsgeraet_dauer": 0,
# Minimum Soc PV Battery
"min_soc_prozent": 15,
}
parameters = OptimizationParameters(
**{
"ems": {
# Value of energy in battery (per Wh)
"preis_euro_pro_wh_akku": 0e-05,
# Feed-in tariff for exporting electricity (per Wh)
"einspeiseverguetung_euro_pro_wh": 7e-05,
# Overall load on the system
"gesamtlast": gesamtlast,
# PV generation forecast (48 hours)
"pv_prognose_wh": pv_forecast,
# Electricity price forecast (48 hours)
"strompreis_euro_pro_wh": strompreis_euro_pro_wh,
},
"pv_akku": {
# Battery capacity (in Wh)
"kapazitaet_wh": 26400,
# Initial state of charge (SOC) of PV battery (%)
"start_soc_prozent": 15,
# Minimum Soc PV Battery
"min_soc_prozent": 15,
},
"eauto": {
# Minimum SOC for electric car
"min_soc_prozent": 50,
# Electric car battery capacity (Wh)
"kapazitaet_wh": 60000,
# Charging efficiency of the electric car
"lade_effizienz": 0.95,
# Charging power of the electric car (W)
"max_ladeleistung_w": 11040,
# Current SOC of the electric car (%)
"start_soc_prozent": 5,
},
# "spuelmaschine": {
# # Household appliance consumption (Wh)
# "verbrauch_wh": 5000,
# # Duration of appliance usage (hours)
# "dauer_h": 0,
# },
# Temperature forecast (48 hours)
"temperature_forecast": temperature_forecast,
# Initial solution for the optimization
"start_solution": start_solution,
}
)
# Startzeit nehmen
start_time = time.time()
@ -281,7 +289,7 @@ config = load_config(working_dir)
opt_class = optimization_problem(config, verbose=True, fixed_seed=42)
# Perform the optimisation based on the provided parameters and start hour
ergebnis = opt_class.optimierung_ems(parameter=parameter, start_hour=start_hour)
ergebnis = opt_class.optimierung_ems(parameters=parameters, start_hour=start_hour)
# Endzeit nehmen
end_time = time.time()
@ -298,23 +306,23 @@ ac_charge, dc_charge, discharge = (
)
visualisiere_ergebnisse(
gesamtlast=gesamtlast,
pv_forecast=pv_forecast,
strompreise=strompreis_euro_pro_wh,
ergebnisse=ergebnis["result"],
ac=ac_charge,
dc=dc_charge,
discharge=discharge,
temperature=temperature_forecast,
start_hour=start_hour,
parameters.ems.gesamtlast,
parameters.ems.pv_prognose_wh,
parameters.ems.strompreis_euro_pro_wh,
ergebnis["result"],
ac_charge,
dc_charge,
discharge,
parameters.temperature_forecast,
start_hour,
einspeiseverguetung_euro_pro_wh=np.full(
config.eos.feed_in_tariff_eur_per_wh, parameter["einspeiseverguetung_euro_pro_wh"]
config.eos.feed_in_tariff_eur_per_wh, parameters.ems.einspeiseverguetung_euro_pro_wh
),
config=config,
filename="visualization_results.pdf",
extra_data=None,
)
json_data = NumpyEncoder.dumps(ergebnis)
print(json_data)
OptimizeResponse(**ergebnis)

View File

@ -1,32 +1,74 @@
from typing import Optional
import numpy as np
from pydantic import BaseModel, Field
def max_ladeleistung_w_field(default=None):
return Field(
default,
gt=0,
description="An integer representing the charging power of the battery in watts.",
)
def start_soc_prozent_field(description: str):
return Field(0, ge=0, le=100, description=description)
class BaseAkkuParameters(BaseModel):
kapazitaet_wh: int = Field(
gt=0, description="An integer representing the capacity of the battery in watt-hours."
)
lade_effizienz: float = Field(
0.88, gt=0, le=1, description="A float representing the charging efficiency of the battery."
)
entlade_effizienz: float = Field(0.88, gt=0, le=1)
max_ladeleistung_w: Optional[float] = max_ladeleistung_w_field()
start_soc_prozent: int = start_soc_prozent_field(
"An integer representing the state of charge of the battery at the **start** of the current hour (not the current state)."
)
min_soc_prozent: int = Field(
0,
ge=0,
le=100,
description="An integer representing the minimum state of charge (SOC) of the battery in percentage.",
)
max_soc_prozent: int = Field(100, ge=0, le=100)
class PVAkkuParameters(BaseAkkuParameters):
max_ladeleistung_w: Optional[float] = max_ladeleistung_w_field(5000)
class EAutoParameters(BaseAkkuParameters):
entlade_effizienz: float = 1.0
start_soc_prozent: int = start_soc_prozent_field(
"An integer representing the current state of charge (SOC) of the battery in percentage."
)
class PVAkku:
def __init__(
self,
kapazitaet_wh=None,
hours=None,
lade_effizienz=0.88,
entlade_effizienz=0.88,
max_ladeleistung_w=None,
start_soc_prozent=0,
min_soc_prozent=0,
max_soc_prozent=100,
):
def __init__(self, parameters: BaseAkkuParameters, hours: int = 24):
# Battery capacity in Wh
self.kapazitaet_wh = kapazitaet_wh
self.kapazitaet_wh = parameters.kapazitaet_wh
# Initial state of charge in Wh
self.start_soc_prozent = start_soc_prozent
self.soc_wh = (start_soc_prozent / 100) * kapazitaet_wh
self.hours = hours if hours is not None else 24 # Default to 24 hours if not specified
self.start_soc_prozent = parameters.start_soc_prozent
self.soc_wh = (parameters.start_soc_prozent / 100) * parameters.kapazitaet_wh
self.hours = hours
self.discharge_array = np.full(self.hours, 1)
self.charge_array = np.full(self.hours, 1)
# Charge and discharge efficiency
self.lade_effizienz = lade_effizienz
self.entlade_effizienz = entlade_effizienz
self.max_ladeleistung_w = max_ladeleistung_w if max_ladeleistung_w else self.kapazitaet_wh
self.min_soc_prozent = min_soc_prozent
self.max_soc_prozent = max_soc_prozent
self.lade_effizienz = parameters.lade_effizienz
self.entlade_effizienz = parameters.entlade_effizienz
self.max_ladeleistung_w = (
parameters.max_ladeleistung_w if parameters.max_ladeleistung_w else self.kapazitaet_wh
)
# Only assign for storage battery
self.min_soc_prozent = (
parameters.min_soc_prozent if isinstance(parameters, PVAkkuParameters) else 0
)
self.max_soc_prozent = parameters.max_soc_prozent
# Calculate min and max SoC in Wh
self.min_soc_wh = (self.min_soc_prozent / 100) * self.kapazitaet_wh
self.max_soc_wh = (self.max_soc_prozent / 100) * self.kapazitaet_wh

View File

@ -2,27 +2,63 @@ from datetime import datetime
from typing import Dict, List, Optional, Union
import numpy as np
from pydantic import BaseModel, Field, model_validator
from typing_extensions import Self
from akkudoktoreos.class_akku import PVAkku
from akkudoktoreos.class_haushaltsgeraet import Haushaltsgeraet
from akkudoktoreos.class_inverter import Wechselrichter
from akkudoktoreos.config import EOSConfig
class EnergieManagementSystemParameters(BaseModel):
pv_prognose_wh: list[float] = Field(
description="An array of floats representing the forecasted photovoltaic output in watts for different time intervals."
)
strompreis_euro_pro_wh: list[float] = Field(
description="An array of floats representing the electricity price in euros per watt-hour for different time intervals."
)
einspeiseverguetung_euro_pro_wh: list[float] | float = Field(
description="A float or array of floats representing the feed-in compensation in euros per watt-hour."
)
preis_euro_pro_wh_akku: float
gesamtlast: list[float] = Field(
description="An array of floats representing the total load (consumption) in watts for different time intervals."
)
@model_validator(mode="after")
def validate_list_length(self) -> Self:
pv_prognose_length = len(self.pv_prognose_wh)
if (
pv_prognose_length != len(self.strompreis_euro_pro_wh)
or pv_prognose_length != len(self.gesamtlast)
or (
isinstance(self.einspeiseverguetung_euro_pro_wh, list)
and pv_prognose_length != len(self.einspeiseverguetung_euro_pro_wh)
)
):
raise ValueError("Input lists have different lengths")
return self
class EnergieManagementSystem:
def __init__(
self,
config: EOSConfig,
pv_prognose_wh: Optional[np.ndarray] = None,
strompreis_euro_pro_wh: Optional[np.ndarray] = None,
einspeiseverguetung_euro_pro_wh: Optional[np.ndarray] = None,
eauto: Optional[object] = None,
gesamtlast: Optional[np.ndarray] = None,
haushaltsgeraet: Optional[object] = None,
wechselrichter: Optional[object] = None,
parameters: EnergieManagementSystemParameters,
eauto: Optional[PVAkku] = None,
haushaltsgeraet: Optional[Haushaltsgeraet] = None,
wechselrichter: Optional[Wechselrichter] = None,
):
self.akku = wechselrichter.akku
self.gesamtlast = gesamtlast
self.pv_prognose_wh = pv_prognose_wh
self.strompreis_euro_pro_wh = strompreis_euro_pro_wh
self.einspeiseverguetung_euro_pro_wh = einspeiseverguetung_euro_pro_wh
self.gesamtlast = np.array(parameters.gesamtlast, float)
self.pv_prognose_wh = np.array(parameters.pv_prognose_wh, float)
self.strompreis_euro_pro_wh = np.array(parameters.strompreis_euro_pro_wh, float)
self.einspeiseverguetung_euro_pro_wh_arr = (
parameters.einspeiseverguetung_euro_pro_wh
if isinstance(parameters.einspeiseverguetung_euro_pro_wh, list)
else np.full(len(self.gesamtlast), parameters.einspeiseverguetung_euro_pro_wh, float)
)
self.eauto = eauto
self.haushaltsgeraet = haushaltsgeraet
self.wechselrichter = wechselrichter
@ -134,7 +170,7 @@ class EnergieManagementSystem:
netzbezug * self.strompreis_euro_pro_wh[stunde]
)
einnahmen_euro_pro_stunde[stunde_since_now] = (
netzeinspeisung * self.einspeiseverguetung_euro_pro_wh[stunde]
netzeinspeisung * self.einspeiseverguetung_euro_pro_wh_arr[stunde]
)
# Akku SOC tracking
@ -152,7 +188,7 @@ class EnergieManagementSystem:
"akku_soc_pro_stunde": akku_soc_pro_stunde,
"Einnahmen_Euro_pro_Stunde": einnahmen_euro_pro_stunde,
"Gesamtbilanz_Euro": gesamtkosten_euro,
"E-Auto_SoC_pro_Stunde": eauto_soc_pro_stunde,
"EAuto_SoC_pro_Stunde": eauto_soc_pro_stunde,
"Gesamteinnahmen_Euro": np.nansum(einnahmen_euro_pro_stunde),
"Gesamtkosten_Euro": np.nansum(kosten_euro_pro_stunde),
"Verluste_Pro_Stunde": verluste_wh_pro_stunde,

View File

@ -1,11 +1,25 @@
import numpy as np
from pydantic import BaseModel, Field
class HaushaltsgeraetParameters(BaseModel):
verbrauch_wh: int = Field(
gt=0,
description="An integer representing the energy consumption of a household device in watt-hours.",
)
dauer_h: int = Field(
gt=0,
description="An integer representing the usage duration of a household device in hours.",
)
class Haushaltsgeraet:
def __init__(self, hours=None, verbrauch_wh=None, dauer_h=None):
def __init__(self, parameters: HaushaltsgeraetParameters, hours=24):
self.hours = hours # Total duration for which the planning is done
self.verbrauch_wh = verbrauch_wh # Total energy consumption of the device in kWh
self.dauer_h = dauer_h # Duration of use in hours
self.verbrauch_wh = (
parameters.verbrauch_wh # Total energy consumption of the device in kWh
)
self.dauer_h = parameters.dauer_h # Duration of use in hours
self.lastkurve = np.zeros(self.hours) # Initialize the load curve with zeros
def set_startzeitpunkt(self, start_hour, global_start_hour=0):

View File

@ -1,6 +1,17 @@
from pydantic import BaseModel, Field
from akkudoktoreos.class_akku import PVAkku
class WechselrichterParameters(BaseModel):
max_leistung_wh: float = Field(10000, gt=0)
class Wechselrichter:
def __init__(self, max_leistung_wh, akku):
self.max_leistung_wh = max_leistung_wh # Maximum power that the inverter can handle
def __init__(self, parameters: WechselrichterParameters, akku: PVAkku):
self.max_leistung_wh = (
parameters.max_leistung_wh # Maximum power that the inverter can handle
)
self.akku = akku # Connection to a battery object
def energie_verarbeiten(self, erzeugung, verbrauch, hour):

View File

@ -1,17 +1,165 @@
import random
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Optional, Tuple
import numpy as np
from deap import algorithms, base, creator, tools
from pydantic import BaseModel, Field, model_validator
from typing_extensions import Self
from akkudoktoreos.class_akku import PVAkku
from akkudoktoreos.class_ems import EnergieManagementSystem
from akkudoktoreos.class_haushaltsgeraet import Haushaltsgeraet
from akkudoktoreos.class_inverter import Wechselrichter
from akkudoktoreos.class_akku import EAutoParameters, PVAkku, PVAkkuParameters
from akkudoktoreos.class_ems import (
EnergieManagementSystem,
EnergieManagementSystemParameters,
)
from akkudoktoreos.class_haushaltsgeraet import (
Haushaltsgeraet,
HaushaltsgeraetParameters,
)
from akkudoktoreos.class_inverter import Wechselrichter, WechselrichterParameters
from akkudoktoreos.config import AppConfig
from akkudoktoreos.visualize import visualisiere_ergebnisse
class OptimizationParameters(BaseModel):
ems: EnergieManagementSystemParameters
pv_akku: PVAkkuParameters
wechselrichter: WechselrichterParameters = WechselrichterParameters()
eauto: EAutoParameters
spuelmaschine: Optional[HaushaltsgeraetParameters] = None
temperature_forecast: list[float] = Field(
"An array of floats representing the temperature forecast in degrees Celsius for different time intervals."
)
start_solution: Optional[list[float]] = Field(
None, description="Can be `null` or contain a previous solution (if available)."
)
@model_validator(mode="after")
def validate_list_length(self) -> Self:
arr_length = len(self.ems.pv_prognose_wh)
if arr_length != len(self.temperature_forecast):
raise ValueError("Input lists have different lenghts")
return self
class EAutoResult(BaseModel):
"""This object contains information related to the electric vehicle and its charging and discharging behavior."""
charge_array: list[float] = Field(
description="Indicates for each hour whether the EV is charging (`0` for no charging, `1` for charging)."
)
discharge_array: list[int] = Field(
description="Indicates for each hour whether the EV is discharging (`0` for no discharging, `1` for discharging)."
)
entlade_effizienz: float = Field(description="The discharge efficiency as a float.")
hours: int = Field("Amount of hours the simulation is done for.")
kapazitaet_wh: int = Field("The capacity of the EVs battery in watt-hours.")
lade_effizienz: float = Field("The charging efficiency as a float.")
max_ladeleistung_w: int = Field(description="The maximum charging power of the EV in watts.")
soc_wh: float = Field(
description="The state of charge of the battery in watt-hours at the start of the simulation."
)
start_soc_prozent: int = Field(
description="The state of charge of the battery in percentage at the start of the simulation."
)
class SimulationResult(BaseModel):
"""This object contains the results of the simulation and provides insights into various parameters over the entire forecast period."""
Last_Wh_pro_Stunde: list[Optional[float]] = Field(description="TBD")
EAuto_SoC_pro_Stunde: list[Optional[float]] = Field(
description="The state of charge of the EV for each hour."
)
Einnahmen_Euro_pro_Stunde: list[Optional[float]] = Field(
description="The revenue from grid feed-in or other sources in euros per hour."
)
Gesamt_Verluste: float = Field(
description="The total losses in watt-hours over the entire period."
)
Gesamtbilanz_Euro: float = Field(
description="The total balance of revenues minus costs in euros."
)
Gesamteinnahmen_Euro: float = Field(description="The total revenues in euros.")
Gesamtkosten_Euro: float = Field(description="The total costs in euros.")
Haushaltsgeraet_wh_pro_stunde: list[Optional[float]] = Field(
description="The energy consumption of a household appliance in watt-hours per hour."
)
Kosten_Euro_pro_Stunde: list[Optional[float]] = Field(
description="The costs in euros per hour."
)
Netzbezug_Wh_pro_Stunde: list[Optional[float]] = Field(
description="The grid energy drawn in watt-hours per hour."
)
Netzeinspeisung_Wh_pro_Stunde: list[Optional[float]] = Field(
description="The energy fed into the grid in watt-hours per hour."
)
Verluste_Pro_Stunde: list[Optional[float]] = Field(
description="The losses in watt-hours per hour."
)
akku_soc_pro_stunde: list[Optional[float]] = Field(
description="The state of charge of the battery (not the EV) in percentage per hour."
)
# class SimulationData(BaseModel):
# """An object containing the simulated data."""
#
# Last_Wh_pro_Stunde: list[Optional[float]] = Field(description="TBD")
# EAuto_SoC_pro_Stunde: list[Optional[float]] = Field(
# description="An array of floats representing the simulated state of charge of the electric car per hour.",
# )
# Einnahmen_Euro_pro_Stunde: list[Optional[float]] = Field(
# description="An array of floats representing the simulated income in euros per hour."
# )
# Gesamt_Verluste: float = Field(description="The total simulated losses in watt-hours.")
# Gesamtbilanz_Euro: float = Field(description="The total simulated balance in euros.")
# Gesamteinnahmen_Euro: float = Field(description="The total simulated income in euros.")
# Gesamtkosten_Euro: float = Field(description="The total simulated costs in euros.")
# Haushaltsgeraet_wh_pro_stunde: list[Optional[float]] = Field(
# description="An array of floats representing the simulated energy consumption of a household appliance in watt-hours per hour."
# )
# Kosten_Euro_pro_Stunde: list[Optional[float]] = Field(
# description="An array of floats representing the simulated costs in euros per hour."
# )
# Netzbezug_Wh_pro_Stunde: list[Optional[float]] = Field(
# description="An array of floats representing the simulated grid consumption in watt-hours per hour."
# )
# Netzeinspeisung_Wh_pro_Stunde: list[Optional[float]] = Field(
# description="An array of floats representing the simulated grid feed-in in watt-hours per hour."
# )
# Verluste_Pro_Stunde: list[Optional[float]] = Field(
# description="An array of floats representing the simulated losses per hour."
# )
# akku_soc_pro_stunde: list[Optional[float]] = Field(
# description="An array of floats representing the simulated state of charge of the battery in percentage per hour."
# )
class OptimizeResponse(BaseModel):
"""**Note**: The first value of "Last_Wh_pro_Stunde", "Netzeinspeisung_Wh_pro_Stunde" and "Netzbezug_Wh_pro_Stunde", will be set to null in the JSON output and represented as NaN or None in the corresponding classes' data returns. This approach is adopted to ensure that the current hour's processing remains unchanged."""
ac_charge: list[float] = Field(
description="Array with AC charging values as relative power (0-1), other values set to 0."
)
dc_charge: list[float] = Field(
description="Array with DC charging values as relative power (0-1), other values set to 0."
)
discharge_allowed: list[int] = Field(
description="Array with discharge values (1 for discharge, 0 otherwise)."
)
result: SimulationResult
eauto_obj: EAutoResult
start_solution: Optional[list[float]] = Field(
None,
description="An array of binary values (0 or 1) representing a possible starting solution for the simulation.",
)
spuelstart: Optional[int] = Field(
None,
description="Can be `null` or contain an object representing the start of washing (if applicable).",
)
# simulation_data: Optional[SimulationData] = None
class optimization_problem:
def __init__(
self,
@ -164,8 +312,8 @@ class optimization_problem:
return creator.Individual(individual_components)
def split_individual(
self, individual: List[float]
) -> Tuple[List[int], List[float], Optional[int]]:
self, individual: list[float]
) -> Tuple[list[int], list[float], Optional[int]]:
"""Split the individual solution into its components.
Components:
@ -187,7 +335,7 @@ class optimization_problem:
)
return discharge_hours_bin, eautocharge_hours_float, spuelstart_int
def setup_deap_environment(self, opti_param: Dict[str, Any], start_hour: int) -> None:
def setup_deap_environment(self, opti_param: dict[str, Any], start_hour: int) -> None:
"""Set up the DEAP environment with fitness and individual creation rules."""
self.opti_param = opti_param
@ -250,8 +398,8 @@ class optimization_problem:
self.toolbox.register("select", tools.selTournament, tournsize=3)
def evaluate_inner(
self, individual: List[float], ems: EnergieManagementSystem, start_hour: int
) -> Dict[str, Any]:
self, individual: list[float], ems: EnergieManagementSystem, start_hour: int
) -> dict[str, Any]:
"""Simulates the energy management system (EMS) using the provided individual solution.
This is an internal function.
@ -283,9 +431,9 @@ class optimization_problem:
def evaluate(
self,
individual: List[float],
individual: list[float],
ems: EnergieManagementSystem,
parameter: Dict[str, Any],
parameters: OptimizationParameters,
start_hour: int,
worst_case: bool,
) -> Tuple[float]:
@ -305,7 +453,7 @@ class optimization_problem:
)
# Penalty for not meeting the minimum SOC (State of Charge) requirement
# if parameter["eauto_min_soc"] - ems.eauto.ladezustand_in_prozent() <= 0.0 and self.optimize_ev:
# if parameters.eauto_min_soc_prozent - ems.eauto.ladezustand_in_prozent() <= 0.0 and self.optimize_ev:
# gesamtbilanz += sum(
# self.strafe for ladeleistung in eautocharge_hours_float if ladeleistung != 0.0
# )
@ -313,26 +461,27 @@ class optimization_problem:
individual.extra_data = (
o["Gesamtbilanz_Euro"],
o["Gesamt_Verluste"],
parameter["eauto_min_soc"] - ems.eauto.ladezustand_in_prozent(),
parameters.eauto.min_soc_prozent - ems.eauto.ladezustand_in_prozent(),
)
# Adjust total balance with battery value and penalties for unmet SOC
restwert_akku = ems.akku.aktueller_energieinhalt() * parameter["preis_euro_pro_wh_akku"]
# print(ems.akku.aktueller_energieinhalt()," * ", parameter["preis_euro_pro_wh_akku"] , " ", restwert_akku, " ", gesamtbilanz)
restwert_akku = ems.akku.aktueller_energieinhalt() * parameters.ems.preis_euro_pro_wh_akku
# print(ems.akku.aktueller_energieinhalt()," * ", parameters.ems.preis_euro_pro_wh_akku , " ", restwert_akku, " ", gesamtbilanz)
gesamtbilanz += -restwert_akku
# print(gesamtbilanz)
if self.optimize_ev:
gesamtbilanz += max(
0,
(parameter["eauto_min_soc"] - ems.eauto.ladezustand_in_prozent()) * self.strafe,
(parameters.eauto.min_soc_prozent - ems.eauto.ladezustand_in_prozent())
* self.strafe,
)
return (gesamtbilanz,)
def optimize(
self, start_solution: Optional[List[float]] = None, ngen: int = 400
) -> Tuple[Any, Dict[str, List[Any]]]:
self, start_solution: Optional[list[float]] = None, ngen: int = 400
) -> Tuple[Any, dict[str, list[Any]]]:
"""Run the optimization process using a genetic algorithm."""
population = self.toolbox.population(n=300)
hof = tools.HallOfFame(1)
@ -373,61 +522,50 @@ class optimization_problem:
def optimierung_ems(
self,
parameter: Optional[Dict[str, Any]] = None,
parameters: OptimizationParameters,
start_hour: Optional[int] = None,
worst_case: bool = False,
startdate: Optional[Any] = None, # startdate is not used!
*,
ngen: int = 600,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""Perform EMS (Energy Management System) optimization and visualize results."""
einspeiseverguetung_euro_pro_wh = np.full(
self.prediction_hours, parameter["einspeiseverguetung_euro_pro_wh"]
self.prediction_hours, parameters.ems.einspeiseverguetung_euro_pro_wh
)
# Initialize PV and EV batteries
akku = PVAkku(
kapazitaet_wh=parameter["pv_akku_cap"],
parameters.pv_akku,
hours=self.prediction_hours,
start_soc_prozent=parameter["pv_soc"],
min_soc_prozent=parameter["min_soc_prozent"],
max_ladeleistung_w=5000,
)
akku.set_charge_per_hour(np.full(self.prediction_hours, 1))
self.optimize_ev = True
if parameter["eauto_min_soc"] - parameter["eauto_soc"] < 0:
if parameters.eauto.min_soc_prozent - parameters.eauto.start_soc_prozent < 0:
self.optimize_ev = False
eauto = PVAkku(
kapazitaet_wh=parameter["eauto_cap"],
parameters.eauto,
hours=self.prediction_hours,
lade_effizienz=parameter["eauto_charge_efficiency"],
entlade_effizienz=1.0,
max_ladeleistung_w=parameter["eauto_charge_power"],
start_soc_prozent=parameter["eauto_soc"],
)
eauto.set_charge_per_hour(np.full(self.prediction_hours, 1))
# Initialize household appliance if applicable
spuelmaschine = (
Haushaltsgeraet(
parameters=parameters.spuelmaschine,
hours=self.prediction_hours,
verbrauch_wh=parameter["haushaltsgeraet_wh"],
dauer_h=parameter["haushaltsgeraet_dauer"],
)
if parameter["haushaltsgeraet_dauer"] > 0
if parameters.spuelmaschine is not None
else None
)
# Initialize the inverter and energy management system
wr = Wechselrichter(10000, akku)
wr = Wechselrichter(parameters.wechselrichter, akku)
ems = EnergieManagementSystem(
config=self._config.eos,
gesamtlast=parameter["gesamtlast"],
pv_prognose_wh=parameter["pv_forecast"],
strompreis_euro_pro_wh=parameter["strompreis_euro_pro_wh"],
einspeiseverguetung_euro_pro_wh=einspeiseverguetung_euro_pro_wh,
self._config.eos,
parameters.ems,
eauto=eauto,
haushaltsgeraet=spuelmaschine,
wechselrichter=wr,
@ -437,9 +575,9 @@ class optimization_problem:
self.setup_deap_environment({"haushaltsgeraete": 1 if spuelmaschine else 0}, start_hour)
self.toolbox.register(
"evaluate",
lambda ind: self.evaluate(ind, ems, parameter, start_hour, worst_case),
lambda ind: self.evaluate(ind, ems, parameters, start_hour, worst_case),
)
start_solution, extra_data = self.optimize(parameter["start_solution"], ngen=ngen) #
start_solution, extra_data = self.optimize(parameters.start_solution, ngen=ngen)
# Perform final evaluation on the best solution
o = self.evaluate_inner(start_solution, ems, start_hour)
@ -455,16 +593,16 @@ class optimization_problem:
ac_charge, dc_charge, discharge = self.decode_charge_discharge(discharge_hours_bin)
# Visualize the results
visualisiere_ergebnisse(
gesamtlast=parameter["gesamtlast"],
pv_forecast=parameter["pv_forecast"],
strompreise=parameter["strompreis_euro_pro_wh"],
ergebnisse=o,
ac=ac_charge,
dc=dc_charge,
discharge=discharge,
temperature=parameter["temperature_forecast"],
start_hour=start_hour,
einspeiseverguetung_euro_pro_wh=einspeiseverguetung_euro_pro_wh,
parameters.ems.gesamtlast,
parameters.ems.pv_prognose_wh,
parameters.ems.strompreis_euro_pro_wh,
o,
ac_charge,
dc_charge,
discharge,
parameters.temperature_forecast,
start_hour,
einspeiseverguetung_euro_pro_wh,
config=self._config,
extra_data=extra_data,
)
@ -477,7 +615,7 @@ class optimization_problem:
"Netzbezug_Wh_pro_Stunde",
"Kosten_Euro_pro_Stunde",
"Einnahmen_Euro_pro_Stunde",
"E-Auto_SoC_pro_Stunde",
"EAuto_SoC_pro_Stunde",
"Verluste_Pro_Stunde",
"Haushaltsgeraet_wh_pro_stunde",
]
@ -507,5 +645,5 @@ class optimization_problem:
"eauto_obj": ems.eauto.to_dict(),
"start_solution": start_solution,
"spuelstart": spuelstart_int,
"simulation_data": o,
# "simulation_data": o,
}

View File

@ -108,6 +108,11 @@ def validate_pv_forecast_data(data) -> str:
return data_type
class ForecastResponse(BaseModel):
temperature: list[float]
pvpower: list[float]
class ForecastData:
"""Stores forecast data for PV power and weather parameters.

View File

View File

@ -0,0 +1,252 @@
#!/usr/bin/env python3
import os
from datetime import datetime
from pathlib import Path
from typing import Annotated, Any, Optional
import matplotlib
import uvicorn
from fastapi.exceptions import HTTPException
# Sets the Matplotlib backend to 'Agg' for rendering plots in environments without a display
matplotlib.use("Agg")
import pandas as pd
from fastapi import FastAPI, Query
from fastapi.responses import FileResponse, RedirectResponse
from akkudoktoreos.class_load import LoadForecast
from akkudoktoreos.class_load_container import Gesamtlast
from akkudoktoreos.class_load_corrector import LoadPredictionAdjuster
from akkudoktoreos.class_optimize import (
OptimizationParameters,
OptimizeResponse,
optimization_problem,
)
from akkudoktoreos.class_pv_forecast import ForecastResponse, PVForecast
from akkudoktoreos.class_strompreis import HourlyElectricityPriceForecast
from akkudoktoreos.config import (
SetupIncomplete,
get_start_enddate,
get_working_dir,
load_config,
)
app = FastAPI(
title="Akkudoktor-EOS",
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.",
summary="Comprehensive solution for simulating and optimizing an energy system based on renewable energy sources",
version="0.0.1",
license_info={
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
},
)
working_dir = get_working_dir()
# copy config to working directory. Make this a CLI option later
config = load_config(working_dir, True)
opt_class = optimization_problem(config)
server_dir = Path(__file__).parent.resolve()
class PdfResponse(FileResponse):
media_type = "application/pdf"
@app.get("/strompreis")
def fastapi_strompreis() -> list[float]:
# Get the current date and the end date based on prediction hours
date_now, date = get_start_enddate(config.eos.prediction_hours, startdate=datetime.now().date())
price_forecast = HourlyElectricityPriceForecast(
source=f"https://api.akkudoktor.net/prices?start={date_now}&end={date}",
config=config,
use_cache=False,
)
specific_date_prices = price_forecast.get_price_for_daterange(
date_now, date
) # Fetch prices for the specified date range
return specific_date_prices.tolist()
@app.post("/gesamtlast")
def fastapi_gesamtlast(
year_energy: float,
measured_data: list[dict[str, Any]],
hours: int = config.eos.prediction_hours,
) -> list[float]:
"""Endpoint to handle total load calculation based on the latest measured data."""
# Measured data in JSON format
measured_data_df = pd.DataFrame(measured_data)
measured_data_df["time"] = pd.to_datetime(measured_data_df["time"])
# Ensure datetime has timezone info for accurate calculations
if measured_data_df["time"].dt.tz is None:
measured_data_df["time"] = measured_data_df["time"].dt.tz_localize("Europe/Berlin")
else:
measured_data_df["time"] = measured_data_df["time"].dt.tz_convert("Europe/Berlin")
# Remove timezone info after conversion to simplify further processing
measured_data_df["time"] = measured_data_df["time"].dt.tz_localize(None)
# Instantiate LoadForecast and generate forecast data
lf = LoadForecast(
filepath=server_dir / ".." / "data" / "load_profiles.npz", year_energy=year_energy
)
forecast_list = []
# Generate daily forecasts for the date range based on measured data
for single_date in pd.date_range(
measured_data_df["time"].min().date(), measured_data_df["time"].max().date()
):
date_str = single_date.strftime("%Y-%m-%d")
daily_forecast = lf.get_daily_stats(date_str)
mean_values = daily_forecast[0]
fc_hours = [single_date + pd.Timedelta(hours=i) for i in range(24)]
daily_forecast_df = pd.DataFrame({"time": fc_hours, "Last Pred": mean_values})
forecast_list.append(daily_forecast_df)
# Concatenate all daily forecasts into a single DataFrame
predicted_data = pd.concat(forecast_list, ignore_index=True)
# Create LoadPredictionAdjuster instance to adjust the predictions based on measured data
adjuster = LoadPredictionAdjuster(measured_data_df, predicted_data, lf)
adjuster.calculate_weighted_mean() # Calculate weighted mean for adjustment
adjuster.adjust_predictions() # Adjust predictions based on measured data
future_predictions = adjuster.predict_next_hours(hours) # Predict future load
# Extract household power predictions
leistung_haushalt = future_predictions["Adjusted Pred"].values
gesamtlast = Gesamtlast(prediction_hours=hours)
gesamtlast.hinzufuegen(
"Haushalt", leistung_haushalt
) # Add household load to total load calculation
# Calculate the total load
last = gesamtlast.gesamtlast_berechnen() # Compute total load
return last.tolist()
@app.get("/gesamtlast_simple")
def fastapi_gesamtlast_simple(year_energy: float) -> list[float]:
date_now, date = get_start_enddate(
config.eos.prediction_hours, startdate=datetime.now().date()
) # Get the current date and prediction end date
###############
# Load Forecast
###############
lf = LoadForecast(
filepath=server_dir / ".." / "data" / "load_profiles.npz", year_energy=year_energy
) # Instantiate LoadForecast with specified parameters
leistung_haushalt = lf.get_stats_for_date_range(date_now, date)[
0
] # Get expected household load for the date range
gesamtlast = Gesamtlast(
prediction_hours=config.eos.prediction_hours
) # Create Gesamtlast instance
gesamtlast.hinzufuegen(
"Haushalt", leistung_haushalt
) # Add household load to total load calculation
# ###############
# # WP (Heat Pump)
# ##############
# leistung_wp = wp.simulate_24h(temperature_forecast) # Simulate heat pump load for 24 hours
# gesamtlast.hinzufuegen("Heatpump", leistung_wp) # Add heat pump load to total load calculation
last = gesamtlast.gesamtlast_berechnen() # Calculate total load
print(last) # Output total load
return last.tolist() # Return total load as JSON
@app.get("/pvforecast")
def fastapi_pvprognose(url: str, ac_power_measurement: Optional[float] = None) -> ForecastResponse:
date_now, date = get_start_enddate(config.eos.prediction_hours, startdate=datetime.now().date())
###############
# PV Forecast
###############
PVforecast = PVForecast(
prediction_hours=config.eos.prediction_hours, url=url
) # Instantiate PVForecast with given parameters
if ac_power_measurement is not None:
PVforecast.update_ac_power_measurement(
date_time=datetime.now(),
ac_power_measurement=ac_power_measurement,
) # Update measurement
# Get PV forecast and temperature forecast for the specified date range
pv_forecast = PVforecast.get_pv_forecast_for_date_range(date_now, date)
temperature_forecast = PVforecast.get_temperature_for_date_range(date_now, date)
# Return both forecasts as a JSON response
ret = {
"temperature": temperature_forecast.tolist(),
"pvpower": pv_forecast.tolist(),
}
return ret
@app.post("/optimize")
def fastapi_optimize(
parameters: OptimizationParameters,
start_hour: Annotated[
Optional[int], Query(description="Defaults to current hour of the day.")
] = None,
) -> OptimizeResponse:
if start_hour is None:
start_hour = datetime.now().hour
# Perform optimization simulation
result = opt_class.optimierung_ems(parameters=parameters, start_hour=start_hour)
# print(result)
# convert to JSON (None accepted by dumps)
return result
@app.get("/visualization_results.pdf", response_class=PdfResponse)
def get_pdf():
# Endpoint to serve the generated PDF with visualization results
output_path = config.working_dir / config.directories.output
if not output_path.is_dir():
raise SetupIncomplete(f"Output path does not exist: {output_path}.")
file_path = output_path / "visualization_results.pdf"
if not file_path.is_file():
raise HTTPException(status_code=404, detail="No visualization result available.")
return FileResponse(file_path)
@app.get("/site-map", include_in_schema=False)
def site_map():
return RedirectResponse(url="/docs")
@app.get("/", include_in_schema=False)
def root():
# Redirect the root URL to the site map
return RedirectResponse(url="/docs")
if __name__ == "__main__":
try:
config.run_setup()
except Exception as e:
print(f"Failed to initialize: {e}")
exit(1)
# Set host and port from environment variables or defaults
host = os.getenv("EOS_RUN_HOST", "0.0.0.0")
port = os.getenv("EOS_RUN_PORT", 8503)
try:
uvicorn.run(app, host=host, port=int(port)) # Run the FastAPI application
except Exception as e:
print(
f"Could not bind to host {host}:{port}. Error: {e}"
) # Error handling for binding issues
exit(1)
else:
# started from cli / dev server
config.run_setup()

View File

@ -179,7 +179,7 @@ def visualisiere_ergebnisse(
plt.plot(hours, ergebnisse["akku_soc_pro_stunde"], label="PV Battery (%)", marker="x")
plt.plot(
hours,
ergebnisse["E-Auto_SoC_pro_Stunde"],
ergebnisse["EAuto_SoC_pro_Stunde"],
label="E-Car Battery (%)",
marker="x",
)

View File

@ -1,305 +0,0 @@
#!/usr/bin/env python3
import os
from datetime import datetime
from typing import Any, TypeGuard
import matplotlib
# Sets the Matplotlib backend to 'Agg' for rendering plots in environments without a display
matplotlib.use("Agg")
import pandas as pd
from flask import Flask, jsonify, redirect, request, send_from_directory, url_for
from akkudoktoreos.class_load import LoadForecast
from akkudoktoreos.class_load_container import Gesamtlast
from akkudoktoreos.class_load_corrector import LoadPredictionAdjuster
from akkudoktoreos.class_numpy_encoder import NumpyEncoder
from akkudoktoreos.class_optimize import optimization_problem
from akkudoktoreos.class_pv_forecast import PVForecast
from akkudoktoreos.class_strompreis import HourlyElectricityPriceForecast
from akkudoktoreos.config import (
SetupIncomplete,
get_start_enddate,
get_working_dir,
load_config,
)
app = Flask(__name__)
working_dir = get_working_dir()
# copy config to working directory. Make this a CLI option later
config = load_config(working_dir, True)
opt_class = optimization_problem(config)
def isfloat(num: Any) -> TypeGuard[float]:
"""Check if a given input can be converted to float."""
if num is None:
return False
if isinstance(num, str):
num = num.strip() # Strip any surrounding whitespace
try:
float_value = float(num)
return not (
float_value == float("inf")
or float_value == float("-inf")
or float_value != float_value
) # Excludes NaN or Infinity
except (ValueError, TypeError):
return False
@app.route("/strompreis", methods=["GET"])
def flask_strompreis():
# Get the current date and the end date based on prediction hours
date_now, date = get_start_enddate(config.eos.prediction_hours, startdate=datetime.now().date())
price_forecast = HourlyElectricityPriceForecast(
source=f"https://api.akkudoktor.net/prices?start={date_now}&end={date}",
config=config.eos.prediction_hours,
use_cache=False,
)
specific_date_prices = price_forecast.get_price_for_daterange(
date_now, date
) # Fetch prices for the specified date range
return jsonify(specific_date_prices.tolist())
# Endpoint to handle total load calculation based on the latest measured data
@app.route("/gesamtlast", methods=["POST"])
def flask_gesamtlast():
# Retrieve data from the JSON body
data = request.get_json()
# Extract year_energy and prediction_hours from the request JSON
year_energy = float(data.get("year_energy"))
prediction_hours = int(data.get("hours", 48)) # Default to 48 hours if not specified
# Measured data in JSON format
measured_data_json = data.get("measured_data")
measured_data = pd.DataFrame(measured_data_json)
measured_data["time"] = pd.to_datetime(measured_data["time"])
# Ensure datetime has timezone info for accurate calculations
if measured_data["time"].dt.tz is None:
measured_data["time"] = measured_data["time"].dt.tz_localize("Europe/Berlin")
else:
measured_data["time"] = measured_data["time"].dt.tz_convert("Europe/Berlin")
# Remove timezone info after conversion to simplify further processing
measured_data["time"] = measured_data["time"].dt.tz_localize(None)
# Instantiate LoadForecast and generate forecast data
file_path = os.path.join("data", "load_profiles.npz")
lf = LoadForecast(filepath=file_path, year_energy=year_energy)
forecast_list = []
# Generate daily forecasts for the date range based on measured data
for single_date in pd.date_range(
measured_data["time"].min().date(), measured_data["time"].max().date()
):
date_str = single_date.strftime("%Y-%m-%d")
daily_forecast = lf.get_daily_stats(date_str)
mean_values = daily_forecast[0]
hours = [single_date + pd.Timedelta(hours=i) for i in range(24)]
daily_forecast_df = pd.DataFrame({"time": hours, "Last Pred": mean_values})
forecast_list.append(daily_forecast_df)
# Concatenate all daily forecasts into a single DataFrame
predicted_data = pd.concat(forecast_list, ignore_index=True)
# Create LoadPredictionAdjuster instance to adjust the predictions based on measured data
adjuster = LoadPredictionAdjuster(measured_data, predicted_data, lf)
adjuster.calculate_weighted_mean() # Calculate weighted mean for adjustment
adjuster.adjust_predictions() # Adjust predictions based on measured data
future_predictions = adjuster.predict_next_hours(prediction_hours) # Predict future load
# Extract household power predictions
leistung_haushalt = future_predictions["Adjusted Pred"].values
gesamtlast = Gesamtlast(prediction_hours=prediction_hours)
gesamtlast.hinzufuegen(
"Haushalt", leistung_haushalt
) # Add household load to total load calculation
# Calculate the total load
last = gesamtlast.gesamtlast_berechnen() # Compute total load
return jsonify(last.tolist())
@app.route("/gesamtlast_simple", methods=["GET"])
def flask_gesamtlast_simple():
if request.method == "GET":
year_energy = float(
request.args.get("year_energy")
) # Get annual energy value from query parameters
date_now, date = get_start_enddate(
config.eos.prediction_hours, startdate=datetime.now().date()
) # Get the current date and prediction end date
###############
# Load Forecast
###############
server_dir = os.path.dirname(os.path.realpath(__file__))
file_path = os.path.join(server_dir, "data", "load_profiles.npz")
print(file_path)
lf = LoadForecast(
filepath=file_path, year_energy=year_energy
) # Instantiate LoadForecast with specified parameters
leistung_haushalt = lf.get_stats_for_date_range(date_now, date)[
0
] # Get expected household load for the date range
gesamtlast = Gesamtlast(
prediction_hours=config.eos.prediction_hours
) # Create Gesamtlast instance
gesamtlast.hinzufuegen(
"Haushalt", leistung_haushalt
) # Add household load to total load calculation
# ###############
# # WP (Heat Pump)
# ##############
# leistung_wp = wp.simulate_24h(temperature_forecast) # Simulate heat pump load for 24 hours
# gesamtlast.hinzufuegen("Heatpump", leistung_wp) # Add heat pump load to total load calculation
last = gesamtlast.gesamtlast_berechnen() # Calculate total load
print(last) # Output total load
return jsonify(last.tolist()) # Return total load as JSON
@app.route("/pvforecast", methods=["GET"])
def flask_pvprognose():
if request.method == "GET":
# Retrieve URL and AC power measurement from query parameters
url = request.args.get("url")
ac_power_measurement = request.args.get("ac_power_measurement")
date_now, date = get_start_enddate(
config.eos.prediction_hours, startdate=datetime.now().date()
)
###############
# PV Forecast
###############
PVforecast = PVForecast(
prediction_hours=config.eos.prediction_hours, url=url
) # Instantiate PVForecast with given parameters
if isfloat(ac_power_measurement): # Check if the AC power measurement is a valid float
PVforecast.update_ac_power_measurement(
date_time=datetime.now(),
ac_power_measurement=float(ac_power_measurement),
) # Update measurement
# Get PV forecast and temperature forecast for the specified date range
pv_forecast = PVforecast.get_pv_forecast_for_date_range(date_now, date)
temperature_forecast = PVforecast.get_temperature_for_date_range(date_now, date)
# Return both forecasts as a JSON response
ret = {
"temperature": temperature_forecast.tolist(),
"pvpower": pv_forecast.tolist(),
}
return jsonify(ret)
@app.route("/optimize", methods=["POST"])
def flask_optimize():
if request.method == "POST":
from datetime import datetime
# Retrieve optimization parameters from the request JSON
parameter = request.json
# Check for required parameters
required_parameters = [
"preis_euro_pro_wh_akku",
"strompreis_euro_pro_wh",
"gesamtlast",
"pv_akku_cap",
"einspeiseverguetung_euro_pro_wh",
"pv_forecast",
"temperature_forecast",
"eauto_min_soc",
"eauto_cap",
"eauto_charge_efficiency",
"eauto_charge_power",
"eauto_soc",
"pv_soc",
"start_solution",
"haushaltsgeraet_dauer",
"haushaltsgeraet_wh",
]
# Identify any missing parameters
missing_params = [p for p in required_parameters if p not in parameter]
if missing_params:
return jsonify(
{"error": f"Missing parameter: {', '.join(missing_params)}"}
), 400 # Return error for missing parameters
# Optional min SoC PV Battery
if "min_soc_prozent" not in parameter:
parameter["min_soc_prozent"] = 0
# Perform optimization simulation
result = opt_class.optimierung_ems(parameter=parameter, start_hour=datetime.now().hour)
# print(result)
# convert to JSON (None accepted by dumps)
return NumpyEncoder.dumps(result)
@app.route("/visualization_results.pdf")
def get_pdf():
# Endpoint to serve the generated PDF with visualization results
output_path = config.working_dir / config.directories.output
if not output_path.is_dir():
raise SetupIncomplete(f"Output path does not exist: {output_path}.")
return send_from_directory(output_path, "visualization_results.pdf")
@app.route("/site-map")
def site_map():
# Function to generate a site map of valid routes in the application
def print_links(links):
content = "<h1>Valid routes</h1><ul>"
for link in links:
content += f"<li><a href='{link}'>{link}</a></li>"
content += "</ul>"
return content
# Check if the route has no empty parameters
def has_no_empty_params(rule):
defaults = rule.defaults if rule.defaults is not None else ()
arguments = rule.arguments if rule.arguments is not None else ()
return len(defaults) >= len(arguments)
# Collect all valid GET routes without empty parameters
links = []
for rule in app.url_map.iter_rules():
if "GET" in rule.methods and has_no_empty_params(rule):
url = url_for(rule.endpoint, **(rule.defaults or {}))
links.append(url)
return print_links(sorted(links)) # Return the sorted links as HTML
@app.route("/")
def root():
# Redirect the root URL to the site map
return redirect("/site-map", code=302)
if __name__ == "__main__":
try:
config.run_setup()
# Set host and port from environment variables or defaults
host = os.getenv("FLASK_RUN_HOST", "0.0.0.0")
port = os.getenv("FLASK_RUN_PORT", 8503)
app.run(debug=True, host=host, port=port) # Run the Flask application
except Exception as e:
print(
f"Could not bind to host {host}:{port}. Error: {e}"
) # Error handling for binding issues

View File

@ -50,7 +50,7 @@ def server(xprocess, tmp_path: Path):
# assure server to be installed
try:
subprocess.run(
[sys.executable, "-c", "import akkudoktoreosserver"],
[sys.executable, "-c", "import akkudoktoreos.server"],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
@ -65,11 +65,11 @@ def server(xprocess, tmp_path: Path):
)
# command to start server process
args = [sys.executable, "-m", "akkudoktoreosserver.flask_server"]
args = [sys.executable, "-m", "akkudoktoreos.server.fastapi_server"]
env = {EOS_DIR: f"{tmp_path}", **os.environ.copy()}
# startup pattern
pattern = "Debugger PIN:"
pattern = "Application startup complete."
# search the first 30 lines for the startup pattern, if not found
# a RuntimeError will be raised informing the user
max_read_lines = 30
@ -81,14 +81,14 @@ def server(xprocess, tmp_path: Path):
terminate_on_interrupt = True
# ensure process is running and return its logfile
logfile = xprocess.ensure("akkudoktoreosserver", Starter)
logfile = xprocess.ensure("eos", Starter)
# create url/port info to the server
url = "http://127.0.0.1:8503"
yield url
# clean up whole process tree afterwards
xprocess.getinfo("akkudoktoreosserver").terminate()
xprocess.getinfo("eos").terminate()
@pytest.fixture

25
tests/generate_openapi.py Normal file
View File

@ -0,0 +1,25 @@
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,6 +1,6 @@
import unittest
from akkudoktoreos.class_akku import PVAkku
from akkudoktoreos.class_akku import PVAkku, PVAkkuParameters
class TestPVAkku(unittest.TestCase):
@ -14,21 +14,25 @@ class TestPVAkku(unittest.TestCase):
def test_initial_state_of_charge(self):
akku = PVAkku(
self.kapazitaet_wh,
PVAkkuParameters(
kapazitaet_wh=self.kapazitaet_wh,
start_soc_prozent=50,
min_soc_prozent=self.min_soc_prozent,
max_soc_prozent=self.max_soc_prozent,
),
hours=1,
start_soc_prozent=50,
min_soc_prozent=self.min_soc_prozent,
max_soc_prozent=self.max_soc_prozent,
)
self.assertEqual(akku.ladezustand_in_prozent(), 50.0, "Initial SoC should be 50%")
def test_discharge_below_min_soc(self):
akku = PVAkku(
self.kapazitaet_wh,
PVAkkuParameters(
kapazitaet_wh=self.kapazitaet_wh,
start_soc_prozent=50,
min_soc_prozent=self.min_soc_prozent,
max_soc_prozent=self.max_soc_prozent,
),
hours=1,
start_soc_prozent=50,
min_soc_prozent=self.min_soc_prozent,
max_soc_prozent=self.max_soc_prozent,
)
akku.reset()
# Try to discharge more energy than available above min_soc
@ -43,11 +47,13 @@ class TestPVAkku(unittest.TestCase):
def test_charge_above_max_soc(self):
akku = PVAkku(
self.kapazitaet_wh,
PVAkkuParameters(
kapazitaet_wh=self.kapazitaet_wh,
start_soc_prozent=50,
min_soc_prozent=self.min_soc_prozent,
max_soc_prozent=self.max_soc_prozent,
),
hours=1,
start_soc_prozent=50,
min_soc_prozent=self.min_soc_prozent,
max_soc_prozent=self.max_soc_prozent,
)
akku.reset()
# Try to charge more energy than available up to max_soc
@ -62,11 +68,13 @@ class TestPVAkku(unittest.TestCase):
def test_charging_at_max_soc(self):
akku = PVAkku(
self.kapazitaet_wh,
PVAkkuParameters(
kapazitaet_wh=self.kapazitaet_wh,
start_soc_prozent=80,
min_soc_prozent=self.min_soc_prozent,
max_soc_prozent=self.max_soc_prozent,
),
hours=1,
start_soc_prozent=80,
min_soc_prozent=self.min_soc_prozent,
max_soc_prozent=self.max_soc_prozent,
)
akku.reset()
# Try to charge when SoC is already at max_soc
@ -80,11 +88,13 @@ class TestPVAkku(unittest.TestCase):
def test_discharging_at_min_soc(self):
akku = PVAkku(
self.kapazitaet_wh,
PVAkkuParameters(
kapazitaet_wh=self.kapazitaet_wh,
start_soc_prozent=20,
min_soc_prozent=self.min_soc_prozent,
max_soc_prozent=self.max_soc_prozent,
),
hours=1,
start_soc_prozent=20,
min_soc_prozent=self.min_soc_prozent,
max_soc_prozent=self.max_soc_prozent,
)
akku.reset()
# Try to discharge when SoC is already at min_soc
@ -99,11 +109,13 @@ class TestPVAkku(unittest.TestCase):
def test_soc_limits(self):
# Test to ensure that SoC never exceeds max_soc or drops below min_soc
akku = PVAkku(
self.kapazitaet_wh,
PVAkkuParameters(
kapazitaet_wh=self.kapazitaet_wh,
start_soc_prozent=50,
min_soc_prozent=self.min_soc_prozent,
max_soc_prozent=self.max_soc_prozent,
),
hours=1,
start_soc_prozent=50,
min_soc_prozent=self.min_soc_prozent,
max_soc_prozent=self.max_soc_prozent,
)
akku.reset()
akku.soc_wh = (

View File

@ -1,10 +1,16 @@
import numpy as np
import pytest
from akkudoktoreos.class_akku import PVAkku
from akkudoktoreos.class_ems import EnergieManagementSystem
from akkudoktoreos.class_haushaltsgeraet import Haushaltsgeraet
from akkudoktoreos.class_inverter import Wechselrichter
from akkudoktoreos.class_akku import EAutoParameters, PVAkku, PVAkkuParameters
from akkudoktoreos.class_ems import (
EnergieManagementSystem,
EnergieManagementSystemParameters,
)
from akkudoktoreos.class_haushaltsgeraet import (
Haushaltsgeraet,
HaushaltsgeraetParameters,
)
from akkudoktoreos.class_inverter import Wechselrichter, WechselrichterParameters
from akkudoktoreos.config import AppConfig
prediction_hours = 48
@ -17,21 +23,30 @@ start_hour = 1
def create_ems_instance(tmp_config: AppConfig) -> EnergieManagementSystem:
"""Fixture to create an EnergieManagementSystem instance with given test parameters."""
# Initialize the battery and the inverter
akku = PVAkku(kapazitaet_wh=5000, start_soc_prozent=80, hours=48, min_soc_prozent=10)
akku = PVAkku(
PVAkkuParameters(kapazitaet_wh=5000, start_soc_prozent=80, min_soc_prozent=10),
hours=prediction_hours,
)
akku.reset()
wechselrichter = Wechselrichter(10000, akku)
wechselrichter = Wechselrichter(WechselrichterParameters(max_leistung_wh=10000), akku)
# Household device (currently not used, set to None)
home_appliance = Haushaltsgeraet(
HaushaltsgeraetParameters(
verbrauch_wh=2000,
dauer_h=2,
),
hours=prediction_hours,
verbrauch_wh=2000,
dauer_h=2,
)
home_appliance.set_startzeitpunkt(2)
# Example initialization of electric car battery
eauto = PVAkku(kapazitaet_wh=26400, start_soc_prozent=10, hours=48, min_soc_prozent=10)
eauto.set_charge_per_hour(np.full(48, 1))
eauto = PVAkku(
EAutoParameters(kapazitaet_wh=26400, start_soc_prozent=10, min_soc_prozent=10),
hours=prediction_hours,
)
eauto.set_charge_per_hour(np.full(prediction_hours, 1))
# Parameters based on previous example data
pv_prognose_wh = [
0,
@ -135,7 +150,8 @@ def create_ems_instance(tmp_config: AppConfig) -> EnergieManagementSystem:
0.0002780,
]
einspeiseverguetung_euro_pro_wh = [0.00007] * len(strompreis_euro_pro_wh)
einspeiseverguetung_euro_pro_wh = 0.00007
preis_euro_pro_wh_akku = 0.0001
gesamtlast = [
676.71,
@ -190,12 +206,15 @@ def create_ems_instance(tmp_config: AppConfig) -> EnergieManagementSystem:
# Initialize the energy management system with the respective parameters
ems = EnergieManagementSystem(
config=tmp_config.eos,
pv_prognose_wh=pv_prognose_wh,
strompreis_euro_pro_wh=strompreis_euro_pro_wh,
einspeiseverguetung_euro_pro_wh=einspeiseverguetung_euro_pro_wh,
tmp_config.eos,
EnergieManagementSystemParameters(
pv_prognose_wh=pv_prognose_wh,
strompreis_euro_pro_wh=strompreis_euro_pro_wh,
einspeiseverguetung_euro_pro_wh=einspeiseverguetung_euro_pro_wh,
preis_euro_pro_wh_akku=preis_euro_pro_wh_akku,
gesamtlast=gesamtlast,
),
eauto=eauto,
gesamtlast=gesamtlast,
haushaltsgeraet=home_appliance,
wechselrichter=wechselrichter,
)
@ -249,7 +268,7 @@ def test_simulation(create_ems_instance):
"akku_soc_pro_stunde",
"Einnahmen_Euro_pro_Stunde",
"Gesamtbilanz_Euro",
"E-Auto_SoC_pro_Stunde",
"EAuto_SoC_pro_Stunde",
"Gesamteinnahmen_Euro",
"Gesamtkosten_Euro",
"Verluste_Pro_Stunde",

View File

@ -1,10 +1,16 @@
import numpy as np
import pytest
from akkudoktoreos.class_akku import PVAkku
from akkudoktoreos.class_ems import EnergieManagementSystem
from akkudoktoreos.class_haushaltsgeraet import Haushaltsgeraet
from akkudoktoreos.class_inverter import Wechselrichter
from akkudoktoreos.class_akku import EAutoParameters, PVAkku, PVAkkuParameters
from akkudoktoreos.class_ems import (
EnergieManagementSystem,
EnergieManagementSystemParameters,
)
from akkudoktoreos.class_haushaltsgeraet import (
Haushaltsgeraet,
HaushaltsgeraetParameters,
)
from akkudoktoreos.class_inverter import Wechselrichter, WechselrichterParameters
from akkudoktoreos.config import AppConfig
prediction_hours = 48
@ -17,20 +23,28 @@ start_hour = 0
def create_ems_instance(tmp_config: AppConfig) -> EnergieManagementSystem:
"""Fixture to create an EnergieManagementSystem instance with given test parameters."""
# Initialize the battery and the inverter
akku = PVAkku(kapazitaet_wh=5000, start_soc_prozent=80, hours=48, min_soc_prozent=10)
akku = PVAkku(
PVAkkuParameters(kapazitaet_wh=5000, start_soc_prozent=80, min_soc_prozent=10),
hours=prediction_hours,
)
akku.reset()
wechselrichter = Wechselrichter(10000, akku)
wechselrichter = Wechselrichter(WechselrichterParameters(max_leistung_wh=10000), akku)
# Household device (currently not used, set to None)
home_appliance = Haushaltsgeraet(
HaushaltsgeraetParameters(
verbrauch_wh=2000,
dauer_h=2,
),
hours=prediction_hours,
verbrauch_wh=2000,
dauer_h=2,
)
home_appliance.set_startzeitpunkt(2)
# Example initialization of electric car battery
eauto = PVAkku(kapazitaet_wh=26400, start_soc_prozent=100, hours=48, min_soc_prozent=100)
eauto = PVAkku(
EAutoParameters(kapazitaet_wh=26400, start_soc_prozent=100, min_soc_prozent=100),
hours=prediction_hours,
)
# Parameters based on previous example data
pv_prognose_wh = np.full(prediction_hours, 0)
@ -97,12 +111,15 @@ def create_ems_instance(tmp_config: AppConfig) -> EnergieManagementSystem:
# Initialize the energy management system with the respective parameters
ems = EnergieManagementSystem(
config=tmp_config.eos,
pv_prognose_wh=pv_prognose_wh,
strompreis_euro_pro_wh=strompreis_euro_pro_wh,
einspeiseverguetung_euro_pro_wh=einspeiseverguetung_euro_pro_wh,
tmp_config.eos,
EnergieManagementSystemParameters(
pv_prognose_wh=pv_prognose_wh,
strompreis_euro_pro_wh=strompreis_euro_pro_wh,
einspeiseverguetung_euro_pro_wh=einspeiseverguetung_euro_pro_wh,
preis_euro_pro_wh_akku=0,
gesamtlast=gesamtlast,
),
eauto=eauto,
gesamtlast=gesamtlast,
haushaltsgeraet=home_appliance,
wechselrichter=wechselrichter,
)
@ -163,7 +180,7 @@ def test_simulation(create_ems_instance):
"akku_soc_pro_stunde",
"Einnahmen_Euro_pro_Stunde",
"Gesamtbilanz_Euro",
"E-Auto_SoC_pro_Stunde",
"EAuto_SoC_pro_Stunde",
"Gesamteinnahmen_Euro",
"Gesamtkosten_Euro",
"Verluste_Pro_Stunde",

View File

@ -5,7 +5,11 @@ from unittest.mock import patch
import pytest
from akkudoktoreos.class_optimize import optimization_problem
from akkudoktoreos.class_optimize import (
OptimizationParameters,
OptimizeResponse,
optimization_problem,
)
from akkudoktoreos.config import AppConfig
DIR_TESTDATA = Path(__file__).parent / "testdata"
@ -46,7 +50,7 @@ def test_optimize(
# Load input and output data
file = DIR_TESTDATA / fn_in
with file.open("r") as f_in:
input_data = json.load(f_in)
input_data = OptimizationParameters(**json.load(f_in))
file = DIR_TESTDATA / fn_out
with file.open("r") as f_out:
@ -59,7 +63,7 @@ def test_optimize(
pytest.skip()
# Call the optimization function
ergebnis = opt_class.optimierung_ems(parameter=input_data, start_hour=start_hour, ngen=ngen)
ergebnis = opt_class.optimierung_ems(parameters=input_data, start_hour=start_hour, ngen=ngen)
# with open(f"new_{fn_out}", "w") as f_out:
# from akkudoktoreos.class_numpy_encoder import NumpyEncoder
# json_data_str = NumpyEncoder.dumps(ergebnis)
@ -72,3 +76,5 @@ def test_optimize(
# The function creates a visualization result PDF as a side-effect.
visualisiere_ergebnisse_patch.assert_called_once()
OptimizeResponse(**ergebnis)

19
tests/test_openapi.py Normal file
View File

@ -0,0 +1,19 @@
import json
from pathlib import Path
from generate_openapi import generate_openapi
DIR_PROJECT_ROOT = Path(__file__).parent.parent
DIR_TESTDATA = Path(__file__).parent / "testdata"
def test_openapi_spec_current():
"""Verify the openapi spec hasn´t changed."""
old_spec_path = DIR_PROJECT_ROOT / "docs" / "akkudoktoreos" / "openapi.json"
new_spec_path = DIR_TESTDATA / "openapi-new.json"
generate_openapi(new_spec_path)
with open(new_spec_path) as f_new:
new_spec = json.load(f_new)
with open(old_spec_path) as f_old:
old_spec = json.load(f_old)
assert new_spec == old_spec

View File

@ -1,52 +1,58 @@
{
"preis_euro_pro_wh_akku": 0.0001,
"pv_soc": 80,
"pv_akku_cap": 26400,
"year_energy": 4100000,
"einspeiseverguetung_euro_pro_wh": 0.00007,
"max_heizleistung": 1000,
"gesamtlast": [
676.71, 876.19, 527.13, 468.88, 531.38, 517.95, 483.15, 472.28, 1011.68, 995.00,
1053.07, 1063.91, 1320.56, 1132.03, 1163.67, 1176.82, 1216.22, 1103.78, 1129.12,
1178.71, 1050.98, 988.56, 912.38, 704.61, 516.37, 868.05, 694.34, 608.79, 556.31,
488.89, 506.91, 804.89, 1141.98, 1056.97, 992.46, 1155.99, 827.01, 1257.98, 1232.67,
871.26, 860.88, 1158.03, 1222.72, 1221.04, 949.99, 987.01, 733.99, 592.97
],
"pv_forecast": [
0, 0, 0, 0, 0, 0, 0, 8.05, 352.91, 728.51, 930.28, 1043.25, 1106.74, 1161.69,
6018.82, 5519.07, 3969.88, 3017.96, 1943.07, 1007.17, 319.67, 7.88, 0, 0, 0, 0,
0, 0, 0, 0, 0, 5.04, 335.59, 705.32, 1121.12, 1604.79, 2157.38, 1433.25, 5718.49,
4553.96, 3027.55, 2574.46, 1720.4, 963.4, 383.3, 0, 0, 0
],
"ems": {
"preis_euro_pro_wh_akku": 0.0001,
"einspeiseverguetung_euro_pro_wh": 0.00007,
"gesamtlast": [
676.71, 876.19, 527.13, 468.88, 531.38, 517.95, 483.15, 472.28, 1011.68, 995.00,
1053.07, 1063.91, 1320.56, 1132.03, 1163.67, 1176.82, 1216.22, 1103.78, 1129.12,
1178.71, 1050.98, 988.56, 912.38, 704.61, 516.37, 868.05, 694.34, 608.79, 556.31,
488.89, 506.91, 804.89, 1141.98, 1056.97, 992.46, 1155.99, 827.01, 1257.98, 1232.67,
871.26, 860.88, 1158.03, 1222.72, 1221.04, 949.99, 987.01, 733.99, 592.97
],
"pv_prognose_wh": [
0, 0, 0, 0, 0, 0, 0, 8.05, 352.91, 728.51, 930.28, 1043.25, 1106.74, 1161.69,
6018.82, 5519.07, 3969.88, 3017.96, 1943.07, 1007.17, 319.67, 7.88, 0, 0, 0, 0,
0, 0, 0, 0, 0, 5.04, 335.59, 705.32, 1121.12, 1604.79, 2157.38, 1433.25, 5718.49,
4553.96, 3027.55, 2574.46, 1720.4, 963.4, 383.3, 0, 0, 0
],
"strompreis_euro_pro_wh": [
0.0003384, 0.0003318, 0.0003284, 0.0003283, 0.0003289, 0.0003334, 0.0003290,
0.0003302, 0.0003042, 0.0002430, 0.0002280, 0.0002212, 0.0002093, 0.0001879,
0.0001838, 0.0002004, 0.0002198, 0.0002270, 0.0002997, 0.0003195, 0.0003081,
0.0002969, 0.0002921, 0.0002780, 0.0003384, 0.0003318, 0.0003284, 0.0003283,
0.0003289, 0.0003334, 0.0003290, 0.0003302, 0.0003042, 0.0002430, 0.0002280,
0.0002212, 0.0002093, 0.0001879, 0.0001838, 0.0002004, 0.0002198, 0.0002270,
0.0002997, 0.0003195, 0.0003081, 0.0002969, 0.0002921, 0.0002780
]
},
"pv_akku": {
"kapazitaet_wh": 26400,
"max_ladeleistung_w": 5000,
"start_soc_prozent": 80,
"min_soc_prozent": 15
},
"wechselrichter": {
"max_leistung_wh": 10000
},
"eauto": {
"kapazitaet_wh": 60000,
"lade_effizienz": 0.95,
"entlade_effizienz": 1.0,
"max_ladeleistung_w": 11040,
"start_soc_prozent": 54,
"min_soc_prozent": 0
},
"temperature_forecast": [
18.3, 17.8, 16.9, 16.2, 15.6, 15.1, 14.6, 14.2, 14.3, 14.8, 15.7, 16.7, 17.4,
18.0, 18.6, 19.2, 19.1, 18.7, 18.5, 17.7, 16.2, 14.6, 13.6, 13.0, 12.6, 12.2,
11.7, 11.6, 11.3, 11.0, 10.7, 10.2, 11.4, 14.4, 16.4, 18.3, 19.5, 20.7, 21.9,
22.7, 23.1, 23.1, 22.8, 21.8, 20.2, 19.1, 18.0, 17.4
],
"strompreis_euro_pro_wh": [
0.0003384, 0.0003318, 0.0003284, 0.0003283, 0.0003289, 0.0003334, 0.0003290,
0.0003302, 0.0003042, 0.0002430, 0.0002280, 0.0002212, 0.0002093, 0.0001879,
0.0001838, 0.0002004, 0.0002198, 0.0002270, 0.0002997, 0.0003195, 0.0003081,
0.0002969, 0.0002921, 0.0002780, 0.0003384, 0.0003318, 0.0003284, 0.0003283,
0.0003289, 0.0003334, 0.0003290, 0.0003302, 0.0003042, 0.0002430, 0.0002280,
0.0002212, 0.0002093, 0.0001879, 0.0001838, 0.0002004, 0.0002198, 0.0002270,
0.0002997, 0.0003195, 0.0003081, 0.0002969, 0.0002921, 0.0002780
],
"eauto_min_soc": 0,
"eauto_cap": 60000,
"eauto_charge_efficiency": 0.95,
"eauto_charge_power": 11040,
"eauto_soc": 54,
"pvpowernow": 211.137503624,
"start_solution": [
1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0,
1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
],
"haushaltsgeraet_wh": 937,
"haushaltsgeraet_dauer": 0,
"min_soc_prozent": 15
]
}

View File

@ -1,47 +1,51 @@
{
"preis_euro_pro_wh_akku": 0.0001,
"pv_soc": 80,
"pv_akku_cap": 26400,
"year_energy": 4100000,
"einspeiseverguetung_euro_pro_wh": 0.00007,
"max_heizleistung": 1000,
"gesamtlast": [
676.71, 876.19, 527.13, 468.88, 531.38, 517.95, 483.15, 472.28, 1011.68, 995.00,
1053.07, 1063.91, 1320.56, 1132.03, 1163.67, 1176.82, 1216.22, 1103.78, 1129.12,
1178.71, 1050.98, 988.56, 912.38, 704.61, 516.37, 868.05, 694.34, 608.79, 556.31,
488.89, 506.91, 804.89, 1141.98, 1056.97, 992.46, 1155.99, 827.01, 1257.98, 1232.67,
871.26, 860.88, 1158.03, 1222.72, 1221.04, 949.99, 987.01, 733.99, 592.97
],
"pv_forecast": [
0, 0, 0, 0, 0, 0, 0, 8.05, 352.91, 728.51, 930.28, 1043.25, 1106.74, 1161.69,
6018.82, 5519.07, 3969.88, 3017.96, 1943.07, 1007.17, 319.67, 7.88, 0, 0, 0, 0,
0, 0, 0, 0, 0, 5.04, 335.59, 705.32, 1121.12, 1604.79, 2157.38, 1433.25, 5718.49,
4553.96, 3027.55, 2574.46, 1720.4, 963.4, 383.3, 0, 0, 0
],
"ems": {
"preis_euro_pro_wh_akku": 0.0001,
"einspeiseverguetung_euro_pro_wh": 0.00007,
"gesamtlast": [
676.71, 876.19, 527.13, 468.88, 531.38, 517.95, 483.15, 472.28, 1011.68, 995.00,
1053.07, 1063.91, 1320.56, 1132.03, 1163.67, 1176.82, 1216.22, 1103.78, 1129.12,
1178.71, 1050.98, 988.56, 912.38, 704.61, 516.37, 868.05, 694.34, 608.79, 556.31,
488.89, 506.91, 804.89, 1141.98, 1056.97, 992.46, 1155.99, 827.01, 1257.98, 1232.67,
871.26, 860.88, 1158.03, 1222.72, 1221.04, 949.99, 987.01, 733.99, 592.97
],
"pv_prognose_wh": [
0, 0, 0, 0, 0, 0, 0, 8.05, 352.91, 728.51, 930.28, 1043.25, 1106.74, 1161.69,
6018.82, 5519.07, 3969.88, 3017.96, 1943.07, 1007.17, 319.67, 7.88, 0, 0, 0, 0,
0, 0, 0, 0, 0, 5.04, 335.59, 705.32, 1121.12, 1604.79, 2157.38, 1433.25, 5718.49,
4553.96, 3027.55, 2574.46, 1720.4, 963.4, 383.3, 0, 0, 0
],
"strompreis_euro_pro_wh": [
0.0003384, 0.0003318, 0.0003284, 0.0003283, 0.0003289, 0.0003334, 0.0003290,
0.0003302, 0.0003042, 0.0002430, 0.0002280, 0.0002212, 0.0002093, 0.0001879,
0.0001838, 0.0002004, 0.0002198, 0.0002270, 0.0002997, 0.0003195, 0.0003081,
0.0002969, 0.0002921, 0.0002780, 0.0003384, 0.0003318, 0.0003284, 0.0003283,
0.0003289, 0.0003334, 0.0003290, 0.0003302, 0.0003042, 0.0002430, 0.0002280,
0.0002212, 0.0002093, 0.0001879, 0.0001838, 0.0002004, 0.0002198, 0.0002270,
0.0002997, 0.0003195, 0.0003081, 0.0002969, 0.0002921, 0.0002780
]
},
"pv_akku": {
"kapazitaet_wh": 26400,
"start_soc_prozent": 80,
"min_soc_prozent": 15
},
"eauto": {
"kapazitaet_wh": 60000,
"lade_effizienz": 0.95,
"max_ladeleistung_w": 11040,
"start_soc_prozent": 5,
"min_soc_prozent": 80
},
"spuelmaschine" :{
"verbrauch_wh": 5000,
"dauer_h": 2
},
"temperature_forecast": [
18.3, 17.8, 16.9, 16.2, 15.6, 15.1, 14.6, 14.2, 14.3, 14.8, 15.7, 16.7, 17.4,
18.0, 18.6, 19.2, 19.1, 18.7, 18.5, 17.7, 16.2, 14.6, 13.6, 13.0, 12.6, 12.2,
11.7, 11.6, 11.3, 11.0, 10.7, 10.2, 11.4, 14.4, 16.4, 18.3, 19.5, 20.7, 21.9,
22.7, 23.1, 23.1, 22.8, 21.8, 20.2, 19.1, 18.0, 17.4
],
"strompreis_euro_pro_wh": [
0.0003384, 0.0003318, 0.0003284, 0.0003283, 0.0003289, 0.0003334, 0.0003290,
0.0003302, 0.0003042, 0.0002430, 0.0002280, 0.0002212, 0.0002093, 0.0001879,
0.0001838, 0.0002004, 0.0002198, 0.0002270, 0.0002997, 0.0003195, 0.0003081,
0.0002969, 0.0002921, 0.0002780, 0.0003384, 0.0003318, 0.0003284, 0.0003283,
0.0003289, 0.0003334, 0.0003290, 0.0003302, 0.0003042, 0.0002430, 0.0002280,
0.0002212, 0.0002093, 0.0001879, 0.0001838, 0.0002004, 0.0002198, 0.0002270,
0.0002997, 0.0003195, 0.0003081, 0.0002969, 0.0002921, 0.0002780
],
"eauto_min_soc": 80,
"eauto_cap": 60000,
"eauto_charge_efficiency": 0.95,
"eauto_charge_power": 11040,
"eauto_soc": 5,
"pvpowernow": 211.137503624,
"start_solution": null,
"haushaltsgeraet_wh": 5000,
"haushaltsgeraet_dauer": 2,
"min_soc_prozent": 15
}
"start_solution": null
}

View File

@ -392,7 +392,7 @@
0.0
],
"Gesamtbilanz_Euro": 1.3505190567851246,
"E-Auto_SoC_pro_Stunde": [
"EAuto_SoC_pro_Stunde": [
54.0,
54.0,
54.0,
@ -727,371 +727,5 @@
1,
1
],
"spuelstart": null,
"simulation_data": {
"Last_Wh_pro_Stunde": [
1053.07,
1063.91,
1320.56,
1132.03,
1163.67,
1176.82,
1216.22,
1103.78,
1129.12,
1178.71,
1050.98,
988.56,
912.38,
704.61,
516.37,
868.05,
694.34,
608.79,
556.31,
488.89,
506.91,
804.89,
1141.98,
1056.97,
992.46,
1155.99,
827.01,
1257.98,
1232.67,
871.26,
860.88,
1158.03,
1222.72,
1221.04,
949.99,
987.01,
733.99,
592.97
],
"Netzeinspeisung_Wh_pro_Stunde": [
0.0,
0.0,
0.0,
0.0,
0.0,
2792.3879958677676,
2753.66,
1914.18,
813.95,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
2738.2769214876007,
3682.7,
2166.67,
1416.43,
497.68000000000006,
0.0,
0.0,
0.0,
0.0,
0.0
],
"Netzbezug_Wh_pro_Stunde": [
0.0,
20.660000000000082,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
171.54000000000008,
0.0,
980.68,
0.0,
704.61,
0.0,
868.05,
694.34,
608.79,
556.31,
488.89,
506.91,
799.85,
0.0,
351.65,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
257.64,
0.0,
987.01,
0.0,
592.97
],
"Kosten_Euro_pro_Stunde": [
0.0,
0.004569992000000018,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.05480703000000003,
0.0,
0.291163892,
0.0,
0.19588158,
0.0,
0.28801899,
0.22802125600000003,
0.199865757,
0.182970359,
0.162995926,
0.16677339,
0.26411047,
0.0,
0.08545095,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.08231598,
0.0,
0.293043269,
0.0,
0.16484566
],
"akku_soc_pro_stunde": [
79.4714617768595,
79.4714617768595,
78.55109331955923,
78.6499599862259,
94.83379331955922,
100.0,
100.0,
100.0,
100.0,
100.0,
96.85214359504131,
96.85214359504131,
92.92488808539943,
92.92488808539943,
90.70222107438015,
90.70222107438015,
90.70222107438015,
90.70222107438015,
90.70222107438015,
90.70222107438015,
90.70222107438015,
90.70222107438015,
87.231189738292,
87.231189738292,
87.66005640495867,
89.15605640495866,
93.59062307162533,
94.17485640495867,
100.0,
100.0,
100.0,
100.0,
100.0,
100.0,
97.56073519283747,
97.56073519283747,
94.40134297520663,
94.40134297520663
],
"Einnahmen_Euro_pro_Stunde": [
0.0,
0.0,
0.0,
0.0,
0.0,
0.19546715971074372,
0.19275619999999996,
0.1339926,
0.0569765,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.19167938450413202,
0.257789,
0.1516669,
0.09915009999999999,
0.0348376,
0.0,
0.0,
0.0,
0.0,
0.0
],
"Gesamtbilanz_Euro": 1.3505190567851246,
"E-Auto_SoC_pro_Stunde": [
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0,
54.0
],
"Gesamteinnahmen_Euro": 1.3143154442148755,
"Gesamtkosten_Euro": 2.664834501,
"Verluste_Pro_Stunde": [
16.744090909090914,
0.0,
29.157272727272726,
3.5592000000000112,
582.6179999999995,
185.98344049586785,
0.0,
0.0,
0.0,
0.0,
99.72409090909093,
0.0,
124.41545454545451,
0.0,
70.41409090909087,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
109.96227272727276,
0.0,
15.439199999999985,
53.855999999999995,
159.6443999999999,
21.032399999999996,
209.70516942148788,
0.0,
0.0,
0.0,
0.0,
0.0,
77.27590909090907,
0.0,
100.08954545454549,
0.0
],
"Gesamt_Verluste": 1859.6205371900821,
"Haushaltsgeraet_wh_pro_stunde": [
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null
]
}
"spuelstart": null
}

View File

@ -31,7 +31,7 @@
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0569765, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.009006199999999989, 0.03141599999999999, 0.0931259, 0.012268899999999998, 0.31400739999999994, 0.257789, 0.1516669, 0.09915009999999999, 0.0348376, 0.0, 0.0, 0.0, 0.0, 0.0
],
"Gesamtbilanz_Euro": 13.525878719636365,
"E-Auto_SoC_pro_Stunde": [
"EAuto_SoC_pro_Stunde": [
13.74, 20.294999999999998, 37.775, 53.06999999999999, 70.55, 81.475, 90.215, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0
],
"Gesamteinnahmen_Euro": 1.0602444999999998,
@ -62,38 +62,5 @@
"start_solution": [
0, 0, 4, 3, 1, 3, 1, 5, 6, 4, 1, 3, 6, 4, 6, 0, 6, 6, 0, 0, 3, 4, 1, 3, 2, 4, 4, 2, 3, 2, 1, 0, 2, 4, 1, 0, 6, 6, 2, 0, 4, 3, 3, 2, 6, 4, 1, 1, 0, 1, 5, 1, 2, 4, 6, 6, 5, 0, 2, 1, 6, 5, 6, 3, 2, 6, 6, 1, 1, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13
],
"spuelstart": 13,
"simulation_data": {
"Last_Wh_pro_Stunde": [
6297.07, 6756.91, 16208.56, 15449.029999999999, 16313.488181818184, 7731.82, 6460.22, 6974.78, 1129.12, 1178.71, 1050.98, 988.56, 912.38, 1741.405454545456, 516.37, 868.05, 694.34, 608.79, 556.31, 488.89, 506.91, 804.89, 1718.014090909092, 1056.97, 992.46, 1155.99, 827.01, 1257.98, 1232.67, 871.26, 860.88, 1158.03, 1222.72, 1221.04, 949.99, 987.01, 733.99, 592.97
],
"Netzeinspeisung_Wh_pro_Stunde": [
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 813.95, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 128.65999999999985, 448.79999999999995, 1330.3700000000001, 175.26999999999998, 4485.82, 3682.7, 2166.67, 1416.43, 497.68000000000006, 0.0, 0.0, 0.0, 0.0, 0.0
],
"Netzbezug_Wh_pro_Stunde": [
366.78999999999996, 5713.66, 15101.82, 14287.339999999998, 10294.668181818184, 2212.75, 2490.34, 3956.8199999999997, 0.0, 171.54000000000008, 731.31, 980.68, 0.0, 1741.405454545456, 516.37, 868.05, 694.34, 608.79, 556.31, 488.89, 0.0, 799.85, 1382.424090909092, 351.65, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 257.64, 566.69, 987.01, 0.0, 0.0
],
"Kosten_Euro_pro_Stunde": [
0.08362812, 1.263861592, 3.160810926, 2.6845911859999996, 1.8921600118181823, 0.44343509999999997, 0.547376732, 0.8981981399999999, 0.0, 0.05480703000000003, 0.225316611, 0.291163892, 0.0, 0.48411071636363673, 0.174739608, 0.28801899, 0.22802125600000003, 0.199865757, 0.182970359, 0.162995926, 0.0, 0.26411047, 0.42053340845454584, 0.08545095, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.08231598, 0.174597189, 0.293043269, 0.0, 0.0
],
"akku_soc_pro_stunde": [
58.47796143250689, 65.14462809917354, 81.81129476584022, 91.81129476584022, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 96.07274449035812, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 97.8180526859504, 97.8180526859504, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 96.84060778236915, 94.28822314049587
],
"Einnahmen_Euro_pro_Stunde": [
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0569765, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.009006199999999989, 0.03141599999999999, 0.0931259, 0.012268899999999998, 0.31400739999999994, 0.257789, 0.1516669, 0.09915009999999999, 0.0348376, 0.0, 0.0, 0.0, 0.0, 0.0
],
"Gesamtbilanz_Euro": 13.525878719636365,
"E-Auto_SoC_pro_Stunde": [
13.74, 20.294999999999998, 37.775, 53.06999999999999, 70.55, 81.475, 90.215, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0
],
"Gesamteinnahmen_Euro": 1.0602444999999998,
"Gesamtkosten_Euro": 14.586123219636365,
"Verluste_Pro_Stunde": [
957.818181818182, 447.0, 1152.0, 843.0, 846.7933884297522, 345.0, 276.0, 309.0, 0.0, 0.0, 0.0, 0.0, 124.41545454545451, 141.38119834710756, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 69.12409090909085, 0.0, 78.55010330578523, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 100.08954545454549, 80.85954545454547
],
"Gesamt_Verluste": 5771.031508264463,
"Haushaltsgeraet_wh_pro_stunde": [
0.0, 0.0, 0.0, 2500.0, 2500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
]
}
"spuelstart": 13
}

View File

@ -31,7 +31,7 @@
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.013086500000000003, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.257789, 0.1516669, 0.09915009999999999, 0.0348376, 0.0, 0.0, 0.0, 0.0, 0.0
],
"Gesamtbilanz_Euro": 11.315688587156364,
"E-Auto_SoC_pro_Stunde": [
"EAuto_SoC_pro_Stunde": [
13.74, 13.74, 31.22, 48.699999999999996, 66.18, 81.475, 90.215, 98.955, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0
],
"Gesamteinnahmen_Euro": 0.5565300999999999,
@ -62,38 +62,5 @@
"start_solution": [
3, 2, 6, 3, 5, 4, 1, 5, 4, 6, 1, 1, 2, 6, 6, 6, 6, 6, 1, 2, 2, 6, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 6, 2, 2, 2, 6, 1, 1, 1, 1, 1, 0, 5, 3, 1, 1, 2, 0, 1, 0, 6, 2, 0, 6, 6, 6, 5, 2, 2, 3, 6, 1, 5, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13
],
"spuelstart": 13,
"simulation_data": {
"Last_Wh_pro_Stunde": [
6297.07, 1063.91, 12688.56, 18520.03, 18551.67, 11659.115454545456, 6460.22, 6347.78, 1756.12, 1178.71, 1050.98, 988.56, 912.38, 704.61, 516.37, 868.05, 694.34, 608.79, 556.31, 488.89, 506.91, 804.89, 1141.98, 1056.97, 992.46, 1155.99, 827.01, 1257.98, 2494.6908545454626, 871.26, 860.88, 1158.03, 1222.72, 1221.04, 949.99, 987.01, 733.99, 592.97
],
"Netzeinspeisung_Wh_pro_Stunde": [
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 186.95000000000005, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3682.7, 2166.67, 1416.43, 497.68000000000006, 0.0, 0.0, 0.0, 0.0, 0.0
],
"Netzbezug_Wh_pro_Stunde": [
366.78999999999996, 0.0, 11581.82, 17358.339999999997, 12532.85, 6140.045454545456, 2490.34, 3329.8199999999997, 0.0, 171.54000000000008, 731.31, 980.68, 912.38, 704.61, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1262.0208545454625, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
],
"Kosten_Euro_pro_Stunde": [
0.08362812, 0.0, 2.424074926, 3.261632085999999, 2.30353783, 1.2304651090909093, 0.547376732, 0.7558691399999999, 0.0, 0.05480703000000003, 0.225316611, 0.291163892, 0.26650619799999997, 0.19588158, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.231959433065456, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
],
"akku_soc_pro_stunde": [
58.47796143250689, 58.38903236914601, 61.722365702479344, 78.38903236914601, 95.05569903581267, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 97.77733298898072, 94.04089187327823, 91.05216942148759, 88.43168904958677, 86.03710399449034, 83.93272210743798, 81.75077479338839, 78.30789428374652, 74.83686294765836, 73.32321797520657, 73.75208464187324, 75.24808464187323, 79.6826513085399, 80.26688464187325, 100.0, 100.0, 100.0, 100.0, 100.0, 98.89101239669421, 96.45174758953168, 92.20325413223141, 89.04386191460057, 86.4914772727273
],
"Einnahmen_Euro_pro_Stunde": [
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.013086500000000003, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.257789, 0.1516669, 0.09915009999999999, 0.0348376, 0.0, 0.0, 0.0, 0.0, 0.0
],
"Gesamtbilanz_Euro": 11.315688587156364,
"E-Auto_SoC_pro_Stunde": [
13.74, 13.74, 31.22, 48.699999999999996, 66.18, 81.475, 90.215, 98.955, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0
],
"Gesamteinnahmen_Euro": 0.5565300999999999,
"Gesamtkosten_Euro": 11.872218687156364,
"Verluste_Pro_Stunde": [
957.818181818182, 2.817272727272737, 672.0, 1152.0, 1152.0, 660.994834710744, 276.0, 276.0, 33.0, 0.0, 0.0, 0.0, 0.0, 0.0, 70.41409090909087, 118.37045454545455, 94.68272727272722, 83.01681818181817, 75.86045454545456, 66.66681818181814, 69.12409090909085, 109.0704545454546, 109.96227272727276, 47.952272727272714, 15.439199999999985, 53.855999999999995, 159.6443999999999, 21.032399999999996, 710.3921528925632, 0.0, 0.0, 0.0, 0.0, 35.132727272727266, 77.27590909090907, 134.59227272727276, 100.08954545454549, 80.85954545454547
],
"Gesamt_Verluste": 7416.064896694217,
"Haushaltsgeraet_wh_pro_stunde": [
0.0, 0.0, 0.0, 2500.0, 2500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
]
}
"spuelstart": 13
}

View File

@ -72,7 +72,7 @@
0.0, 0.0, 0.0, 0.0, 0.0
],
"Gesamtbilanz_Euro": 27.732796636363638,
"E-Auto_SoC_pro_Stunde": [
"EAuto_SoC_pro_Stunde": [
30.294999999999998, 43.405, 60.885, 78.365, 93.66, 93.66, 100.0, 100.0,
100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0,
100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0,