mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-04-19 08:55:15 +00:00
Json configuration (#141)
* Add json config * Adjust code to new config --------- Co-authored-by: Chris <git@nootch.de>
This commit is contained in:
parent
fc74cde56f
commit
ed3226e522
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
|||||||
cache/
|
cache/
|
||||||
output/
|
output/
|
||||||
|
EOS.config.json
|
||||||
|
|
||||||
# Default ignore folders and files for VS Code, Python
|
# Default ignore folders and files for VS Code, Python
|
||||||
|
|
||||||
|
25
README.md
25
README.md
@ -9,10 +9,29 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Good installation guide:
|
Good installation guide:
|
||||||
https://meintechblog.de/2024/09/05/andreas-schmitz-joerg-installiert-mein-energieoptimierungssystem/
|
<https://meintechblog.de/2024/09/05/andreas-schmitz-joerg-installiert-mein-energieoptimierungssystem/>
|
||||||
|
|
||||||
The project requires Python 3.10 or newer.
|
The project requires Python 3.10 or newer.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
This project uses a `config.json` file to manage configuration settings.
|
||||||
|
|
||||||
|
### Default Configuration
|
||||||
|
|
||||||
|
A default configuration file `default.config.json` is provided. This file contains all the necessary configuration keys with their default values.
|
||||||
|
|
||||||
|
### Custom Configuration
|
||||||
|
|
||||||
|
Users can specify a custom configuration directory by setting the environment variable `EOS_DIR`.
|
||||||
|
|
||||||
|
- If the directory specified by `EOS_DIR` contains an existing `config.json` file, the application will use this configuration file.
|
||||||
|
- If the `config.json` file does not exist in the specified directory, the `default.config.json` file will be copied to the directory as `config.json`.
|
||||||
|
|
||||||
|
### Configuration Updates
|
||||||
|
|
||||||
|
If the configuration keys in the `config.json` file are missing or different from those in `default.config.json`, they will be automatically updated to match the default settings, ensuring that all required keys are present.
|
||||||
|
|
||||||
### Quick Start Guide
|
### Quick Start Guide
|
||||||
|
|
||||||
On Linux (Ubuntu/Debian):
|
On Linux (Ubuntu/Debian):
|
||||||
@ -27,8 +46,7 @@ On MacOS (requires [Homebrew](https://brew.sh)):
|
|||||||
brew install make
|
brew install make
|
||||||
```
|
```
|
||||||
|
|
||||||
Next, adjust `config.py`.
|
The server can be started with `make run`. A full overview of the main shortcuts is given by `make help`.
|
||||||
The server can then be started with `make run`. A full overview of the main shortcuts is given by `make help`.
|
|
||||||
|
|
||||||
### Detailed Instructions
|
### Detailed Instructions
|
||||||
|
|
||||||
@ -65,7 +83,6 @@ source .venv/bin/activate
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Adjust `config.py`.
|
|
||||||
To use the system, run `flask_server.py`, which starts the server:
|
To use the system, run `flask_server.py`, which starts the server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -17,6 +17,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- "eos"
|
- "eos"
|
||||||
volumes:
|
volumes:
|
||||||
- ./src/akkudoktoreos/config.py:/opt/eos/akkudoktoreos/config.py:ro
|
- ./src/akkudoktoreos/default.config.json:/opt/eos/EOS.config.json:ro
|
||||||
ports:
|
ports:
|
||||||
- "${EOS_PORT}:${EOS_PORT}"
|
- "${EOS_PORT}:${EOS_PORT}"
|
||||||
|
@ -23,7 +23,6 @@ On MacOS (requires [Homebrew](https://brew.sh)):
|
|||||||
brew install make
|
brew install make
|
||||||
```
|
```
|
||||||
|
|
||||||
Next, adjust `config.py`.
|
|
||||||
The server can then be started with `make run`. A full overview of the main shortcuts is given by `make help`.
|
The server can then be started with `make run`. A full overview of the main shortcuts is given by `make help`.
|
||||||
|
|
||||||
### Detailed Instructions
|
### Detailed Instructions
|
||||||
@ -52,6 +51,7 @@ To always use the Python version from the virtual environment, you should activa
|
|||||||
```bash
|
```bash
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
(for Bash users, the default under Linux) or
|
(for Bash users, the default under Linux) or
|
||||||
|
|
||||||
```zsh
|
```zsh
|
||||||
@ -60,7 +60,29 @@ source .venv/bin/activate
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Adjust `config.py`.
|
### Configuration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This project uses a `config.json` file to manage configuration settings.
|
||||||
|
|
||||||
|
#### Default Configuration
|
||||||
|
|
||||||
|
A default configuration file `default.config.json` is provided. This file contains all the necessary configuration keys with their default values.
|
||||||
|
|
||||||
|
#### Custom Configuration
|
||||||
|
|
||||||
|
Users can specify a custom configuration directory by setting the environment variable `EOS_DIR`.
|
||||||
|
|
||||||
|
- If the directory specified by `EOS_DIR` contains an existing `config.json` file, the application will use this configuration file.
|
||||||
|
- If the `config.json` file does not exist in the specified directory, the `default.config.json` file will be copied to the directory as `config.json`.
|
||||||
|
|
||||||
|
#### Configuration Updates
|
||||||
|
|
||||||
|
If the configuration keys in the `config.json` file are missing or different from those in `default.config.json`, they will be automatically updated to match the default settings, ensuring that all required keys are present.
|
||||||
|
|
||||||
|
### Run server
|
||||||
|
|
||||||
To use the system, run `flask_server.py`, which starts the server:
|
To use the system, run `flask_server.py`, which starts the server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -31,6 +31,7 @@ where = ["src/"]
|
|||||||
include = ["akkudoktoreos", "akkudoktoreosserver", ]
|
include = ["akkudoktoreos", "akkudoktoreosserver", ]
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
|
akkudoktoreos = ["*.json", ]
|
||||||
akkudoktoreosserver = ["data/*.npz", ]
|
akkudoktoreosserver = ["data/*.npz", ]
|
||||||
|
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
|
@ -5,9 +5,8 @@ import time
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from akkudoktoreos.class_numpy_encoder import NumpyEncoder
|
from akkudoktoreos.class_numpy_encoder import NumpyEncoder
|
||||||
|
|
||||||
# Import necessary modules from the project
|
|
||||||
from akkudoktoreos.class_optimize import optimization_problem
|
from akkudoktoreos.class_optimize import optimization_problem
|
||||||
|
from akkudoktoreos.config import get_working_dir, load_config
|
||||||
from akkudoktoreos.visualize import visualisiere_ergebnisse
|
from akkudoktoreos.visualize import visualisiere_ergebnisse
|
||||||
|
|
||||||
start_hour = 0
|
start_hour = 0
|
||||||
@ -276,10 +275,10 @@ parameter = {
|
|||||||
# Startzeit nehmen
|
# Startzeit nehmen
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
# Initialize the optimization problem
|
# Initialize the optimization problem using the default configuration
|
||||||
opt_class = optimization_problem(
|
working_dir = get_working_dir()
|
||||||
prediction_hours=48, strafe=10, optimization_hours=24, verbose=True, fixed_seed=42
|
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
|
# 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(parameter=parameter, start_hour=start_hour)
|
||||||
@ -299,17 +298,19 @@ ac_charge, dc_charge, discharge = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
visualisiere_ergebnisse(
|
visualisiere_ergebnisse(
|
||||||
gesamtlast,
|
gesamtlast=gesamtlast,
|
||||||
pv_forecast,
|
pv_forecast=pv_forecast,
|
||||||
strompreis_euro_pro_wh,
|
strompreise=strompreis_euro_pro_wh,
|
||||||
ergebnis["result"],
|
ergebnisse=ergebnis["result"],
|
||||||
ac_charge,
|
ac=ac_charge,
|
||||||
dc_charge,
|
dc=dc_charge,
|
||||||
discharge,
|
discharge=discharge,
|
||||||
temperature_forecast,
|
temperature=temperature_forecast,
|
||||||
start_hour,
|
start_hour=start_hour,
|
||||||
48,
|
einspeiseverguetung_euro_pro_wh=np.full(
|
||||||
np.full(48, parameter["einspeiseverguetung_euro_pro_wh"]),
|
config.eos.feed_in_tariff_eur_per_wh, parameter["einspeiseverguetung_euro_pro_wh"]
|
||||||
|
),
|
||||||
|
config=config,
|
||||||
filename="visualization_results.pdf",
|
filename="visualization_results.pdf",
|
||||||
extra_data=None,
|
extra_data=None,
|
||||||
)
|
)
|
||||||
|
@ -3,12 +3,13 @@ from typing import Dict, List, Optional, Union
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from akkudoktoreos.config import prediction_hours
|
from akkudoktoreos.config import EOSConfig
|
||||||
|
|
||||||
|
|
||||||
class EnergieManagementSystem:
|
class EnergieManagementSystem:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
config: EOSConfig,
|
||||||
pv_prognose_wh: Optional[np.ndarray] = None,
|
pv_prognose_wh: Optional[np.ndarray] = None,
|
||||||
strompreis_euro_pro_wh: Optional[np.ndarray] = None,
|
strompreis_euro_pro_wh: Optional[np.ndarray] = None,
|
||||||
einspeiseverguetung_euro_pro_wh: Optional[np.ndarray] = None,
|
einspeiseverguetung_euro_pro_wh: Optional[np.ndarray] = None,
|
||||||
@ -25,9 +26,9 @@ class EnergieManagementSystem:
|
|||||||
self.eauto = eauto
|
self.eauto = eauto
|
||||||
self.haushaltsgeraet = haushaltsgeraet
|
self.haushaltsgeraet = haushaltsgeraet
|
||||||
self.wechselrichter = wechselrichter
|
self.wechselrichter = wechselrichter
|
||||||
self.ac_charge_hours = np.full(prediction_hours, 0)
|
self.ac_charge_hours = np.full(config.prediction_hours, 0)
|
||||||
self.dc_charge_hours = np.full(prediction_hours, 1)
|
self.dc_charge_hours = np.full(config.prediction_hours, 1)
|
||||||
self.ev_charge_hours = np.full(prediction_hours, 0)
|
self.ev_charge_hours = np.full(config.prediction_hours, 0)
|
||||||
|
|
||||||
def set_akku_discharge_hours(self, ds: List[int]) -> None:
|
def set_akku_discharge_hours(self, ds: List[int]) -> None:
|
||||||
self.akku.set_discharge_per_hour(ds)
|
self.akku.set_discharge_per_hour(ds)
|
||||||
|
@ -8,25 +8,24 @@ from akkudoktoreos.class_akku import PVAkku
|
|||||||
from akkudoktoreos.class_ems import EnergieManagementSystem
|
from akkudoktoreos.class_ems import EnergieManagementSystem
|
||||||
from akkudoktoreos.class_haushaltsgeraet import Haushaltsgeraet
|
from akkudoktoreos.class_haushaltsgeraet import Haushaltsgeraet
|
||||||
from akkudoktoreos.class_inverter import Wechselrichter
|
from akkudoktoreos.class_inverter import Wechselrichter
|
||||||
from akkudoktoreos.config import possible_ev_charge_currents
|
from akkudoktoreos.config import AppConfig
|
||||||
from akkudoktoreos.visualize import visualisiere_ergebnisse
|
from akkudoktoreos.visualize import visualisiere_ergebnisse
|
||||||
|
|
||||||
|
|
||||||
class optimization_problem:
|
class optimization_problem:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
prediction_hours: int = 48,
|
config: AppConfig,
|
||||||
strafe: float = 10,
|
|
||||||
optimization_hours: int = 24,
|
|
||||||
verbose: bool = False,
|
verbose: bool = False,
|
||||||
fixed_seed: Optional[int] = None,
|
fixed_seed: Optional[int] = None,
|
||||||
):
|
):
|
||||||
"""Initialize the optimization problem with the required parameters."""
|
"""Initialize the optimization problem with the required parameters."""
|
||||||
self.prediction_hours = prediction_hours
|
self._config = config
|
||||||
self.strafe = strafe
|
self.prediction_hours = config.eos.prediction_hours
|
||||||
|
self.strafe = config.eos.penalty
|
||||||
self.opti_param = None
|
self.opti_param = None
|
||||||
self.fixed_eauto_hours = prediction_hours - optimization_hours
|
self.fixed_eauto_hours = config.eos.prediction_hours - config.eos.optimization_hours
|
||||||
self.possible_charge_values = possible_ev_charge_currents
|
self.possible_charge_values = config.eos.available_charging_rates_in_percentage
|
||||||
self.verbose = verbose
|
self.verbose = verbose
|
||||||
self.fix_seed = fixed_seed
|
self.fix_seed = fixed_seed
|
||||||
self.optimize_ev = True
|
self.optimize_ev = True
|
||||||
@ -210,7 +209,10 @@ class optimization_problem:
|
|||||||
|
|
||||||
if self.optimize_ev:
|
if self.optimize_ev:
|
||||||
self.toolbox.register(
|
self.toolbox.register(
|
||||||
"attr_ev_charge_index", random.randint, 0, len(possible_ev_charge_currents) - 1
|
"attr_ev_charge_index",
|
||||||
|
random.randint,
|
||||||
|
0,
|
||||||
|
len(self._config.eos.available_charging_rates_in_percentage) - 1,
|
||||||
)
|
)
|
||||||
self.toolbox.register("attr_int", random.randint, start_hour, 23)
|
self.toolbox.register("attr_int", random.randint, start_hour, 23)
|
||||||
|
|
||||||
@ -236,7 +238,7 @@ class optimization_problem:
|
|||||||
"mutate_ev_charge_index",
|
"mutate_ev_charge_index",
|
||||||
tools.mutUniformInt,
|
tools.mutUniformInt,
|
||||||
low=0,
|
low=0,
|
||||||
up=len(possible_ev_charge_currents) - 1,
|
up=len(self._config.eos.available_charging_rates_in_percentage) - 1,
|
||||||
indpb=0.2,
|
indpb=0.2,
|
||||||
)
|
)
|
||||||
# - Start hour mutation for household devices
|
# - Start hour mutation for household devices
|
||||||
@ -271,7 +273,8 @@ class optimization_problem:
|
|||||||
|
|
||||||
if self.optimize_ev:
|
if self.optimize_ev:
|
||||||
eautocharge_hours_float = [
|
eautocharge_hours_float = [
|
||||||
possible_ev_charge_currents[i] for i in eautocharge_hours_index
|
self._config.eos.available_charging_rates_in_percentage[i]
|
||||||
|
for i in eautocharge_hours_index
|
||||||
]
|
]
|
||||||
ems.set_ev_charge_hours(eautocharge_hours_float)
|
ems.set_ev_charge_hours(eautocharge_hours_float)
|
||||||
else:
|
else:
|
||||||
@ -420,6 +423,7 @@ class optimization_problem:
|
|||||||
# Initialize the inverter and energy management system
|
# Initialize the inverter and energy management system
|
||||||
wr = Wechselrichter(10000, akku)
|
wr = Wechselrichter(10000, akku)
|
||||||
ems = EnergieManagementSystem(
|
ems = EnergieManagementSystem(
|
||||||
|
config=self._config.eos,
|
||||||
gesamtlast=parameter["gesamtlast"],
|
gesamtlast=parameter["gesamtlast"],
|
||||||
pv_prognose_wh=parameter["pv_forecast"],
|
pv_prognose_wh=parameter["pv_forecast"],
|
||||||
strompreis_euro_pro_wh=parameter["strompreis_euro_pro_wh"],
|
strompreis_euro_pro_wh=parameter["strompreis_euro_pro_wh"],
|
||||||
@ -444,23 +448,24 @@ class optimization_problem:
|
|||||||
)
|
)
|
||||||
if self.optimize_ev:
|
if self.optimize_ev:
|
||||||
eautocharge_hours_float = [
|
eautocharge_hours_float = [
|
||||||
possible_ev_charge_currents[i] for i in eautocharge_hours_float
|
self._config.eos.available_charging_rates_in_percentage[i]
|
||||||
|
for i in eautocharge_hours_float
|
||||||
]
|
]
|
||||||
|
|
||||||
ac_charge, dc_charge, discharge = self.decode_charge_discharge(discharge_hours_bin)
|
ac_charge, dc_charge, discharge = self.decode_charge_discharge(discharge_hours_bin)
|
||||||
# Visualize the results
|
# Visualize the results
|
||||||
visualisiere_ergebnisse(
|
visualisiere_ergebnisse(
|
||||||
parameter["gesamtlast"],
|
gesamtlast=parameter["gesamtlast"],
|
||||||
parameter["pv_forecast"],
|
pv_forecast=parameter["pv_forecast"],
|
||||||
parameter["strompreis_euro_pro_wh"],
|
strompreise=parameter["strompreis_euro_pro_wh"],
|
||||||
o,
|
ergebnisse=o,
|
||||||
ac_charge,
|
ac=ac_charge,
|
||||||
dc_charge,
|
dc=dc_charge,
|
||||||
discharge,
|
discharge=discharge,
|
||||||
parameter["temperature_forecast"],
|
temperature=parameter["temperature_forecast"],
|
||||||
start_hour,
|
start_hour=start_hour,
|
||||||
self.prediction_hours,
|
einspeiseverguetung_euro_pro_wh=einspeiseverguetung_euro_pro_wh,
|
||||||
einspeiseverguetung_euro_pro_wh,
|
config=self._config,
|
||||||
extra_data=extra_data,
|
extra_data=extra_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from akkudoktoreos.config import AppConfig, SetupIncomplete
|
||||||
|
|
||||||
|
|
||||||
def repeat_to_shape(array, target_shape):
|
def repeat_to_shape(array, target_shape):
|
||||||
# Check if the array fits the target shape
|
# Check if the array fits the target shape
|
||||||
@ -23,53 +25,57 @@ def repeat_to_shape(array, target_shape):
|
|||||||
|
|
||||||
class HourlyElectricityPriceForecast:
|
class HourlyElectricityPriceForecast:
|
||||||
def __init__(
|
def __init__(
|
||||||
self, source, cache_dir="cache", charges=0.000228, prediction_hours=24, cache=True
|
self, source: str | Path, config: AppConfig, charges=0.000228, use_cache=True
|
||||||
): # 228
|
): # 228
|
||||||
self.cache_dir = cache_dir
|
self.cache_dir = config.working_dir / config.directories.cache
|
||||||
self.cache = cache
|
self.use_cache = use_cache
|
||||||
os.makedirs(self.cache_dir, exist_ok=True)
|
if not self.cache_dir.is_dir():
|
||||||
self.cache_time_file = os.path.join(self.cache_dir, "cache_timestamp.txt")
|
raise SetupIncomplete(f"Output path does not exist: {self.cache_dir}.")
|
||||||
|
|
||||||
|
self.cache_time_file = self.cache_dir / "cache_timestamp.txt"
|
||||||
self.prices = self.load_data(source)
|
self.prices = self.load_data(source)
|
||||||
self.charges = charges
|
self.charges = charges
|
||||||
self.prediction_hours = prediction_hours
|
self.prediction_hours = config.eos.prediction_hours
|
||||||
|
|
||||||
def load_data(self, source):
|
def load_data(self, source: str | Path):
|
||||||
cache_filename = self.get_cache_filename(source)
|
cache_file = self.get_cache_file(source)
|
||||||
if source.startswith("http"):
|
if isinstance(source, str):
|
||||||
if os.path.exists(cache_filename) and not self.is_cache_expired() and self.cache:
|
if cache_file.is_file() and not self.is_cache_expired() and self.use_cache:
|
||||||
print("Loading data from cache...")
|
print("Loading data from cache...")
|
||||||
with open(cache_filename, "r") as file:
|
with cache_file.open("r") as file:
|
||||||
json_data = json.load(file)
|
json_data = json.load(file)
|
||||||
else:
|
else:
|
||||||
print("Loading data from the URL...")
|
print("Loading data from the URL...")
|
||||||
response = requests.get(source)
|
response = requests.get(source)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
json_data = response.json()
|
json_data = response.json()
|
||||||
with open(cache_filename, "w") as file:
|
with cache_file.open("w") as file:
|
||||||
json.dump(json_data, file)
|
json.dump(json_data, file)
|
||||||
self.update_cache_timestamp()
|
self.update_cache_timestamp()
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Error fetching data: {response.status_code}")
|
raise Exception(f"Error fetching data: {response.status_code}")
|
||||||
else:
|
elif source.is_file():
|
||||||
with open(source, "r") as file:
|
with source.open("r") as file:
|
||||||
json_data = json.load(file)
|
json_data = json.load(file)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Input is not a valid path: {source}")
|
||||||
return json_data["values"]
|
return json_data["values"]
|
||||||
|
|
||||||
def get_cache_filename(self, url):
|
def get_cache_file(self, url):
|
||||||
hash_object = hashlib.sha256(url.encode())
|
hash_object = hashlib.sha256(url.encode())
|
||||||
hex_dig = hash_object.hexdigest()
|
hex_dig = hash_object.hexdigest()
|
||||||
return os.path.join(self.cache_dir, f"cache_{hex_dig}.json")
|
return self.cache_dir / f"cache_{hex_dig}.json"
|
||||||
|
|
||||||
def is_cache_expired(self):
|
def is_cache_expired(self):
|
||||||
if not os.path.exists(self.cache_time_file):
|
if not self.cache_time_file.is_file():
|
||||||
return True
|
return True
|
||||||
with open(self.cache_time_file, "r") as file:
|
with self.cache_time_file.open("r") as file:
|
||||||
timestamp_str = file.read()
|
timestamp_str = file.read()
|
||||||
last_cache_time = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S")
|
last_cache_time = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S")
|
||||||
return datetime.now() - last_cache_time > timedelta(hours=1)
|
return datetime.now() - last_cache_time > timedelta(hours=1)
|
||||||
|
|
||||||
def update_cache_timestamp(self):
|
def update_cache_timestamp(self):
|
||||||
with open(self.cache_time_file, "w") as file:
|
with self.cache_time_file.open("w") as file:
|
||||||
file.write(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
file.write(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||||
|
|
||||||
def get_price_for_date(self, date_str):
|
def get_price_for_date(self, date_str):
|
||||||
|
@ -1,30 +1,290 @@
|
|||||||
|
"""This module provides functionality to manage and handle configuration for the EOS system.
|
||||||
|
|
||||||
|
The module including loading, merging, and validating JSON configuration files.
|
||||||
|
It also provides utility functions for working directory setup and date handling.
|
||||||
|
|
||||||
|
Key features:
|
||||||
|
- Loading and merging configurations from default or custom JSON files
|
||||||
|
- Validating configurations using Pydantic models
|
||||||
|
- Managing directory setups for the application
|
||||||
|
- Utility to get prediction start and end dates
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
output_dir = "output"
|
from pydantic import BaseModel, ValidationError
|
||||||
|
|
||||||
prediction_hours = 48
|
EOS_DIR = "EOS_DIR"
|
||||||
optimization_hours = 24
|
ENCODING = "UTF-8"
|
||||||
strafe = 10
|
CONFIG_FILE_NAME = "EOS.config.json"
|
||||||
possible_ev_charge_currents = [
|
DEFAULT_CONFIG_FILE = Path(__file__).parent.joinpath("default.config.json")
|
||||||
0.0,
|
|
||||||
6.0 / 16.0,
|
|
||||||
# 7.0 / 16.0,
|
|
||||||
8.0 / 16.0,
|
|
||||||
# 9.0 / 16.0,
|
|
||||||
10.0 / 16.0,
|
|
||||||
# 11.0 / 16.0,
|
|
||||||
12.0 / 16.0,
|
|
||||||
# 13.0 / 16.0,
|
|
||||||
14.0 / 16.0,
|
|
||||||
# 15.0 / 16.0,
|
|
||||||
1.0,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def get_start_enddate(prediction_hours=48, startdate=None):
|
class FolderConfig(BaseModel):
|
||||||
############
|
"""Folder configuration for the EOS system.
|
||||||
# Parameter
|
|
||||||
############
|
Uses working_dir as root path.
|
||||||
|
The working directory can be either cwd or
|
||||||
|
a path or folder defined by the EOS_DIR environment variable.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
output (str): Directory name for output files.
|
||||||
|
cache (str): Directory name for cache files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
output: str
|
||||||
|
cache: str
|
||||||
|
|
||||||
|
|
||||||
|
class EOSConfig(BaseModel):
|
||||||
|
"""EOS system-specific configuration.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
prediction_hours (int): Number of hours for predictions.
|
||||||
|
optimization_hours (int): Number of hours for optimizations.
|
||||||
|
penalty (int): Penalty factor used in optimization.
|
||||||
|
available_charging_rates_in_percentage (list[float]): List of available charging rates as percentages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
prediction_hours: int
|
||||||
|
optimization_hours: int
|
||||||
|
penalty: int
|
||||||
|
available_charging_rates_in_percentage: list[float]
|
||||||
|
feed_in_tariff_eur_per_wh: int
|
||||||
|
|
||||||
|
|
||||||
|
class BaseConfig(BaseModel):
|
||||||
|
"""Base configuration for the EOS system.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
directories (FolderConfig): Configuration for directory paths (output, cache).
|
||||||
|
eos (EOSConfig): Configuration for EOS-specific settings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
directories: FolderConfig
|
||||||
|
eos: EOSConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AppConfig(BaseConfig):
|
||||||
|
"""Application-level configuration that extends the base configuration with a working directory.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
working_dir (Path): The root directory for the application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
working_dir: Path
|
||||||
|
|
||||||
|
def run_setup(self) -> None:
|
||||||
|
"""Runs setup for the application by ensuring that required directories exist.
|
||||||
|
|
||||||
|
If a directory does not exist, it is created.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
OSError: If directories cannot be created.
|
||||||
|
"""
|
||||||
|
print("Checking directory settings and creating missing directories...")
|
||||||
|
for key, value in self.directories.model_dump().items():
|
||||||
|
if not isinstance(value, str):
|
||||||
|
continue
|
||||||
|
path = self.working_dir / value
|
||||||
|
print(f"'{key}': {path}")
|
||||||
|
os.makedirs(path, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
class SetupIncomplete(Exception):
|
||||||
|
"""Exception class for errors related to incomplete setup of the EOS system."""
|
||||||
|
|
||||||
|
|
||||||
|
def _load_json(path: Path) -> dict[str, Any]:
|
||||||
|
"""Load a JSON file from a given path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (Path): Path to the JSON file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, Any]: Parsed JSON content.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If the JSON file does not exist.
|
||||||
|
json.JSONDecodeError: If the file cannot be parsed as valid JSON.
|
||||||
|
"""
|
||||||
|
with path.open("r") as f_in:
|
||||||
|
return json.load(f_in)
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_json(default_data: dict[str, Any], custom_data: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Recursively merge two dictionaries, using values from `custom_data` when available.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
default_data (dict[str, Any]): The default configuration values.
|
||||||
|
custom_data (dict[str, Any]): The custom configuration values.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, Any]: Merged configuration data.
|
||||||
|
"""
|
||||||
|
merged_data = {}
|
||||||
|
for key, default_value in default_data.items():
|
||||||
|
if key in custom_data:
|
||||||
|
custom_value = custom_data[key]
|
||||||
|
if isinstance(default_value, dict) and isinstance(custom_value, dict):
|
||||||
|
merged_data[key] = _merge_json(default_value, custom_value)
|
||||||
|
elif type(default_value) is type(custom_value):
|
||||||
|
merged_data[key] = custom_value
|
||||||
|
else:
|
||||||
|
# use default value if types differ
|
||||||
|
merged_data[key] = default_value
|
||||||
|
else:
|
||||||
|
merged_data[key] = default_value
|
||||||
|
return merged_data
|
||||||
|
|
||||||
|
|
||||||
|
def _config_update_available(merged_data: dict[str, Any], custom_data: dict[str, Any]) -> bool:
|
||||||
|
"""Check if the configuration needs to be updated by comparing merged data and custom data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
merged_data (dict[str, Any]): The merged configuration data.
|
||||||
|
custom_data (dict[str, Any]): The custom configuration data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if there is a difference indicating that an update is needed, otherwise False.
|
||||||
|
"""
|
||||||
|
if merged_data.keys() != custom_data.keys():
|
||||||
|
return True
|
||||||
|
|
||||||
|
for key in merged_data:
|
||||||
|
value1 = merged_data[key]
|
||||||
|
value2 = custom_data[key]
|
||||||
|
|
||||||
|
if isinstance(value1, dict) and isinstance(value2, dict):
|
||||||
|
if _config_update_available(value1, value2):
|
||||||
|
return True
|
||||||
|
elif value1 != value2:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_file(path: Path, copy_default: bool) -> Path:
|
||||||
|
"""Get the valid configuration file path. If the custom config is not found, it uses the default config.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (Path): Path to the working directory.
|
||||||
|
copy_default (bool): If True, copy the default configuration if custom config is not found.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path: Path to the valid configuration file.
|
||||||
|
"""
|
||||||
|
config = path.resolve() / CONFIG_FILE_NAME
|
||||||
|
if config.is_file():
|
||||||
|
print(f"Using configuration from: {config}")
|
||||||
|
return config
|
||||||
|
|
||||||
|
if not path.is_dir():
|
||||||
|
print(f"Path does not exist: {path}. Using default configuration...")
|
||||||
|
return DEFAULT_CONFIG_FILE
|
||||||
|
|
||||||
|
if not copy_default:
|
||||||
|
print("No custom configuration provided. Using default configuration...")
|
||||||
|
return DEFAULT_CONFIG_FILE
|
||||||
|
|
||||||
|
try:
|
||||||
|
return Path(shutil.copy2(DEFAULT_CONFIG_FILE, config))
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Could not copy default config: {exc}. Using default copy...")
|
||||||
|
return DEFAULT_CONFIG_FILE
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_and_update(custom_config: Path, update_outdated: bool = False) -> bool:
|
||||||
|
"""Merge custom and default configurations, and optionally update the custom config if outdated.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
custom_config (Path): Path to the custom configuration file.
|
||||||
|
update_outdated (bool): If True, update the custom config if it is outdated.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the custom config was updated, otherwise False.
|
||||||
|
"""
|
||||||
|
if custom_config == DEFAULT_CONFIG_FILE:
|
||||||
|
return False
|
||||||
|
default_data = _load_json(DEFAULT_CONFIG_FILE)
|
||||||
|
custom_data = _load_json(custom_config)
|
||||||
|
merged_data = _merge_json(default_data, custom_data)
|
||||||
|
|
||||||
|
if not _config_update_available(merged_data, custom_data):
|
||||||
|
print(f"Custom config {custom_config} is up-to-date...")
|
||||||
|
return False
|
||||||
|
print(f"Custom config {custom_config} is outdated...")
|
||||||
|
if update_outdated:
|
||||||
|
with custom_config.open("w") as f_out:
|
||||||
|
json.dump(merged_data, f_out, indent=2)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(
|
||||||
|
working_dir: Path, copy_default: bool = False, update_outdated: bool = True
|
||||||
|
) -> AppConfig:
|
||||||
|
"""Load the application configuration from the specified directory, merging with defaults if needed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
working_dir (Path): Path to the working directory.
|
||||||
|
copy_default (bool): Whether to copy the default configuration if custom config is missing.
|
||||||
|
update_outdated (bool): Whether to update outdated custom configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AppConfig: Loaded application configuration.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the configuration is incomplete or not valid.
|
||||||
|
"""
|
||||||
|
# make sure working_dir is always a full path
|
||||||
|
working_dir = working_dir.resolve()
|
||||||
|
|
||||||
|
config = get_config_file(working_dir, copy_default)
|
||||||
|
_merge_and_update(config, update_outdated)
|
||||||
|
|
||||||
|
with config.open("r", encoding=ENCODING) as f_in:
|
||||||
|
try:
|
||||||
|
base_config = BaseConfig.model_validate(json.load(f_in))
|
||||||
|
return AppConfig.model_validate(
|
||||||
|
{"working_dir": working_dir, **base_config.model_dump()}
|
||||||
|
)
|
||||||
|
except ValidationError as exc:
|
||||||
|
raise ValueError(f"Configuration {config} is incomplete or not valid: {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_working_dir() -> Path:
|
||||||
|
"""Get the working directory for the application, either from an environment variable or the current working directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path: The path to the working directory.
|
||||||
|
"""
|
||||||
|
custom_dir = os.getenv(EOS_DIR)
|
||||||
|
if custom_dir is None:
|
||||||
|
working_dir = Path.cwd()
|
||||||
|
print(f"No custom directory provided. Setting working directory to: {working_dir}")
|
||||||
|
else:
|
||||||
|
working_dir = Path(custom_dir).resolve()
|
||||||
|
print(f"Custom directory provided. Setting working directory to: {working_dir}")
|
||||||
|
return working_dir
|
||||||
|
|
||||||
|
|
||||||
|
def get_start_enddate(
|
||||||
|
prediction_hours: int, startdate: Optional[datetime] = None
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
"""Calculate the start and end dates based on the given prediction hours and optional start date.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prediction_hours (int): Number of hours for predictions.
|
||||||
|
startdate (Optional[datetime]): Optional starting datetime.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[str, str]: The current date (start date) and end date in the format 'YYYY-MM-DD'.
|
||||||
|
"""
|
||||||
if startdate is None:
|
if startdate is None:
|
||||||
date = (datetime.now().date() + timedelta(hours=prediction_hours)).strftime("%Y-%m-%d")
|
date = (datetime.now().date() + timedelta(hours=prediction_hours)).strftime("%Y-%m-%d")
|
||||||
date_now = datetime.now().strftime("%Y-%m-%d")
|
date_now = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
15
src/akkudoktoreos/default.config.json
Normal file
15
src/akkudoktoreos/default.config.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"directories": {
|
||||||
|
"output": "output",
|
||||||
|
"cache": "cache"
|
||||||
|
},
|
||||||
|
"eos": {
|
||||||
|
"prediction_hours": 48,
|
||||||
|
"optimization_hours": 24,
|
||||||
|
"penalty": 10,
|
||||||
|
"available_charging_rates_in_percentage": [
|
||||||
|
0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0
|
||||||
|
],
|
||||||
|
"feed_in_tariff_eur_per_wh": 48
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
|
||||||
|
|
||||||
# Set the backend for matplotlib to Agg
|
# Set the backend for matplotlib to Agg
|
||||||
import matplotlib
|
import matplotlib
|
||||||
@ -8,7 +7,7 @@ import numpy as np
|
|||||||
from matplotlib.backends.backend_pdf import PdfPages
|
from matplotlib.backends.backend_pdf import PdfPages
|
||||||
|
|
||||||
from akkudoktoreos.class_sommerzeit import ist_dst_wechsel
|
from akkudoktoreos.class_sommerzeit import ist_dst_wechsel
|
||||||
from akkudoktoreos.config import output_dir
|
from akkudoktoreos.config import AppConfig, SetupIncomplete
|
||||||
|
|
||||||
matplotlib.use("Agg")
|
matplotlib.use("Agg")
|
||||||
|
|
||||||
@ -23,22 +22,24 @@ def visualisiere_ergebnisse(
|
|||||||
discharge, # Discharge allowed
|
discharge, # Discharge allowed
|
||||||
temperature,
|
temperature,
|
||||||
start_hour,
|
start_hour,
|
||||||
prediction_hours,
|
|
||||||
einspeiseverguetung_euro_pro_wh,
|
einspeiseverguetung_euro_pro_wh,
|
||||||
|
config: AppConfig,
|
||||||
filename="visualization_results.pdf",
|
filename="visualization_results.pdf",
|
||||||
extra_data=None,
|
extra_data=None,
|
||||||
):
|
):
|
||||||
#####################
|
#####################
|
||||||
# 24-hour visualization
|
# 24-hour visualization
|
||||||
#####################
|
#####################
|
||||||
if not os.path.exists(output_dir):
|
output_dir = config.working_dir / config.directories.output
|
||||||
os.makedirs(output_dir)
|
if not output_dir.is_dir():
|
||||||
output_file = os.path.join(output_dir, filename)
|
raise SetupIncomplete(f"Output path does not exist: {output_dir}.")
|
||||||
|
|
||||||
|
output_file = output_dir.joinpath(filename)
|
||||||
with PdfPages(output_file) as pdf:
|
with PdfPages(output_file) as pdf:
|
||||||
# Load and PV generation
|
# Load and PV generation
|
||||||
plt.figure(figsize=(14, 14))
|
plt.figure(figsize=(14, 14))
|
||||||
plt.subplot(3, 3, 1)
|
plt.subplot(3, 3, 1)
|
||||||
hours = np.arange(0, prediction_hours)
|
hours = np.arange(0, config.eos.prediction_hours)
|
||||||
|
|
||||||
gesamtlast_array = np.array(gesamtlast)
|
gesamtlast_array = np.array(gesamtlast)
|
||||||
# Plot individual loads
|
# Plot individual loads
|
||||||
@ -101,9 +102,9 @@ def visualisiere_ergebnisse(
|
|||||||
plt.figure(figsize=(14, 10))
|
plt.figure(figsize=(14, 10))
|
||||||
|
|
||||||
if ist_dst_wechsel(datetime.datetime.now()):
|
if ist_dst_wechsel(datetime.datetime.now()):
|
||||||
hours = np.arange(start_hour, prediction_hours - 1)
|
hours = np.arange(start_hour, config.eos.prediction_hours - 1)
|
||||||
else:
|
else:
|
||||||
hours = np.arange(start_hour, prediction_hours)
|
hours = np.arange(start_hour, config.eos.prediction_hours)
|
||||||
|
|
||||||
# Energy flow, grid feed-in, and grid consumption
|
# Energy flow, grid feed-in, and grid consumption
|
||||||
plt.subplot(3, 2, 1)
|
plt.subplot(3, 2, 1)
|
||||||
@ -187,7 +188,7 @@ def visualisiere_ergebnisse(
|
|||||||
|
|
||||||
# Plot for AC, DC charging, and Discharge status using bar charts
|
# Plot for AC, DC charging, and Discharge status using bar charts
|
||||||
ax1 = plt.subplot(3, 2, 5)
|
ax1 = plt.subplot(3, 2, 5)
|
||||||
hours = np.arange(0, prediction_hours)
|
hours = np.arange(0, config.eos.prediction_hours)
|
||||||
# Plot AC charging as bars (relative values between 0 and 1)
|
# Plot AC charging as bars (relative values between 0 and 1)
|
||||||
plt.bar(hours, ac, width=0.4, label="AC Charging (relative)", color="blue", alpha=0.6)
|
plt.bar(hours, ac, width=0.4, label="AC Charging (relative)", color="blue", alpha=0.6)
|
||||||
|
|
||||||
@ -209,16 +210,16 @@ def visualisiere_ergebnisse(
|
|||||||
|
|
||||||
# Configure the plot
|
# Configure the plot
|
||||||
ax1.legend(loc="upper left")
|
ax1.legend(loc="upper left")
|
||||||
ax1.set_xlim(0, prediction_hours)
|
ax1.set_xlim(0, config.eos.prediction_hours)
|
||||||
ax1.set_xlabel("Hour")
|
ax1.set_xlabel("Hour")
|
||||||
ax1.set_ylabel("Relative Power (0-1) / Discharge (0 or 1)")
|
ax1.set_ylabel("Relative Power (0-1) / Discharge (0 or 1)")
|
||||||
ax1.set_title("AC/DC Charging and Discharge Overview")
|
ax1.set_title("AC/DC Charging and Discharge Overview")
|
||||||
ax1.grid(True)
|
ax1.grid(True)
|
||||||
|
|
||||||
if ist_dst_wechsel(datetime.datetime.now()):
|
if ist_dst_wechsel(datetime.datetime.now()):
|
||||||
hours = np.arange(start_hour, prediction_hours - 1)
|
hours = np.arange(start_hour, config.eos.prediction_hours - 1)
|
||||||
else:
|
else:
|
||||||
hours = np.arange(start_hour, prediction_hours)
|
hours = np.arange(start_hour, config.eos.prediction_hours)
|
||||||
|
|
||||||
pdf.savefig() # Save the current figure state to the PDF
|
pdf.savefig() # Save the current figure state to the PDF
|
||||||
plt.close() # Close the current figure to free up memory
|
plt.close() # Close the current figure to free up memory
|
||||||
|
@ -20,20 +20,18 @@ from akkudoktoreos.class_optimize import optimization_problem
|
|||||||
from akkudoktoreos.class_pv_forecast import PVForecast
|
from akkudoktoreos.class_pv_forecast import PVForecast
|
||||||
from akkudoktoreos.class_strompreis import HourlyElectricityPriceForecast
|
from akkudoktoreos.class_strompreis import HourlyElectricityPriceForecast
|
||||||
from akkudoktoreos.config import (
|
from akkudoktoreos.config import (
|
||||||
|
SetupIncomplete,
|
||||||
get_start_enddate,
|
get_start_enddate,
|
||||||
optimization_hours,
|
get_working_dir,
|
||||||
output_dir,
|
load_config,
|
||||||
prediction_hours,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
opt_class = optimization_problem(
|
working_dir = get_working_dir()
|
||||||
prediction_hours=prediction_hours,
|
# copy config to working directory. Make this a CLI option later
|
||||||
strafe=10,
|
config = load_config(working_dir, True)
|
||||||
optimization_hours=optimization_hours,
|
opt_class = optimization_problem(config)
|
||||||
verbose=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def isfloat(num: Any) -> TypeGuard[float]:
|
def isfloat(num: Any) -> TypeGuard[float]:
|
||||||
@ -58,14 +56,11 @@ def isfloat(num: Any) -> TypeGuard[float]:
|
|||||||
@app.route("/strompreis", methods=["GET"])
|
@app.route("/strompreis", methods=["GET"])
|
||||||
def flask_strompreis():
|
def flask_strompreis():
|
||||||
# Get the current date and the end date based on prediction hours
|
# Get the current date and the end date based on prediction hours
|
||||||
date_now, date = get_start_enddate(prediction_hours, startdate=datetime.now().date())
|
date_now, date = get_start_enddate(config.eos.prediction_hours, startdate=datetime.now().date())
|
||||||
filepath = os.path.join(
|
|
||||||
r"test_data", r"strompreise_akkudokAPI.json"
|
|
||||||
) # Adjust the path to the JSON file
|
|
||||||
price_forecast = HourlyElectricityPriceForecast(
|
price_forecast = HourlyElectricityPriceForecast(
|
||||||
source=f"https://api.akkudoktor.net/prices?start={date_now}&end={date}",
|
source=f"https://api.akkudoktor.net/prices?start={date_now}&end={date}",
|
||||||
prediction_hours=prediction_hours,
|
config=config.eos.prediction_hours,
|
||||||
cache=False,
|
use_cache=False,
|
||||||
)
|
)
|
||||||
specific_date_prices = price_forecast.get_price_for_daterange(
|
specific_date_prices = price_forecast.get_price_for_daterange(
|
||||||
date_now, date
|
date_now, date
|
||||||
@ -141,7 +136,7 @@ def flask_gesamtlast_simple():
|
|||||||
request.args.get("year_energy")
|
request.args.get("year_energy")
|
||||||
) # Get annual energy value from query parameters
|
) # Get annual energy value from query parameters
|
||||||
date_now, date = get_start_enddate(
|
date_now, date = get_start_enddate(
|
||||||
prediction_hours, startdate=datetime.now().date()
|
config.eos.prediction_hours, startdate=datetime.now().date()
|
||||||
) # Get the current date and prediction end date
|
) # Get the current date and prediction end date
|
||||||
|
|
||||||
###############
|
###############
|
||||||
@ -159,7 +154,9 @@ def flask_gesamtlast_simple():
|
|||||||
0
|
0
|
||||||
] # Get expected household load for the date range
|
] # Get expected household load for the date range
|
||||||
|
|
||||||
gesamtlast = Gesamtlast(prediction_hours=prediction_hours) # Create Gesamtlast instance
|
gesamtlast = Gesamtlast(
|
||||||
|
prediction_hours=config.eos.prediction_hours
|
||||||
|
) # Create Gesamtlast instance
|
||||||
gesamtlast.hinzufuegen(
|
gesamtlast.hinzufuegen(
|
||||||
"Haushalt", leistung_haushalt
|
"Haushalt", leistung_haushalt
|
||||||
) # Add household load to total load calculation
|
) # Add household load to total load calculation
|
||||||
@ -181,13 +178,15 @@ def flask_pvprognose():
|
|||||||
# Retrieve URL and AC power measurement from query parameters
|
# Retrieve URL and AC power measurement from query parameters
|
||||||
url = request.args.get("url")
|
url = request.args.get("url")
|
||||||
ac_power_measurement = request.args.get("ac_power_measurement")
|
ac_power_measurement = request.args.get("ac_power_measurement")
|
||||||
date_now, date = get_start_enddate(prediction_hours, startdate=datetime.now().date())
|
date_now, date = get_start_enddate(
|
||||||
|
config.eos.prediction_hours, startdate=datetime.now().date()
|
||||||
|
)
|
||||||
|
|
||||||
###############
|
###############
|
||||||
# PV Forecast
|
# PV Forecast
|
||||||
###############
|
###############
|
||||||
PVforecast = PVForecast(
|
PVforecast = PVForecast(
|
||||||
prediction_hours=prediction_hours, url=url
|
prediction_hours=config.eos.prediction_hours, url=url
|
||||||
) # Instantiate PVForecast with given parameters
|
) # Instantiate PVForecast with given parameters
|
||||||
if isfloat(ac_power_measurement): # Check if the AC power measurement is a valid float
|
if isfloat(ac_power_measurement): # Check if the AC power measurement is a valid float
|
||||||
PVforecast.update_ac_power_measurement(
|
PVforecast.update_ac_power_measurement(
|
||||||
@ -255,9 +254,10 @@ def flask_optimize():
|
|||||||
@app.route("/visualization_results.pdf")
|
@app.route("/visualization_results.pdf")
|
||||||
def get_pdf():
|
def get_pdf():
|
||||||
# Endpoint to serve the generated PDF with visualization results
|
# Endpoint to serve the generated PDF with visualization results
|
||||||
return send_from_directory(
|
output_path = config.working_dir / config.directories.output
|
||||||
os.path.abspath(output_dir), "visualization_results.pdf"
|
if not output_path.is_dir():
|
||||||
) # Adjust the directory if needed
|
raise SetupIncomplete(f"Output path does not exist: {output_path}.")
|
||||||
|
return send_from_directory(output_path, "visualization_results.pdf")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/site-map")
|
@app.route("/site-map")
|
||||||
@ -293,6 +293,8 @@ def root():
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
|
config.run_setup()
|
||||||
|
|
||||||
# Set host and port from environment variables or defaults
|
# Set host and port from environment variables or defaults
|
||||||
host = os.getenv("FLASK_RUN_HOST", "0.0.0.0")
|
host = os.getenv("FLASK_RUN_HOST", "0.0.0.0")
|
||||||
port = os.getenv("FLASK_RUN_PORT", 8503)
|
port = os.getenv("FLASK_RUN_PORT", 8503)
|
||||||
|
@ -3,10 +3,21 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from xprocess import ProcessStarter
|
from xprocess import ProcessStarter
|
||||||
|
|
||||||
|
from akkudoktoreos.config import EOS_DIR, AppConfig, load_config
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="tmp_config")
|
||||||
|
def load_config_tmp(tmp_path: Path) -> AppConfig:
|
||||||
|
"""Creates an AppConfig from default.config.json with a tmp output directory."""
|
||||||
|
config = load_config(tmp_path)
|
||||||
|
config.directories.output = tmp_path
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def disable_debug_logging():
|
def disable_debug_logging():
|
||||||
@ -29,7 +40,7 @@ def is_full_run(request):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def server(xprocess):
|
def server(xprocess, tmp_path: Path):
|
||||||
"""Fixture to start the server.
|
"""Fixture to start the server.
|
||||||
|
|
||||||
Provides URL of the server.
|
Provides URL of the server.
|
||||||
@ -45,8 +56,7 @@ def server(xprocess):
|
|||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
)
|
)
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
test_dir = os.path.dirname(os.path.realpath(__file__))
|
project_dir = Path(__file__).parent.parent
|
||||||
project_dir = os.path.abspath(os.path.join(test_dir, ".."))
|
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
[sys.executable, "-m", "pip", "install", "-e", project_dir],
|
[sys.executable, "-m", "pip", "install", "-e", project_dir],
|
||||||
check=True,
|
check=True,
|
||||||
@ -56,14 +66,15 @@ def server(xprocess):
|
|||||||
|
|
||||||
# command to start server process
|
# command to start server process
|
||||||
args = [sys.executable, "-m", "akkudoktoreosserver.flask_server"]
|
args = [sys.executable, "-m", "akkudoktoreosserver.flask_server"]
|
||||||
|
env = {EOS_DIR: f"{tmp_path}", **os.environ.copy()}
|
||||||
|
|
||||||
# startup pattern
|
# startup pattern
|
||||||
pattern = "Debugger PIN:"
|
pattern = "Debugger PIN:"
|
||||||
# search the first 12 lines for the startup pattern, if not found
|
# search the first 30 lines for the startup pattern, if not found
|
||||||
# a RuntimeError will be raised informing the user
|
# a RuntimeError will be raised informing the user
|
||||||
max_read_lines = 12
|
max_read_lines = 30
|
||||||
|
|
||||||
# will wait for 10 seconds before timing out
|
# will wait for 30 seconds before timing out
|
||||||
timeout = 30
|
timeout = 30
|
||||||
|
|
||||||
# xprocess will now attempt to clean up upon interruptions
|
# xprocess will now attempt to clean up upon interruptions
|
||||||
|
@ -4,7 +4,8 @@ import pytest
|
|||||||
from akkudoktoreos.class_akku import PVAkku
|
from akkudoktoreos.class_akku import PVAkku
|
||||||
from akkudoktoreos.class_ems import EnergieManagementSystem
|
from akkudoktoreos.class_ems import EnergieManagementSystem
|
||||||
from akkudoktoreos.class_haushaltsgeraet import Haushaltsgeraet
|
from akkudoktoreos.class_haushaltsgeraet import Haushaltsgeraet
|
||||||
from akkudoktoreos.class_inverter import Wechselrichter # Example import
|
from akkudoktoreos.class_inverter import Wechselrichter
|
||||||
|
from akkudoktoreos.config import AppConfig
|
||||||
|
|
||||||
prediction_hours = 48
|
prediction_hours = 48
|
||||||
optimization_hours = 24
|
optimization_hours = 24
|
||||||
@ -13,7 +14,7 @@ start_hour = 1
|
|||||||
|
|
||||||
# Example initialization of necessary components
|
# Example initialization of necessary components
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def create_ems_instance():
|
def create_ems_instance(tmp_config: AppConfig) -> EnergieManagementSystem:
|
||||||
"""Fixture to create an EnergieManagementSystem instance with given test parameters."""
|
"""Fixture to create an EnergieManagementSystem instance with given test parameters."""
|
||||||
# Initialize the battery and the inverter
|
# Initialize the battery and the inverter
|
||||||
akku = PVAkku(kapazitaet_wh=5000, start_soc_prozent=80, hours=48, min_soc_prozent=10)
|
akku = PVAkku(kapazitaet_wh=5000, start_soc_prozent=80, hours=48, min_soc_prozent=10)
|
||||||
@ -189,6 +190,7 @@ def create_ems_instance():
|
|||||||
|
|
||||||
# Initialize the energy management system with the respective parameters
|
# Initialize the energy management system with the respective parameters
|
||||||
ems = EnergieManagementSystem(
|
ems = EnergieManagementSystem(
|
||||||
|
config=tmp_config.eos,
|
||||||
pv_prognose_wh=pv_prognose_wh,
|
pv_prognose_wh=pv_prognose_wh,
|
||||||
strompreis_euro_pro_wh=strompreis_euro_pro_wh,
|
strompreis_euro_pro_wh=strompreis_euro_pro_wh,
|
||||||
einspeiseverguetung_euro_pro_wh=einspeiseverguetung_euro_pro_wh,
|
einspeiseverguetung_euro_pro_wh=einspeiseverguetung_euro_pro_wh,
|
||||||
|
@ -4,7 +4,8 @@ import pytest
|
|||||||
from akkudoktoreos.class_akku import PVAkku
|
from akkudoktoreos.class_akku import PVAkku
|
||||||
from akkudoktoreos.class_ems import EnergieManagementSystem
|
from akkudoktoreos.class_ems import EnergieManagementSystem
|
||||||
from akkudoktoreos.class_haushaltsgeraet import Haushaltsgeraet
|
from akkudoktoreos.class_haushaltsgeraet import Haushaltsgeraet
|
||||||
from akkudoktoreos.class_inverter import Wechselrichter # Example import
|
from akkudoktoreos.class_inverter import Wechselrichter
|
||||||
|
from akkudoktoreos.config import AppConfig
|
||||||
|
|
||||||
prediction_hours = 48
|
prediction_hours = 48
|
||||||
optimization_hours = 24
|
optimization_hours = 24
|
||||||
@ -13,7 +14,7 @@ start_hour = 0
|
|||||||
|
|
||||||
# Example initialization of necessary components
|
# Example initialization of necessary components
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def create_ems_instance():
|
def create_ems_instance(tmp_config: AppConfig) -> EnergieManagementSystem:
|
||||||
"""Fixture to create an EnergieManagementSystem instance with given test parameters."""
|
"""Fixture to create an EnergieManagementSystem instance with given test parameters."""
|
||||||
# Initialize the battery and the inverter
|
# Initialize the battery and the inverter
|
||||||
akku = PVAkku(kapazitaet_wh=5000, start_soc_prozent=80, hours=48, min_soc_prozent=10)
|
akku = PVAkku(kapazitaet_wh=5000, start_soc_prozent=80, hours=48, min_soc_prozent=10)
|
||||||
@ -96,6 +97,7 @@ def create_ems_instance():
|
|||||||
|
|
||||||
# Initialize the energy management system with the respective parameters
|
# Initialize the energy management system with the respective parameters
|
||||||
ems = EnergieManagementSystem(
|
ems = EnergieManagementSystem(
|
||||||
|
config=tmp_config.eos,
|
||||||
pv_prognose_wh=pv_prognose_wh,
|
pv_prognose_wh=pv_prognose_wh,
|
||||||
strompreis_euro_pro_wh=strompreis_euro_pro_wh,
|
strompreis_euro_pro_wh=strompreis_euro_pro_wh,
|
||||||
einspeiseverguetung_euro_pro_wh=einspeiseverguetung_euro_pro_wh,
|
einspeiseverguetung_euro_pro_wh=einspeiseverguetung_euro_pro_wh,
|
||||||
|
@ -6,6 +6,7 @@ from unittest.mock import patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from akkudoktoreos.class_optimize import optimization_problem
|
from akkudoktoreos.class_optimize import optimization_problem
|
||||||
|
from akkudoktoreos.config import AppConfig
|
||||||
|
|
||||||
DIR_TESTDATA = Path(__file__).parent / "testdata"
|
DIR_TESTDATA = Path(__file__).parent / "testdata"
|
||||||
|
|
||||||
@ -33,17 +34,25 @@ def compare_dict(actual: dict[str, Any], expected: dict[str, Any]):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
@patch("akkudoktoreos.class_optimize.visualisiere_ergebnisse")
|
@patch("akkudoktoreos.class_optimize.visualisiere_ergebnisse")
|
||||||
def test_optimize(visualisiere_ergebnisse_patch, fn_in: str, fn_out: str, ngen: int, is_full_run):
|
def test_optimize(
|
||||||
|
visualisiere_ergebnisse_patch,
|
||||||
|
fn_in: str,
|
||||||
|
fn_out: str,
|
||||||
|
ngen: int,
|
||||||
|
is_full_run: bool,
|
||||||
|
tmp_config: AppConfig,
|
||||||
|
):
|
||||||
|
"""Test optimierung_ems."""
|
||||||
# Load input and output data
|
# Load input and output data
|
||||||
with open(DIR_TESTDATA / fn_in, "r") as f_in:
|
file = DIR_TESTDATA / fn_in
|
||||||
|
with file.open("r") as f_in:
|
||||||
input_data = json.load(f_in)
|
input_data = json.load(f_in)
|
||||||
|
|
||||||
with open(DIR_TESTDATA / fn_out, "r") as f_out:
|
file = DIR_TESTDATA / fn_out
|
||||||
|
with file.open("r") as f_out:
|
||||||
expected_output_data = json.load(f_out)
|
expected_output_data = json.load(f_out)
|
||||||
|
|
||||||
opt_class = optimization_problem(
|
opt_class = optimization_problem(tmp_config, fixed_seed=42)
|
||||||
prediction_hours=48, strafe=10, optimization_hours=24, fixed_seed=42
|
|
||||||
)
|
|
||||||
start_hour = 10
|
start_hour = 10
|
||||||
|
|
||||||
if ngen > 10 and not is_full_run:
|
if ngen > 10 and not is_full_run:
|
||||||
|
71
tests/test_config.py
Normal file
71
tests/test_config.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from akkudoktoreos.config import (
|
||||||
|
CONFIG_FILE_NAME,
|
||||||
|
DEFAULT_CONFIG_FILE,
|
||||||
|
get_config_file,
|
||||||
|
load_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_config() -> None:
|
||||||
|
"""Test the default config file."""
|
||||||
|
try:
|
||||||
|
load_config(Path.cwd())
|
||||||
|
except ValidationError as exc:
|
||||||
|
pytest.fail(f"Default configuration is not valid: {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_copy(tmp_path: Path) -> None:
|
||||||
|
"""Test if the config is copied to the provided path."""
|
||||||
|
assert DEFAULT_CONFIG_FILE == get_config_file(Path("does", "not", "exist"), False)
|
||||||
|
|
||||||
|
load_config(tmp_path, True)
|
||||||
|
expected_config = tmp_path.joinpath(CONFIG_FILE_NAME)
|
||||||
|
|
||||||
|
assert expected_config == get_config_file(tmp_path, False)
|
||||||
|
assert expected_config.is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_merge(tmp_path: Path) -> None:
|
||||||
|
"""Test if config is merged and updated correctly."""
|
||||||
|
config_file = tmp_path.joinpath(CONFIG_FILE_NAME)
|
||||||
|
custom_config = {
|
||||||
|
"eos": {
|
||||||
|
"optimization_hours": 30,
|
||||||
|
"penalty": 21,
|
||||||
|
"does_not_exist": "nope",
|
||||||
|
"available_charging_rates_in_percentage": "False entry",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
with config_file.open("w") as f_out:
|
||||||
|
json.dump(custom_config, f_out)
|
||||||
|
|
||||||
|
assert config_file.exists()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
# custom configuration is broken but not updated.
|
||||||
|
load_config(tmp_path, tmp_path, False)
|
||||||
|
|
||||||
|
with config_file.open("r") as f_in:
|
||||||
|
# custom configuration is not changed.
|
||||||
|
assert json.load(f_in) == custom_config
|
||||||
|
|
||||||
|
config = load_config(tmp_path)
|
||||||
|
|
||||||
|
assert config.eos.optimization_hours == 30
|
||||||
|
assert config.eos.penalty == 21
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup(tmp_path: Path) -> None:
|
||||||
|
"""Test setup."""
|
||||||
|
config = load_config(tmp_path, True)
|
||||||
|
config.run_setup()
|
||||||
|
|
||||||
|
assert tmp_path.joinpath(CONFIG_FILE_NAME).is_file()
|
||||||
|
assert tmp_path.joinpath(config.directories.cache).is_dir()
|
||||||
|
assert tmp_path.joinpath(config.directories.output).is_dir()
|
@ -1,10 +1,24 @@
|
|||||||
|
from http import HTTPStatus
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from akkudoktoreos.config import prediction_hours
|
from akkudoktoreos.config import CONFIG_FILE_NAME, load_config
|
||||||
|
|
||||||
|
|
||||||
def test_server(server):
|
def test_fixture_setup(server, tmp_path: Path) -> None:
|
||||||
|
"""Test if the fixture sets up the server with the env var."""
|
||||||
|
# validate correct path in server
|
||||||
|
config = load_config(tmp_path, False)
|
||||||
|
assert tmp_path.joinpath(CONFIG_FILE_NAME).is_file()
|
||||||
|
cache = tmp_path / config.directories.cache
|
||||||
|
assert cache.is_dir()
|
||||||
|
|
||||||
|
|
||||||
|
def test_server(server, tmp_path: Path):
|
||||||
"""Test the server."""
|
"""Test the server."""
|
||||||
result = requests.get(f"{server}/gesamtlast_simple?year_energy=2000&")
|
result = requests.get(f"{server}/gesamtlast_simple?year_energy=2000&")
|
||||||
assert result.status_code == 200
|
assert result.status_code == HTTPStatus.OK
|
||||||
assert len(result.json()) == prediction_hours
|
|
||||||
|
config = load_config(tmp_path, False)
|
||||||
|
assert len(result.json()) == config.eos.prediction_hours
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from matplotlib.testing.compare import compare_images
|
from matplotlib.testing.compare import compare_images
|
||||||
|
|
||||||
|
from akkudoktoreos.config import AppConfig
|
||||||
from akkudoktoreos.visualize import visualisiere_ergebnisse
|
from akkudoktoreos.visualize import visualisiere_ergebnisse
|
||||||
|
|
||||||
DIR_TESTDATA = Path(__file__).parent / "testdata"
|
DIR_TESTDATA = Path(__file__).parent / "testdata"
|
||||||
@ -15,14 +15,16 @@ DIR_IMAGEDATA = DIR_TESTDATA / "images"
|
|||||||
"fn_in, fn_out, fn_out_base",
|
"fn_in, fn_out, fn_out_base",
|
||||||
[("visualize_input_1.json", "visualize_output_1.pdf", "visualize_base_output_1.pdf")],
|
[("visualize_input_1.json", "visualize_output_1.pdf", "visualize_base_output_1.pdf")],
|
||||||
)
|
)
|
||||||
@patch("akkudoktoreos.visualize.output_dir", DIR_IMAGEDATA)
|
def test_visualisiere_ergebnisse(fn_in, fn_out, fn_out_base, tmp_config: AppConfig):
|
||||||
def test_visualisiere_ergebnisse(fn_in, fn_out, fn_out_base):
|
|
||||||
with open(DIR_TESTDATA / fn_in, "r") as f:
|
with open(DIR_TESTDATA / fn_in, "r") as f:
|
||||||
input_data = json.load(f)
|
input_data = json.load(f)
|
||||||
visualisiere_ergebnisse(**input_data)
|
visualisiere_ergebnisse(config=tmp_config, **input_data)
|
||||||
|
output_file: Path = tmp_config.working_dir / tmp_config.directories.output / fn_out
|
||||||
|
|
||||||
|
assert output_file.is_file()
|
||||||
assert (
|
assert (
|
||||||
compare_images(
|
compare_images(
|
||||||
str(DIR_IMAGEDATA / fn_out),
|
str(output_file),
|
||||||
str(DIR_IMAGEDATA / fn_out_base),
|
str(DIR_IMAGEDATA / fn_out_base),
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
148
tests/testdata/visualize_input_1.json
vendored
148
tests/testdata/visualize_input_1.json
vendored
@ -1,62 +1,174 @@
|
|||||||
{
|
{
|
||||||
"gesamtlast": [
|
"gesamtlast": [
|
||||||
676.71, 876.19, 527.13, 468.88, 531.38, 517.95, 483.15, 472.28, 1011.68, 995.0, 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
|
676.71, 876.19, 527.13, 468.88, 531.38, 517.95, 483.15, 472.28, 1011.68,
|
||||||
|
995.0, 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": [
|
"pv_forecast": [
|
||||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 5000.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, 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, 5000.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, 0.0, 0.0
|
||||||
],
|
],
|
||||||
"strompreise": [
|
"strompreise": [
|
||||||
0.00001, 0.00001, 0.00001, 0.00001, 0.00001, 0.00001, 0.00001, 0.00001, 0.00001, 0.00001, 0.001, 0.00005, 0.00005, 0.00005, 0.00005, 0.001, 0.001, 0.001, 0.001, 0.001, 0.00001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001
|
0.00001, 0.00001, 0.00001, 0.00001, 0.00001, 0.00001, 0.00001, 0.00001,
|
||||||
|
0.00001, 0.00001, 0.001, 0.00005, 0.00005, 0.00005, 0.00005, 0.001, 0.001,
|
||||||
|
0.001, 0.001, 0.001, 0.00001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001,
|
||||||
|
0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001,
|
||||||
|
0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001
|
||||||
],
|
],
|
||||||
"ergebnisse": {
|
"ergebnisse": {
|
||||||
"Last_Wh_pro_Stunde": [
|
"Last_Wh_pro_Stunde": [
|
||||||
12493.71, 10502.19, 12775.13, 15356.88, 11468.38, 4037.95, 6047.15, 3112.2799999999997, 3211.68, 995.0, 1053.07, 1063.91, 1320.56, 1132.03, 1163.67, 1176.82, 1216.22, 1103.78, 1129.12, 1178.71, 1050.98, 988.56, 2035.7436363636361, 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
|
12493.71, 10502.19, 12775.13, 15356.88, 11468.38, 4037.95, 6047.15,
|
||||||
|
3112.2799999999997, 3211.68, 995.0, 1053.07, 1063.91, 1320.56, 1132.03,
|
||||||
|
1163.67, 1176.82, 1216.22, 1103.78, 1129.12, 1178.71, 1050.98, 988.56,
|
||||||
|
2035.7436363636361, 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": [
|
"Netzeinspeisung_Wh_pro_Stunde": [
|
||||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3679.44, 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.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, 3679.44, 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.0, 0.0
|
||||||
],
|
],
|
||||||
"Netzbezug_Wh_pro_Stunde": [
|
"Netzbezug_Wh_pro_Stunde": [
|
||||||
12493.71, 10502.19, 12775.13, 15356.88, 11468.38, 4037.95, 6047.15, 3112.2799999999997, 3211.68, 995.0, 1053.07, 1063.91, 0.0, 1132.03, 1163.67, 1176.82, 1216.22, 1103.78, 1129.12, 1178.71, 1050.98, 0.0, 2035.7436363636361, 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, 0.0, 1221.04, 0.0, 0.0, 0.0, 592.97
|
12493.71, 10502.19, 12775.13, 15356.88, 11468.38, 4037.95, 6047.15,
|
||||||
|
3112.2799999999997, 3211.68, 995.0, 1053.07, 1063.91, 0.0, 1132.03,
|
||||||
|
1163.67, 1176.82, 1216.22, 1103.78, 1129.12, 1178.71, 1050.98, 0.0,
|
||||||
|
2035.7436363636361, 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, 0.0, 1221.04, 0.0, 0.0, 0.0,
|
||||||
|
592.97
|
||||||
],
|
],
|
||||||
"Kosten_Euro_pro_Stunde": [
|
"Kosten_Euro_pro_Stunde": [
|
||||||
0.1249371, 0.10502190000000002, 0.1277513, 0.1535688, 0.1146838, 0.0403795, 0.060471500000000004, 0.0311228, 0.0321168, 0.00995, 1.05307, 0.05319550000000001, 0.0, 0.0566015, 0.058183500000000006, 1.17682, 1.21622, 1.10378, 1.12912, 1.1787100000000001, 0.010509800000000001, 0.0, 2.035743636363636, 0.7046100000000001, 0.51637, 0.86805, 0.6943400000000001, 0.6087899999999999, 0.55631, 0.48889, 0.5069100000000001, 0.80489, 1.14198, 1.05697, 0.99246, 1.15599, 0.82701, 1.25798, 1.2326700000000002, 0.87126, 0.86088, 1.15803, 0.0, 1.22104, 0.0, 0.0, 0.0, 0.59297
|
0.1249371, 0.10502190000000002, 0.1277513, 0.1535688, 0.1146838,
|
||||||
|
0.0403795, 0.060471500000000004, 0.0311228, 0.0321168, 0.00995, 1.05307,
|
||||||
|
0.05319550000000001, 0.0, 0.0566015, 0.058183500000000006, 1.17682,
|
||||||
|
1.21622, 1.10378, 1.12912, 1.1787100000000001, 0.010509800000000001, 0.0,
|
||||||
|
2.035743636363636, 0.7046100000000001, 0.51637, 0.86805,
|
||||||
|
0.6943400000000001, 0.6087899999999999, 0.55631, 0.48889,
|
||||||
|
0.5069100000000001, 0.80489, 1.14198, 1.05697, 0.99246, 1.15599, 0.82701,
|
||||||
|
1.25798, 1.2326700000000002, 0.87126, 0.86088, 1.15803, 0.0, 1.22104, 0.0,
|
||||||
|
0.0, 0.0, 0.59297
|
||||||
],
|
],
|
||||||
"akku_soc_pro_stunde": [
|
"akku_soc_pro_stunde": [
|
||||||
25.0, 31.666666666666664, 38.333333333333336, 55.00000000000001, 61.66666666666667, 75.0, 81.66666666666667, 91.66666666666666, 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, 95.7448347107438, 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, 94.73691460055097, 94.73691460055097, 90.64777031680441, 86.39927685950414, 83.23988464187329, 83.23988464187329
|
25.0, 31.666666666666664, 38.333333333333336, 55.00000000000001,
|
||||||
|
61.66666666666667, 75.0, 81.66666666666667, 91.66666666666666, 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, 95.7448347107438, 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, 94.73691460055097, 94.73691460055097,
|
||||||
|
90.64777031680441, 86.39927685950414, 83.23988464187329, 83.23988464187329
|
||||||
],
|
],
|
||||||
"Einnahmen_Euro_pro_Stunde": [
|
"Einnahmen_Euro_pro_Stunde": [
|
||||||
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.2575608, 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.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.2575608,
|
||||||
|
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.0, 0.0
|
||||||
],
|
],
|
||||||
"Gesamtbilanz_Euro": 27.732796636363638,
|
"Gesamtbilanz_Euro": 27.732796636363638,
|
||||||
"E-Auto_SoC_pro_Stunde": [
|
"E-Auto_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, 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
|
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,
|
||||||
|
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.2575608,
|
"Gesamteinnahmen_Euro": 0.2575608,
|
||||||
"Gesamtkosten_Euro": 27.990357436363638,
|
"Gesamtkosten_Euro": 27.990357436363638,
|
||||||
"Verluste_Pro_Stunde": [
|
"Verluste_Pro_Stunde": [
|
||||||
843.0, 654.0, 792.0, 1152.0, 723.0, 480.0, 440.2105263157896, 360.0, 300.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, 134.80363636363631, 153.18595041322305, 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, 166.73454545454547, 0.0, 129.54409090909098, 134.59227272727276, 100.08954545454549, 0.0
|
843.0, 654.0, 792.0, 1152.0, 723.0, 480.0, 440.2105263157896, 360.0,
|
||||||
|
300.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,
|
||||||
|
134.80363636363631, 153.18595041322305, 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,
|
||||||
|
166.73454545454547, 0.0, 129.54409090909098, 134.59227272727276,
|
||||||
|
100.08954545454549, 0.0
|
||||||
],
|
],
|
||||||
"Gesamt_Verluste": 6563.160567638104,
|
"Gesamt_Verluste": 6563.160567638104,
|
||||||
"Haushaltsgeraet_wh_pro_stunde": [
|
"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, 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,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ac": [
|
"ac": [
|
||||||
0.6, 0.4, 0.4, 1.0, 0.4, 0.8, 0.4, 0.6, 1.0, 0.2, 0.2, 0.2, 0.6, 0.0, 0.2, 0.6, 0.0, 0.0, 0.8, 0.8, 0.4, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.2, 0.8, 0.2, 0.4, 0.6, 1.0, 0.0, 1.0, 0.8, 0.4, 0.4, 1.0, 0.2, 0.6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
|
0.6, 0.4, 0.4, 1.0, 0.4, 0.8, 0.4, 0.6, 1.0, 0.2, 0.2, 0.2, 0.6, 0.0, 0.2,
|
||||||
|
0.6, 0.0, 0.0, 0.8, 0.8, 0.4, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.2, 0.8,
|
||||||
|
0.2, 0.4, 0.6, 1.0, 0.0, 1.0, 0.8, 0.4, 0.4, 1.0, 0.2, 0.6, 0.0, 0.0, 0.0,
|
||||||
|
0.0, 0.0, 0.0
|
||||||
],
|
],
|
||||||
"dc": [
|
"dc": [
|
||||||
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, 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, 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, 1, 1, 1, 1, 1, 1
|
||||||
],
|
],
|
||||||
"discharge": [
|
"discharge": [
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0
|
||||||
],
|
],
|
||||||
"temperature": [
|
"temperature": [
|
||||||
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
|
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
|
||||||
],
|
],
|
||||||
"start_hour": 0,
|
"start_hour": 0,
|
||||||
"prediction_hours": 48,
|
|
||||||
"einspeiseverguetung_euro_pro_wh": [
|
"einspeiseverguetung_euro_pro_wh": [
|
||||||
0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007
|
0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007,
|
||||||
|
0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007,
|
||||||
|
0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007,
|
||||||
|
0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007,
|
||||||
|
0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007,
|
||||||
|
0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007, 0.00007
|
||||||
],
|
],
|
||||||
"filename": "visualize_output_1.pdf",
|
"filename": "visualize_output_1.pdf",
|
||||||
"extra_data": null
|
"extra_data": null
|
||||||
|
Loading…
x
Reference in New Issue
Block a user