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/
|
||||
output/
|
||||
EOS.config.json
|
||||
|
||||
# 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
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
On Linux (Ubuntu/Debian):
|
||||
@ -27,8 +46,7 @@ On MacOS (requires [Homebrew](https://brew.sh)):
|
||||
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 be started with `make run`. A full overview of the main shortcuts is given by `make help`.
|
||||
|
||||
### Detailed Instructions
|
||||
|
||||
@ -65,7 +83,6 @@ source .venv/bin/activate
|
||||
|
||||
## Usage
|
||||
|
||||
Adjust `config.py`.
|
||||
To use the system, run `flask_server.py`, which starts the server:
|
||||
|
||||
```bash
|
||||
|
@ -17,6 +17,6 @@ services:
|
||||
networks:
|
||||
- "eos"
|
||||
volumes:
|
||||
- ./src/akkudoktoreos/config.py:/opt/eos/akkudoktoreos/config.py:ro
|
||||
- ./src/akkudoktoreos/default.config.json:/opt/eos/EOS.config.json:ro
|
||||
ports:
|
||||
- "${EOS_PORT}:${EOS_PORT}"
|
||||
|
@ -23,7 +23,6 @@ On MacOS (requires [Homebrew](https://brew.sh)):
|
||||
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`.
|
||||
|
||||
### Detailed Instructions
|
||||
@ -52,6 +51,7 @@ To always use the Python version from the virtual environment, you should activa
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
(for Bash users, the default under Linux) or
|
||||
|
||||
```zsh
|
||||
@ -60,7 +60,29 @@ source .venv/bin/activate
|
||||
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
|
@ -31,6 +31,7 @@ where = ["src/"]
|
||||
include = ["akkudoktoreos", "akkudoktoreosserver", ]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
akkudoktoreos = ["*.json", ]
|
||||
akkudoktoreosserver = ["data/*.npz", ]
|
||||
|
||||
[tool.pyright]
|
||||
|
@ -5,9 +5,8 @@ import time
|
||||
import numpy as np
|
||||
|
||||
from akkudoktoreos.class_numpy_encoder import NumpyEncoder
|
||||
|
||||
# Import necessary modules from the project
|
||||
from akkudoktoreos.class_optimize import optimization_problem
|
||||
from akkudoktoreos.config import get_working_dir, load_config
|
||||
from akkudoktoreos.visualize import visualisiere_ergebnisse
|
||||
|
||||
start_hour = 0
|
||||
@ -276,10 +275,10 @@ parameter = {
|
||||
# Startzeit nehmen
|
||||
start_time = time.time()
|
||||
|
||||
# Initialize the optimization problem
|
||||
opt_class = optimization_problem(
|
||||
prediction_hours=48, strafe=10, optimization_hours=24, verbose=True, fixed_seed=42
|
||||
)
|
||||
# Initialize the optimization problem using the default configuration
|
||||
working_dir = get_working_dir()
|
||||
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)
|
||||
@ -299,17 +298,19 @@ ac_charge, dc_charge, discharge = (
|
||||
)
|
||||
|
||||
visualisiere_ergebnisse(
|
||||
gesamtlast,
|
||||
pv_forecast,
|
||||
strompreis_euro_pro_wh,
|
||||
ergebnis["result"],
|
||||
ac_charge,
|
||||
dc_charge,
|
||||
discharge,
|
||||
temperature_forecast,
|
||||
start_hour,
|
||||
48,
|
||||
np.full(48, parameter["einspeiseverguetung_euro_pro_wh"]),
|
||||
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,
|
||||
einspeiseverguetung_euro_pro_wh=np.full(
|
||||
config.eos.feed_in_tariff_eur_per_wh, parameter["einspeiseverguetung_euro_pro_wh"]
|
||||
),
|
||||
config=config,
|
||||
filename="visualization_results.pdf",
|
||||
extra_data=None,
|
||||
)
|
||||
|
@ -3,12 +3,13 @@ from typing import Dict, List, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from akkudoktoreos.config import prediction_hours
|
||||
from akkudoktoreos.config import EOSConfig
|
||||
|
||||
|
||||
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,
|
||||
@ -25,9 +26,9 @@ class EnergieManagementSystem:
|
||||
self.eauto = eauto
|
||||
self.haushaltsgeraet = haushaltsgeraet
|
||||
self.wechselrichter = wechselrichter
|
||||
self.ac_charge_hours = np.full(prediction_hours, 0)
|
||||
self.dc_charge_hours = np.full(prediction_hours, 1)
|
||||
self.ev_charge_hours = np.full(prediction_hours, 0)
|
||||
self.ac_charge_hours = np.full(config.prediction_hours, 0)
|
||||
self.dc_charge_hours = np.full(config.prediction_hours, 1)
|
||||
self.ev_charge_hours = np.full(config.prediction_hours, 0)
|
||||
|
||||
def set_akku_discharge_hours(self, ds: List[int]) -> None:
|
||||
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_haushaltsgeraet import Haushaltsgeraet
|
||||
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
|
||||
|
||||
|
||||
class optimization_problem:
|
||||
def __init__(
|
||||
self,
|
||||
prediction_hours: int = 48,
|
||||
strafe: float = 10,
|
||||
optimization_hours: int = 24,
|
||||
config: AppConfig,
|
||||
verbose: bool = False,
|
||||
fixed_seed: Optional[int] = None,
|
||||
):
|
||||
"""Initialize the optimization problem with the required parameters."""
|
||||
self.prediction_hours = prediction_hours
|
||||
self.strafe = strafe
|
||||
self._config = config
|
||||
self.prediction_hours = config.eos.prediction_hours
|
||||
self.strafe = config.eos.penalty
|
||||
self.opti_param = None
|
||||
self.fixed_eauto_hours = prediction_hours - optimization_hours
|
||||
self.possible_charge_values = possible_ev_charge_currents
|
||||
self.fixed_eauto_hours = config.eos.prediction_hours - config.eos.optimization_hours
|
||||
self.possible_charge_values = config.eos.available_charging_rates_in_percentage
|
||||
self.verbose = verbose
|
||||
self.fix_seed = fixed_seed
|
||||
self.optimize_ev = True
|
||||
@ -210,7 +209,10 @@ class optimization_problem:
|
||||
|
||||
if self.optimize_ev:
|
||||
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)
|
||||
|
||||
@ -236,7 +238,7 @@ class optimization_problem:
|
||||
"mutate_ev_charge_index",
|
||||
tools.mutUniformInt,
|
||||
low=0,
|
||||
up=len(possible_ev_charge_currents) - 1,
|
||||
up=len(self._config.eos.available_charging_rates_in_percentage) - 1,
|
||||
indpb=0.2,
|
||||
)
|
||||
# - Start hour mutation for household devices
|
||||
@ -271,7 +273,8 @@ class optimization_problem:
|
||||
|
||||
if self.optimize_ev:
|
||||
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)
|
||||
else:
|
||||
@ -420,6 +423,7 @@ class optimization_problem:
|
||||
# Initialize the inverter and energy management system
|
||||
wr = Wechselrichter(10000, 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"],
|
||||
@ -444,23 +448,24 @@ class optimization_problem:
|
||||
)
|
||||
if self.optimize_ev:
|
||||
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)
|
||||
# Visualize the results
|
||||
visualisiere_ergebnisse(
|
||||
parameter["gesamtlast"],
|
||||
parameter["pv_forecast"],
|
||||
parameter["strompreis_euro_pro_wh"],
|
||||
o,
|
||||
ac_charge,
|
||||
dc_charge,
|
||||
discharge,
|
||||
parameter["temperature_forecast"],
|
||||
start_hour,
|
||||
self.prediction_hours,
|
||||
einspeiseverguetung_euro_pro_wh,
|
||||
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,
|
||||
config=self._config,
|
||||
extra_data=extra_data,
|
||||
)
|
||||
|
||||
|
@ -1,12 +1,14 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import zoneinfo
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
|
||||
from akkudoktoreos.config import AppConfig, SetupIncomplete
|
||||
|
||||
|
||||
def repeat_to_shape(array, target_shape):
|
||||
# Check if the array fits the target shape
|
||||
@ -23,53 +25,57 @@ def repeat_to_shape(array, target_shape):
|
||||
|
||||
class HourlyElectricityPriceForecast:
|
||||
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
|
||||
self.cache_dir = cache_dir
|
||||
self.cache = cache
|
||||
os.makedirs(self.cache_dir, exist_ok=True)
|
||||
self.cache_time_file = os.path.join(self.cache_dir, "cache_timestamp.txt")
|
||||
self.cache_dir = config.working_dir / config.directories.cache
|
||||
self.use_cache = use_cache
|
||||
if not self.cache_dir.is_dir():
|
||||
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.charges = charges
|
||||
self.prediction_hours = prediction_hours
|
||||
self.prediction_hours = config.eos.prediction_hours
|
||||
|
||||
def load_data(self, source):
|
||||
cache_filename = self.get_cache_filename(source)
|
||||
if source.startswith("http"):
|
||||
if os.path.exists(cache_filename) and not self.is_cache_expired() and self.cache:
|
||||
def load_data(self, source: str | Path):
|
||||
cache_file = self.get_cache_file(source)
|
||||
if isinstance(source, str):
|
||||
if cache_file.is_file() and not self.is_cache_expired() and self.use_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)
|
||||
else:
|
||||
print("Loading data from the URL...")
|
||||
response = requests.get(source)
|
||||
if response.status_code == 200:
|
||||
json_data = response.json()
|
||||
with open(cache_filename, "w") as file:
|
||||
with cache_file.open("w") as file:
|
||||
json.dump(json_data, file)
|
||||
self.update_cache_timestamp()
|
||||
else:
|
||||
raise Exception(f"Error fetching data: {response.status_code}")
|
||||
else:
|
||||
with open(source, "r") as file:
|
||||
elif source.is_file():
|
||||
with source.open("r") as file:
|
||||
json_data = json.load(file)
|
||||
else:
|
||||
raise ValueError(f"Input is not a valid path: {source}")
|
||||
return json_data["values"]
|
||||
|
||||
def get_cache_filename(self, url):
|
||||
def get_cache_file(self, url):
|
||||
hash_object = hashlib.sha256(url.encode())
|
||||
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):
|
||||
if not os.path.exists(self.cache_time_file):
|
||||
if not self.cache_time_file.is_file():
|
||||
return True
|
||||
with open(self.cache_time_file, "r") as file:
|
||||
with self.cache_time_file.open("r") as file:
|
||||
timestamp_str = file.read()
|
||||
last_cache_time = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S")
|
||||
return datetime.now() - last_cache_time > timedelta(hours=1)
|
||||
|
||||
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"))
|
||||
|
||||
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 pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
output_dir = "output"
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
prediction_hours = 48
|
||||
optimization_hours = 24
|
||||
strafe = 10
|
||||
possible_ev_charge_currents = [
|
||||
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,
|
||||
]
|
||||
EOS_DIR = "EOS_DIR"
|
||||
ENCODING = "UTF-8"
|
||||
CONFIG_FILE_NAME = "EOS.config.json"
|
||||
DEFAULT_CONFIG_FILE = Path(__file__).parent.joinpath("default.config.json")
|
||||
|
||||
|
||||
def get_start_enddate(prediction_hours=48, startdate=None):
|
||||
############
|
||||
# Parameter
|
||||
############
|
||||
class FolderConfig(BaseModel):
|
||||
"""Folder configuration for the EOS system.
|
||||
|
||||
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:
|
||||
date = (datetime.now().date() + timedelta(hours=prediction_hours)).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 os
|
||||
|
||||
# Set the backend for matplotlib to Agg
|
||||
import matplotlib
|
||||
@ -8,7 +7,7 @@ import numpy as np
|
||||
from matplotlib.backends.backend_pdf import PdfPages
|
||||
|
||||
from akkudoktoreos.class_sommerzeit import ist_dst_wechsel
|
||||
from akkudoktoreos.config import output_dir
|
||||
from akkudoktoreos.config import AppConfig, SetupIncomplete
|
||||
|
||||
matplotlib.use("Agg")
|
||||
|
||||
@ -23,22 +22,24 @@ def visualisiere_ergebnisse(
|
||||
discharge, # Discharge allowed
|
||||
temperature,
|
||||
start_hour,
|
||||
prediction_hours,
|
||||
einspeiseverguetung_euro_pro_wh,
|
||||
config: AppConfig,
|
||||
filename="visualization_results.pdf",
|
||||
extra_data=None,
|
||||
):
|
||||
#####################
|
||||
# 24-hour visualization
|
||||
#####################
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
output_file = os.path.join(output_dir, filename)
|
||||
output_dir = config.working_dir / config.directories.output
|
||||
if not output_dir.is_dir():
|
||||
raise SetupIncomplete(f"Output path does not exist: {output_dir}.")
|
||||
|
||||
output_file = output_dir.joinpath(filename)
|
||||
with PdfPages(output_file) as pdf:
|
||||
# Load and PV generation
|
||||
plt.figure(figsize=(14, 14))
|
||||
plt.subplot(3, 3, 1)
|
||||
hours = np.arange(0, prediction_hours)
|
||||
hours = np.arange(0, config.eos.prediction_hours)
|
||||
|
||||
gesamtlast_array = np.array(gesamtlast)
|
||||
# Plot individual loads
|
||||
@ -101,9 +102,9 @@ def visualisiere_ergebnisse(
|
||||
plt.figure(figsize=(14, 10))
|
||||
|
||||
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:
|
||||
hours = np.arange(start_hour, prediction_hours)
|
||||
hours = np.arange(start_hour, config.eos.prediction_hours)
|
||||
|
||||
# Energy flow, grid feed-in, and grid consumption
|
||||
plt.subplot(3, 2, 1)
|
||||
@ -187,7 +188,7 @@ def visualisiere_ergebnisse(
|
||||
|
||||
# Plot for AC, DC charging, and Discharge status using bar charts
|
||||
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)
|
||||
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
|
||||
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_ylabel("Relative Power (0-1) / Discharge (0 or 1)")
|
||||
ax1.set_title("AC/DC Charging and Discharge Overview")
|
||||
ax1.grid(True)
|
||||
|
||||
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:
|
||||
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
|
||||
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_strompreis import HourlyElectricityPriceForecast
|
||||
from akkudoktoreos.config import (
|
||||
SetupIncomplete,
|
||||
get_start_enddate,
|
||||
optimization_hours,
|
||||
output_dir,
|
||||
prediction_hours,
|
||||
get_working_dir,
|
||||
load_config,
|
||||
)
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
opt_class = optimization_problem(
|
||||
prediction_hours=prediction_hours,
|
||||
strafe=10,
|
||||
optimization_hours=optimization_hours,
|
||||
verbose=True,
|
||||
)
|
||||
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]:
|
||||
@ -58,14 +56,11 @@ def isfloat(num: Any) -> TypeGuard[float]:
|
||||
@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(prediction_hours, startdate=datetime.now().date())
|
||||
filepath = os.path.join(
|
||||
r"test_data", r"strompreise_akkudokAPI.json"
|
||||
) # Adjust the path to the JSON file
|
||||
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}",
|
||||
prediction_hours=prediction_hours,
|
||||
cache=False,
|
||||
config=config.eos.prediction_hours,
|
||||
use_cache=False,
|
||||
)
|
||||
specific_date_prices = price_forecast.get_price_for_daterange(
|
||||
date_now, date
|
||||
@ -141,7 +136,7 @@ def flask_gesamtlast_simple():
|
||||
request.args.get("year_energy")
|
||||
) # Get annual energy value from query parameters
|
||||
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
|
||||
|
||||
###############
|
||||
@ -159,7 +154,9 @@ def flask_gesamtlast_simple():
|
||||
0
|
||||
] # 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(
|
||||
"Haushalt", leistung_haushalt
|
||||
) # Add household load to total load calculation
|
||||
@ -181,13 +178,15 @@ def flask_pvprognose():
|
||||
# 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(prediction_hours, startdate=datetime.now().date())
|
||||
date_now, date = get_start_enddate(
|
||||
config.eos.prediction_hours, startdate=datetime.now().date()
|
||||
)
|
||||
|
||||
###############
|
||||
# PV Forecast
|
||||
###############
|
||||
PVforecast = PVForecast(
|
||||
prediction_hours=prediction_hours, url=url
|
||||
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(
|
||||
@ -255,9 +254,10 @@ def flask_optimize():
|
||||
@app.route("/visualization_results.pdf")
|
||||
def get_pdf():
|
||||
# Endpoint to serve the generated PDF with visualization results
|
||||
return send_from_directory(
|
||||
os.path.abspath(output_dir), "visualization_results.pdf"
|
||||
) # Adjust the directory if needed
|
||||
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")
|
||||
@ -293,6 +293,8 @@ def root():
|
||||
|
||||
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)
|
||||
|
@ -3,10 +3,21 @@ import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
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)
|
||||
def disable_debug_logging():
|
||||
@ -29,7 +40,7 @@ def is_full_run(request):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def server(xprocess):
|
||||
def server(xprocess, tmp_path: Path):
|
||||
"""Fixture to start the server.
|
||||
|
||||
Provides URL of the server.
|
||||
@ -45,8 +56,7 @@ def server(xprocess):
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
test_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
project_dir = os.path.abspath(os.path.join(test_dir, ".."))
|
||||
project_dir = Path(__file__).parent.parent
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "-e", project_dir],
|
||||
check=True,
|
||||
@ -56,14 +66,15 @@ def server(xprocess):
|
||||
|
||||
# command to start server process
|
||||
args = [sys.executable, "-m", "akkudoktoreosserver.flask_server"]
|
||||
env = {EOS_DIR: f"{tmp_path}", **os.environ.copy()}
|
||||
|
||||
# startup pattern
|
||||
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
|
||||
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
|
||||
|
||||
# xprocess will now attempt to clean up upon interruptions
|
||||
|
@ -4,7 +4,8 @@ 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 # Example import
|
||||
from akkudoktoreos.class_inverter import Wechselrichter
|
||||
from akkudoktoreos.config import AppConfig
|
||||
|
||||
prediction_hours = 48
|
||||
optimization_hours = 24
|
||||
@ -13,7 +14,7 @@ start_hour = 1
|
||||
|
||||
# Example initialization of necessary components
|
||||
@pytest.fixture
|
||||
def create_ems_instance():
|
||||
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)
|
||||
@ -189,6 +190,7 @@ def create_ems_instance():
|
||||
|
||||
# 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,
|
||||
|
@ -4,7 +4,8 @@ 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 # Example import
|
||||
from akkudoktoreos.class_inverter import Wechselrichter
|
||||
from akkudoktoreos.config import AppConfig
|
||||
|
||||
prediction_hours = 48
|
||||
optimization_hours = 24
|
||||
@ -13,7 +14,7 @@ start_hour = 0
|
||||
|
||||
# Example initialization of necessary components
|
||||
@pytest.fixture
|
||||
def create_ems_instance():
|
||||
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)
|
||||
@ -96,6 +97,7 @@ def create_ems_instance():
|
||||
|
||||
# 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,
|
||||
|
@ -6,6 +6,7 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from akkudoktoreos.class_optimize import optimization_problem
|
||||
from akkudoktoreos.config import AppConfig
|
||||
|
||||
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")
|
||||
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
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
opt_class = optimization_problem(
|
||||
prediction_hours=48, strafe=10, optimization_hours=24, fixed_seed=42
|
||||
)
|
||||
opt_class = optimization_problem(tmp_config, fixed_seed=42)
|
||||
start_hour = 10
|
||||
|
||||
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
|
||||
|
||||
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."""
|
||||
result = requests.get(f"{server}/gesamtlast_simple?year_energy=2000&")
|
||||
assert result.status_code == 200
|
||||
assert len(result.json()) == prediction_hours
|
||||
assert result.status_code == HTTPStatus.OK
|
||||
|
||||
config = load_config(tmp_path, False)
|
||||
assert len(result.json()) == config.eos.prediction_hours
|
||||
|
@ -1,10 +1,10 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from matplotlib.testing.compare import compare_images
|
||||
|
||||
from akkudoktoreos.config import AppConfig
|
||||
from akkudoktoreos.visualize import visualisiere_ergebnisse
|
||||
|
||||
DIR_TESTDATA = Path(__file__).parent / "testdata"
|
||||
@ -15,14 +15,16 @@ DIR_IMAGEDATA = DIR_TESTDATA / "images"
|
||||
"fn_in, fn_out, fn_out_base",
|
||||
[("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):
|
||||
def test_visualisiere_ergebnisse(fn_in, fn_out, fn_out_base, tmp_config: AppConfig):
|
||||
with open(DIR_TESTDATA / fn_in, "r") as 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 (
|
||||
compare_images(
|
||||
str(DIR_IMAGEDATA / fn_out),
|
||||
str(output_file),
|
||||
str(DIR_IMAGEDATA / fn_out_base),
|
||||
0,
|
||||
)
|
||||
|
148
tests/testdata/visualize_input_1.json
vendored
148
tests/testdata/visualize_input_1.json
vendored
@ -1,62 +1,174 @@
|
||||
{
|
||||
"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": [
|
||||
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": [
|
||||
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": {
|
||||
"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": [
|
||||
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": [
|
||||
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": [
|
||||
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": [
|
||||
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": [
|
||||
0.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,
|
||||
"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,
|
||||
"Gesamtkosten_Euro": 27.990357436363638,
|
||||
"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,
|
||||
"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": [
|
||||
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": [
|
||||
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": [
|
||||
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": [
|
||||
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,
|
||||
"prediction_hours": 48,
|
||||
"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",
|
||||
"extra_data": null
|
||||
|
Loading…
x
Reference in New Issue
Block a user