From 5f898e8aab29be239a31e6b30b2bc2514ff5a485 Mon Sep 17 00:00:00 2001 From: Normann Date: Thu, 19 Dec 2024 14:50:19 +0100 Subject: [PATCH] translation of battery.py v3 (#262) --- README.md | 11 +- docs/akkudoktoreos/openapi.json | 295 ++++++++-------- single_test_optimization.py | 32 +- src/akkudoktoreos/core/ems.py | 22 +- src/akkudoktoreos/devices/battery.py | 370 +++++++++------------ src/akkudoktoreos/devices/devices.py | 36 +- src/akkudoktoreos/devices/inverter.py | 8 +- src/akkudoktoreos/optimization/genetic.py | 36 +- tests/test_battery.py | 219 ++++++++++++ tests/test_class_akku.py | 143 -------- tests/test_class_ems.py | 18 +- tests/test_class_ems_2.py | 18 +- tests/test_inverter.py | 48 +-- tests/testdata/optimize_input_1.json | 24 +- tests/testdata/optimize_input_2.json | 16 +- tests/testdata/optimize_result_1.json | 12 +- tests/testdata/optimize_result_2.json | 12 +- tests/testdata/optimize_result_2_full.json | 12 +- 18 files changed, 684 insertions(+), 648 deletions(-) create mode 100644 tests/test_battery.py delete mode 100644 tests/test_class_akku.py diff --git a/README.md b/README.md index 8af5f4b..3fea67f 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,14 @@ Following sections describe how to locally start the EOS server on `http://local Install dependencies in virtual environment: Linux: + ```bash python -m venv .venv .venv/bin/pip install -r requirements.txt ``` Windows: + ```bash python -m venv .venv .venv\Scripts\pip install -r requirements.txt @@ -31,13 +33,17 @@ python -m venv .venv Finally, start EOS fastapi server: Linux: + ```bash .venv/bin/fastapi run --port 8503 src/akkudoktoreos/server/fastapi_server.py ``` + Windows: + ``` .venv\Scripts\fastapi run --port 8503 src/akkudoktoreos/server/fastapi_server.py ``` + ### Docker ```bash @@ -67,7 +73,7 @@ If the configuration keys in the `EOS.config.json` file are missing or different This project uses various classes to simulate and optimize the components of an energy system. Each class represents a specific aspect of the system, as described below: -- `PVAkku`: Simulates a battery storage system, including capacity, state of charge, and now charge and discharge losses. +- `Battery`: Simulates a battery storage system, including capacity, state of charge, and now charge and discharge losses. - `PVForecast`: Provides forecast data for photovoltaic generation, based on weather data and historical generation data. @@ -89,7 +95,6 @@ Each class is designed to be easily customized and extended to integrate additio See the Swagger API documentation for detailed information: [EOS OpenAPI Spec](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/Akkudoktor-EOS/EOS/refs/heads/main/docs/akkudoktoreos/openapi.json) - ## Further resources - - [Installation guide (de)](https://meintechblog.de/2024/09/05/andreas-schmitz-joerg-installiert-mein-energieoptimierungssystem/) +- [Installation guide (de)](https://meintechblog.de/2024/09/05/andreas-schmitz-joerg-installiert-mein-energieoptimierungssystem/) diff --git a/docs/akkudoktoreos/openapi.json b/docs/akkudoktoreos/openapi.json index 703ecd6..6181ef7 100644 --- a/docs/akkudoktoreos/openapi.json +++ b/docs/akkudoktoreos/openapi.json @@ -2091,7 +2091,7 @@ "title": "Battery Capacity", "description": "Battery capacity [Wh]." }, - "battery_soc_start": { + "battery_initial_soc": { "anyOf": [ { "type": "integer" @@ -2100,7 +2100,7 @@ "type": "null" } ], - "title": "Battery Soc Start", + "title": "Battery Initial Soc", "description": "Battery initial state of charge [%]." }, "battery_soc_min": { @@ -2127,7 +2127,7 @@ "title": "Battery Soc Max", "description": "Battery maximum state of charge [%]." }, - "battery_charge_efficiency": { + "battery_charging_efficiency": { "anyOf": [ { "type": "number" @@ -2136,10 +2136,10 @@ "type": "null" } ], - "title": "Battery Charge Efficiency", + "title": "Battery Charging Efficiency", "description": "Battery charging efficiency [%]." }, - "battery_discharge_efficiency": { + "battery_discharging_efficiency": { "anyOf": [ { "type": "number" @@ -2148,10 +2148,10 @@ "type": "null" } ], - "title": "Battery Discharge Efficiency", + "title": "Battery Discharging Efficiency", "description": "Battery discharging efficiency [%]." }, - "battery_charge_power_max": { + "battery_max_charging_power": { "anyOf": [ { "type": "integer" @@ -2160,7 +2160,7 @@ "type": "null" } ], - "title": "Battery Charge Power Max", + "title": "Battery Max Charging Power", "description": "Battery maximum charge power [W]." }, "bev_provider": { @@ -2187,7 +2187,7 @@ "title": "Bev Capacity", "description": "Battery Electric Vehicle capacity [Wh]." }, - "bev_soc_start": { + "bev_initial_soc": { "anyOf": [ { "type": "integer" @@ -2196,7 +2196,7 @@ "type": "null" } ], - "title": "Bev Soc Start", + "title": "Bev Initial Soc", "description": "Battery Electric Vehicle initial state of charge [%]." }, "bev_soc_max": { @@ -2211,7 +2211,7 @@ "title": "Bev Soc Max", "description": "Battery Electric Vehicle maximum state of charge [%]." }, - "bev_charge_efficiency": { + "bev_charging_efficiency": { "anyOf": [ { "type": "number" @@ -2220,10 +2220,10 @@ "type": "null" } ], - "title": "Bev Charge Efficiency", + "title": "Bev Charging Efficiency", "description": "Battery Electric Vehicle charging efficiency [%]." }, - "bev_discharge_efficiency": { + "bev_discharging_efficiency": { "anyOf": [ { "type": "number" @@ -2232,10 +2232,10 @@ "type": "null" } ], - "title": "Bev Discharge Efficiency", + "title": "Bev Discharging Efficiency", "description": "Battery Electric Vehicle discharging efficiency [%]." }, - "bev_charge_power_max": { + "bev_max_charging_power": { "anyOf": [ { "type": "integer" @@ -2244,7 +2244,7 @@ "type": "null" } ], - "title": "Bev Charge Power Max", + "title": "Bev Max Charging Power", "description": "Battery Electric Vehicle maximum charge power [W]." }, "dishwasher_provider": { @@ -2485,28 +2485,28 @@ "title": "ConfigEOS", "description": "Singleton configuration handler for the EOS application.\n\nConfigEOS extends `SettingsEOS` with support for default configuration paths and automatic\ninitialization.\n\n`ConfigEOS` ensures that only one instance of the class is created throughout the application,\nallowing consistent access to EOS configuration settings. This singleton instance loads\nconfiguration data from a predefined set of directories or creates a default configuration if\nnone is found.\n\nInitialization Process:\n - Upon instantiation, the singleton instance attempts to load a configuration file in this order:\n 1. The directory specified by the `EOS_DIR` environment variable.\n 2. A platform specific default directory for EOS.\n 3. The current working directory.\n - The first available configuration file found in these directories is loaded.\n - If no configuration file is found, a default configuration file is created in the platform\n specific default directory, and default settings are loaded into it.\n\nAttributes from the loaded configuration are accessible directly as instance attributes of\n`ConfigEOS`, providing a centralized, shared configuration object for EOS.\n\nSingleton Behavior:\n - This class uses the `SingletonMixin` to ensure that all requests for `ConfigEOS` return\n the same instance, which contains the most up-to-date configuration. Modifying the configuration\n in one part of the application reflects across all references to this class.\n\nAttributes:\n _settings (ClassVar[SettingsEOS]): Holds application-wide settings.\n _file_settings (ClassVar[SettingsEOS]): Stores configuration loaded from file.\n config_folder_path (Optional[Path]): Path to the configuration directory.\n config_file_path (Optional[Path]): Path to the configuration file.\n\nRaises:\n FileNotFoundError: If no configuration file is found, and creating a default configuration fails.\n\nExample:\n To initialize and access configuration attributes (only one instance is created):\n ```python\n config_eos = ConfigEOS() # Always returns the same instance\n print(config_eos.prediction_hours) # Access a setting from the loaded configuration\n ```" }, - "EAutoParameters": { + "ElectricVehicleParameters": { "properties": { - "kapazitaet_wh": { + "capacity_wh": { "type": "integer", "exclusiveMinimum": 0.0, - "title": "Kapazitaet Wh", + "title": "Capacity Wh", "description": "An integer representing the capacity of the battery in watt-hours." }, - "lade_effizienz": { + "charging_efficiency": { "type": "number", "maximum": 1.0, "exclusiveMinimum": 0.0, - "title": "Lade Effizienz", + "title": "Charging Efficiency", "description": "A float representing the charging efficiency of the battery.", "default": 0.88 }, - "entlade_effizienz": { + "discharging_efficiency": { "type": "number", - "title": "Entlade Effizienz", + "title": "Discharging Efficiency", "default": 1.0 }, - "max_ladeleistung_w": { + "max_charge_power_w": { "anyOf": [ { "type": "number", @@ -2516,38 +2516,41 @@ "type": "null" } ], - "title": "Max Ladeleistung W", - "description": "An integer representing the charging power of the battery in watts." + "title": "Max Charge Power W", + "description": "Maximum charging power in watts.", + "default": 5000 }, - "start_soc_prozent": { + "initial_soc_percentage": { "type": "integer", "maximum": 100.0, "minimum": 0.0, - "title": "Start Soc Prozent", + "title": "Initial Soc Percentage", "description": "An integer representing the current state of charge (SOC) of the battery in percentage.", "default": 0 }, - "min_soc_prozent": { + "min_soc_percentage": { "type": "integer", "maximum": 100.0, "minimum": 0.0, - "title": "Min Soc Prozent", + "title": "Min Soc Percentage", "description": "An integer representing the minimum state of charge (SOC) of the battery in percentage.", "default": 0 }, - "max_soc_prozent": { + "max_soc_percentage": { "type": "integer", "maximum": 100.0, "minimum": 0.0, - "title": "Max Soc Prozent", + "title": "Max Soc Percentage", + "description": "An integer representing the maximum state of charge (SOC) of the battery in percentage.", "default": 100 } }, "type": "object", - "required": ["kapazitaet_wh"], - "title": "EAutoParameters" + "required": ["capacity_wh"], + "title": "ElectricVehicleParameters", + "description": "Parameters specific to an electric vehicle (EV)." }, - "EAutoResult": { + "ElectricVehicleResult": { "properties": { "charge_array": { "items": { @@ -2555,7 +2558,7 @@ }, "type": "array", "title": "Charge Array", - "description": "Indicates for each hour whether the EV is charging (`0` for no charging, `1` for charging)." + "description": "Hourly charging status (0 for no charging, 1 for charging)." }, "discharge_array": { "items": { @@ -2563,58 +2566,58 @@ }, "type": "array", "title": "Discharge Array", - "description": "Indicates for each hour whether the EV is discharging (`0` for no discharging, `1` for discharging)." + "description": "Hourly discharging status (0 for no discharging, 1 for discharging)." }, - "entlade_effizienz": { + "discharging_efficiency": { "type": "number", - "title": "Entlade Effizienz", - "description": "The discharge efficiency as a float." + "title": "Discharging Efficiency", + "description": "The discharge efficiency as a float.." }, "hours": { "type": "integer", "title": "Hours", - "description": "Amount of hours the simulation is done for." + "description": "Number of hours in the simulation." }, - "kapazitaet_wh": { + "capacity_wh": { "type": "integer", - "title": "Kapazitaet Wh", - "description": "The capacity of the EV\u2019s battery in watt-hours." + "title": "Capacity Wh", + "description": "Capacity of the EV\u2019s battery in watt-hours." }, - "lade_effizienz": { + "charging_efficiency": { "type": "number", - "title": "Lade Effizienz", - "description": "The charging efficiency as a float." + "title": "Charging Efficiency", + "description": "Charging efficiency as a float.." }, - "max_ladeleistung_w": { + "max_charge_power_w": { "type": "integer", - "title": "Max Ladeleistung W", - "description": "The maximum charging power of the EV in watts." + "title": "Max Charge Power W", + "description": "Maximum charging power in watts." }, "soc_wh": { "type": "number", "title": "Soc Wh", - "description": "The state of charge of the battery in watt-hours at the start of the simulation." + "description": "State of charge of the battery in watt-hours at the start of the simulation." }, - "start_soc_prozent": { + "initial_soc_percentage": { "type": "integer", - "title": "Start Soc Prozent", - "description": "The state of charge of the battery in percentage at the start of the simulation." + "title": "Initial Soc Percentage", + "description": "State of charge at the start of the simulation in percentage." } }, "type": "object", "required": [ "charge_array", "discharge_array", - "entlade_effizienz", + "discharging_efficiency", "hours", - "kapazitaet_wh", - "lade_effizienz", - "max_ladeleistung_w", + "capacity_wh", + "charging_efficiency", + "max_charge_power_w", "soc_wh", - "start_soc_prozent" + "initial_soc_percentage" ], - "title": "EAutoResult", - "description": "This object contains information related to the electric vehicle and its charging and discharging behavior." + "title": "ElectricVehicleResult", + "description": "Result class containing information related to the electric vehicle's charging and discharging behavior." }, "EnergieManagementSystemParameters": { "properties": { @@ -2766,7 +2769,7 @@ "$ref": "#/components/schemas/EnergieManagementSystemParameters" }, "pv_akku": { - "$ref": "#/components/schemas/PVAkkuParameters" + "$ref": "#/components/schemas/SolarPanelBatteryParameters" }, "inverter": { "$ref": "#/components/schemas/InverterParameters", @@ -2777,7 +2780,7 @@ "eauto": { "anyOf": [ { - "$ref": "#/components/schemas/EAutoParameters" + "$ref": "#/components/schemas/ElectricVehicleParameters" }, { "type": "null" @@ -2876,7 +2879,7 @@ "eauto_obj": { "anyOf": [ { - "$ref": "#/components/schemas/EAutoResult" + "$ref": "#/components/schemas/ElectricVehicleResult" }, { "type": "null" @@ -2923,71 +2926,6 @@ "title": "OptimizeResponse", "description": "**Note**: The first value of \"Last_Wh_per_hour\", \"Netzeinspeisung_Wh_per_hour\", and \"Netzbezug_Wh_per_hour\", will be set to null in the JSON output and represented as NaN or None in the corresponding classes' data returns. This approach is adopted to ensure that the current hour's processing remains unchanged." }, - "PVAkkuParameters": { - "properties": { - "kapazitaet_wh": { - "type": "integer", - "exclusiveMinimum": 0.0, - "title": "Kapazitaet Wh", - "description": "An integer representing the capacity of the battery in watt-hours." - }, - "lade_effizienz": { - "type": "number", - "maximum": 1.0, - "exclusiveMinimum": 0.0, - "title": "Lade Effizienz", - "description": "A float representing the charging efficiency of the battery.", - "default": 0.88 - }, - "entlade_effizienz": { - "type": "number", - "maximum": 1.0, - "exclusiveMinimum": 0.0, - "title": "Entlade Effizienz", - "default": 0.88 - }, - "max_ladeleistung_w": { - "anyOf": [ - { - "type": "number", - "exclusiveMinimum": 0.0 - }, - { - "type": "null" - } - ], - "title": "Max Ladeleistung W", - "description": "An integer representing the charging power of the battery in watts.", - "default": 5000 - }, - "start_soc_prozent": { - "type": "integer", - "maximum": 100.0, - "minimum": 0.0, - "title": "Start Soc Prozent", - "description": "An integer representing the state of charge of the battery at the **start** of the current hour (not the current state).", - "default": 0 - }, - "min_soc_prozent": { - "type": "integer", - "maximum": 100.0, - "minimum": 0.0, - "title": "Min Soc Prozent", - "description": "An integer representing the minimum state of charge (SOC) of the battery in percentage.", - "default": 0 - }, - "max_soc_prozent": { - "type": "integer", - "maximum": 100.0, - "minimum": 0.0, - "title": "Max Soc Prozent", - "default": 100 - } - }, - "type": "object", - "required": ["kapazitaet_wh"], - "title": "PVAkkuParameters" - }, "SettingsEOS": { "properties": { "server_fastapi_host": { @@ -4568,7 +4506,7 @@ "title": "Battery Capacity", "description": "Battery capacity [Wh]." }, - "battery_soc_start": { + "battery_initial_soc": { "anyOf": [ { "type": "integer" @@ -4577,7 +4515,7 @@ "type": "null" } ], - "title": "Battery Soc Start", + "title": "Battery Initial Soc", "description": "Battery initial state of charge [%]." }, "battery_soc_min": { @@ -4604,7 +4542,7 @@ "title": "Battery Soc Max", "description": "Battery maximum state of charge [%]." }, - "battery_charge_efficiency": { + "battery_charging_efficiency": { "anyOf": [ { "type": "number" @@ -4613,10 +4551,10 @@ "type": "null" } ], - "title": "Battery Charge Efficiency", + "title": "Battery Charging Efficiency", "description": "Battery charging efficiency [%]." }, - "battery_discharge_efficiency": { + "battery_discharging_efficiency": { "anyOf": [ { "type": "number" @@ -4625,10 +4563,10 @@ "type": "null" } ], - "title": "Battery Discharge Efficiency", + "title": "Battery Discharging Efficiency", "description": "Battery discharging efficiency [%]." }, - "battery_charge_power_max": { + "battery_max_charging_power": { "anyOf": [ { "type": "integer" @@ -4637,7 +4575,7 @@ "type": "null" } ], - "title": "Battery Charge Power Max", + "title": "Battery Max Charging Power", "description": "Battery maximum charge power [W]." }, "bev_provider": { @@ -4664,7 +4602,7 @@ "title": "Bev Capacity", "description": "Battery Electric Vehicle capacity [Wh]." }, - "bev_soc_start": { + "bev_initial_soc": { "anyOf": [ { "type": "integer" @@ -4673,7 +4611,7 @@ "type": "null" } ], - "title": "Bev Soc Start", + "title": "Bev Initial Soc", "description": "Battery Electric Vehicle initial state of charge [%]." }, "bev_soc_max": { @@ -4688,7 +4626,7 @@ "title": "Bev Soc Max", "description": "Battery Electric Vehicle maximum state of charge [%]." }, - "bev_charge_efficiency": { + "bev_charging_efficiency": { "anyOf": [ { "type": "number" @@ -4697,10 +4635,10 @@ "type": "null" } ], - "title": "Bev Charge Efficiency", + "title": "Bev Charging Efficiency", "description": "Battery Electric Vehicle charging efficiency [%]." }, - "bev_discharge_efficiency": { + "bev_discharging_efficiency": { "anyOf": [ { "type": "number" @@ -4709,10 +4647,10 @@ "type": "null" } ], - "title": "Bev Discharge Efficiency", + "title": "Bev Discharging Efficiency", "description": "Battery Electric Vehicle discharging efficiency [%]." }, - "bev_charge_power_max": { + "bev_max_charging_power": { "anyOf": [ { "type": "integer" @@ -4721,7 +4659,7 @@ "type": "null" } ], - "title": "Bev Charge Power Max", + "title": "Bev Max Charging Power", "description": "Battery Electric Vehicle maximum charge power [W]." }, "dishwasher_provider": { @@ -5007,6 +4945,73 @@ "title": "SimulationResult", "description": "This object contains the results of the simulation and provides insights into various parameters over the entire forecast period." }, + "SolarPanelBatteryParameters": { + "properties": { + "capacity_wh": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Capacity Wh", + "description": "An integer representing the capacity of the battery in watt-hours." + }, + "charging_efficiency": { + "type": "number", + "maximum": 1.0, + "exclusiveMinimum": 0.0, + "title": "Charging Efficiency", + "description": "A float representing the charging efficiency of the battery.", + "default": 0.88 + }, + "discharging_efficiency": { + "type": "number", + "maximum": 1.0, + "exclusiveMinimum": 0.0, + "title": "Discharging Efficiency", + "description": "A float representing the discharge efficiency of the battery.", + "default": 0.88 + }, + "max_charge_power_w": { + "anyOf": [ + { + "type": "number", + "exclusiveMinimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Max Charge Power W", + "description": "Maximum charging power in watts.", + "default": 5000 + }, + "initial_soc_percentage": { + "type": "integer", + "maximum": 100.0, + "minimum": 0.0, + "title": "Initial Soc Percentage", + "description": "An integer representing the state of charge of the battery at the **start** of the current hour (not the current state).", + "default": 0 + }, + "min_soc_percentage": { + "type": "integer", + "maximum": 100.0, + "minimum": 0.0, + "title": "Min Soc Percentage", + "description": "An integer representing the minimum state of charge (SOC) of the battery in percentage.", + "default": 0 + }, + "max_soc_percentage": { + "type": "integer", + "maximum": 100.0, + "minimum": 0.0, + "title": "Max Soc Percentage", + "description": "An integer representing the maximum state of charge (SOC) of the battery in percentage.", + "default": 100 + } + }, + "type": "object", + "required": ["capacity_wh"], + "title": "SolarPanelBatteryParameters" + }, "ValidationError": { "properties": { "loc": { diff --git a/single_test_optimization.py b/single_test_optimization.py index 06079b4..0d301cf 100644 --- a/single_test_optimization.py +++ b/single_test_optimization.py @@ -125,16 +125,16 @@ def prepare_optimization_real_parameters() -> OptimizationParameters: "strompreis_euro_pro_wh": strompreis_euro_pro_wh, }, "pv_akku": { - "kapazitaet_wh": 26400, - "start_soc_prozent": 15, - "min_soc_prozent": 15, + "capacity_wh": 26400, + "initial_soc_percentage": 15, + "min_soc_percentage": 15, }, "eauto": { - "min_soc_prozent": 50, - "kapazitaet_wh": 60000, - "lade_effizienz": 0.95, - "max_ladeleistung_w": 11040, - "start_soc_prozent": 5, + "min_soc_percentage": 50, + "capacity_wh": 60000, + "charging_efficiency": 0.95, + "max_charge_power_w": 11040, + "initial_soc_percentage": 5, }, "temperature_forecast": temperature_forecast, "start_solution": start_solution, @@ -276,16 +276,16 @@ def prepare_optimization_parameters() -> OptimizationParameters: "strompreis_euro_pro_wh": strompreis_euro_pro_wh, }, "pv_akku": { - "kapazitaet_wh": 26400, - "start_soc_prozent": 15, - "min_soc_prozent": 15, + "capacity_wh": 26400, + "initial_soc_percentage": 15, + "min_soc_percentage": 15, }, "eauto": { - "min_soc_prozent": 50, - "kapazitaet_wh": 60000, - "lade_effizienz": 0.95, - "max_ladeleistung_w": 11040, - "start_soc_prozent": 5, + "min_soc_percentage": 50, + "capacity_wh": 60000, + "charging_efficiency": 0.95, + "max_charge_power_w": 11040, + "initial_soc_percentage": 5, }, "temperature_forecast": temperature_forecast, "start_solution": start_solution, diff --git a/src/akkudoktoreos/core/ems.py b/src/akkudoktoreos/core/ems.py index 1d274c9..bb112a0 100644 --- a/src/akkudoktoreos/core/ems.py +++ b/src/akkudoktoreos/core/ems.py @@ -8,7 +8,7 @@ from typing_extensions import Self from akkudoktoreos.core.coreabc import ConfigMixin, PredictionMixin, SingletonMixin from akkudoktoreos.core.pydantic import PydanticBaseModel -from akkudoktoreos.devices.battery import PVAkku +from akkudoktoreos.devices.battery import Battery from akkudoktoreos.devices.generic import HomeAppliance from akkudoktoreos.devices.inverter import Inverter from akkudoktoreos.utils.datetimeutil import to_datetime @@ -152,8 +152,8 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda # TODO: Move to devices # ------------------------- - akku: Optional[PVAkku] = Field(default=None, description="TBD.") - eauto: Optional[PVAkku] = Field(default=None, description="TBD.") + akku: Optional[Battery] = Field(default=None, description="TBD.") + eauto: Optional[Battery] = Field(default=None, description="TBD.") home_appliance: Optional[HomeAppliance] = Field(default=None, description="TBD.") inverter: Optional[Inverter] = Field(default=None, description="TBD.") @@ -168,7 +168,7 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda def set_parameters( self, parameters: EnergieManagementSystemParameters, - eauto: Optional[PVAkku] = None, + eauto: Optional[Battery] = None, home_appliance: Optional[HomeAppliance] = None, inverter: Optional[Inverter] = None, ) -> None: @@ -323,9 +323,9 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda # Set initial state if self.akku: - akku_soc_pro_stunde[0] = self.akku.ladezustand_in_prozent() + akku_soc_pro_stunde[0] = self.akku.current_soc_percentage() if self.eauto: - eauto_soc_pro_stunde[0] = self.eauto.ladezustand_in_prozent() + eauto_soc_pro_stunde[0] = self.eauto.current_soc_percentage() for stunde in range(start_stunde, ende): stunde_since_now = stunde - start_stunde @@ -343,12 +343,12 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda # E-Auto handling if self.eauto: if self.ev_charge_hours[stunde] > 0: - geladene_menge_eauto, verluste_eauto = self.eauto.energie_laden( + geladene_menge_eauto, verluste_eauto = self.eauto.charge_energy( None, stunde, relative_power=self.ev_charge_hours[stunde] ) verbrauch += geladene_menge_eauto verluste_wh_pro_stunde[stunde_since_now] += verluste_eauto - eauto_soc_pro_stunde[stunde_since_now] = self.eauto.ladezustand_in_prozent() + eauto_soc_pro_stunde[stunde_since_now] = self.eauto.current_soc_percentage() # Process inverter logic netzeinspeisung, netzbezug, verluste, eigenverbrauch = (0.0, 0.0, 0.0, 0.0) @@ -363,10 +363,10 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda # AC PV Battery Charge if self.akku and self.ac_charge_hours[stunde] > 0.0: self.akku.set_charge_allowed_for_hour(1, stunde) - geladene_menge, verluste_wh = self.akku.energie_laden( + geladene_menge, verluste_wh = self.akku.charge_energy( None, stunde, relative_power=self.ac_charge_hours[stunde] ) - # print(stunde, " ", geladene_menge, " ",self.ac_charge_hours[stunde]," ",self.akku.ladezustand_in_prozent()) + # print(stunde, " ", geladene_menge, " ",self.ac_charge_hours[stunde]," ",self.akku.current_soc_percentage()) verbrauch += geladene_menge verbrauch += verluste_wh netzbezug += geladene_menge @@ -388,7 +388,7 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda # Akku SOC tracking if self.akku: - akku_soc_pro_stunde[stunde_since_now] = self.akku.ladezustand_in_prozent() + akku_soc_pro_stunde[stunde_since_now] = self.akku.current_soc_percentage() else: akku_soc_pro_stunde[stunde_since_now] = 0.0 diff --git a/src/akkudoktoreos/devices/battery.py b/src/akkudoktoreos/devices/battery.py index 98b5d40..0ca5708 100644 --- a/src/akkudoktoreos/devices/battery.py +++ b/src/akkudoktoreos/devices/battery.py @@ -10,345 +10,279 @@ from akkudoktoreos.utils.utils import NumpyEncoder logger = get_logger(__name__) -def max_ladeleistung_w_field(default: Optional[float] = None) -> Optional[float]: +def max_charging_power_field(description: Optional[str] = None) -> float: + if description is None: + description = "Maximum charging power in watts." return Field( - default=default, + default=5000, gt=0, - description="An integer representing the charging power of the battery in watts.", + description=description, ) -def start_soc_prozent_field(description: str) -> int: +def initial_soc_percentage_field(description: str) -> int: return Field(default=0, ge=0, le=100, description=description) -class BaseAkkuParameters(BaseModel): - kapazitaet_wh: int = Field( +class BaseBatteryParameters(BaseModel): + """Base class for battery parameters with fields for capacity, efficiency, and state of charge.""" + + capacity_wh: int = Field( gt=0, description="An integer representing the capacity of the battery in watt-hours." ) - lade_effizienz: float = Field( + charging_efficiency: float = Field( default=0.88, gt=0, le=1, description="A float representing the charging efficiency of the battery.", ) - entlade_effizienz: float = Field(default=0.88, gt=0, le=1) - max_ladeleistung_w: Optional[float] = max_ladeleistung_w_field() - start_soc_prozent: int = start_soc_prozent_field( + discharging_efficiency: float = Field( + default=0.88, + gt=0, + le=1, + description="A float representing the discharge efficiency of the battery.", + ) + max_charge_power_w: Optional[float] = max_charging_power_field() + initial_soc_percentage: int = initial_soc_percentage_field( "An integer representing the state of charge of the battery at the **start** of the current hour (not the current state)." ) - min_soc_prozent: int = Field( + min_soc_percentage: int = Field( default=0, ge=0, le=100, description="An integer representing the minimum state of charge (SOC) of the battery in percentage.", ) - max_soc_prozent: int = Field(default=100, ge=0, le=100) + max_soc_percentage: int = Field( + default=100, + ge=0, + le=100, + description="An integer representing the maximum state of charge (SOC) of the battery in percentage.", + ) -class PVAkkuParameters(BaseAkkuParameters): - max_ladeleistung_w: Optional[float] = max_ladeleistung_w_field(5000) +class SolarPanelBatteryParameters(BaseBatteryParameters): + max_charge_power_w: Optional[float] = max_charging_power_field() -class EAutoParameters(BaseAkkuParameters): - entlade_effizienz: float = 1.0 - start_soc_prozent: int = start_soc_prozent_field( +class ElectricVehicleParameters(BaseBatteryParameters): + """Parameters specific to an electric vehicle (EV).""" + + discharging_efficiency: float = 1.0 + initial_soc_percentage: int = initial_soc_percentage_field( "An integer representing the current state of charge (SOC) of the battery in percentage." ) -class EAutoResult(BaseModel): - """This object contains information related to the electric vehicle and its charging and discharging behavior.""" +class ElectricVehicleResult(BaseModel): + """Result class containing information related to the electric vehicle's charging and discharging behavior.""" charge_array: list[float] = Field( - description="Indicates for each hour whether the EV is charging (`0` for no charging, `1` for charging)." + description="Hourly charging status (0 for no charging, 1 for charging)." ) discharge_array: list[int] = Field( - description="Indicates for each hour whether the EV is discharging (`0` for no discharging, `1` for discharging)." + description="Hourly discharging status (0 for no discharging, 1 for discharging)." ) - entlade_effizienz: float = Field(description="The discharge efficiency as a float.") - hours: int = Field(description="Amount of hours the simulation is done for.") - kapazitaet_wh: int = Field(description="The capacity of the EV’s battery in watt-hours.") - lade_effizienz: float = Field(description="The charging efficiency as a float.") - max_ladeleistung_w: int = Field(description="The maximum charging power of the EV in watts.") + discharging_efficiency: float = Field(description="The discharge efficiency as a float..") + hours: int = Field(description="Number of hours in the simulation.") + capacity_wh: int = Field(description="Capacity of the EV’s battery in watt-hours.") + charging_efficiency: float = Field(description="Charging efficiency as a float..") + max_charge_power_w: int = Field(description="Maximum charging power in watts.") soc_wh: float = Field( - description="The state of charge of the battery in watt-hours at the start of the simulation." + description="State of charge of the battery in watt-hours at the start of the simulation." ) - start_soc_prozent: int = Field( - description="The state of charge of the battery in percentage at the start of the simulation." + initial_soc_percentage: int = Field( + description="State of charge at the start of the simulation in percentage." ) - @field_validator( - "discharge_array", - "charge_array", - mode="before", - ) + @field_validator("discharge_array", "charge_array", mode="before") def convert_numpy(cls, field: Any) -> Any: return NumpyEncoder.convert_numpy(field)[0] -class PVAkku(DeviceBase): +class Battery(DeviceBase): + """Represents a battery device with methods to simulate energy charging and discharging.""" + def __init__( self, - parameters: Optional[BaseAkkuParameters] = None, + parameters: Optional[BaseBatteryParameters] = None, hours: Optional[int] = 24, provider_id: Optional[str] = None, ): - # Configuration initialisation + # Initialize configuration and parameters self.provider_id = provider_id self.prefix = "" if self.provider_id == "GenericBattery": self.prefix = "battery" elif self.provider_id == "GenericBEV": self.prefix = "bev" - # Parameter initialisiation + self.parameters = parameters if hours is None: - self.hours = self.total_hours + self.hours = self.total_hours # TODO where does that come from? else: self.hours = hours self.initialised = False + # Run setup if parameters are given, otherwise setup() has to be called later when the config is initialised. if self.parameters is not None: self.setup() def setup(self) -> None: + """Sets up the battery parameters based on configuration or provided parameters.""" if self.initialised: return - if self.provider_id is not None: - # Setup by configuration - # Battery capacity in Wh - self.kapazitaet_wh = getattr(self.config, f"{self.prefix}_capacity") - # Initial state of charge in Wh - self.start_soc_prozent = getattr(self.config, f"{self.prefix}_soc_start") - self.hours = self.total_hours - # Charge and discharge efficiency - self.lade_effizienz = getattr(self.config, f"{self.prefix}_charge_efficiency") - self.entlade_effizienz = getattr(self.config, f"{self.prefix}_discharge_efficiency") - self.max_ladeleistung_w = getattr(self.config, f"{self.prefix}_charge_power_max") - # Only assign for storage battery + + if self.provider_id: + # Setup from configuration + self.capacity_wh = getattr(self.config, f"{self.prefix}_capacity") + self.initial_soc_percentage = getattr(self.config, f"{self.prefix}_initial_soc") + self.hours = self.total_hours # TODO where does that come from? + self.charging_efficiency = getattr(self.config, f"{self.prefix}_charging_efficiency") + self.discharging_efficiency = getattr( + self.config, f"{self.prefix}_discharging_efficiency" + ) + self.max_charge_power_w = getattr(self.config, f"{self.prefix}_max_charging_power") + if self.provider_id == "GenericBattery": - self.min_soc_prozent = getattr(self.config, f"{self.prefix}_soc_mint") + self.min_soc_percentage = getattr( + self.config, + f"{self.prefix}_soc_min", + ) else: - self.min_soc_prozent = 0 - self.max_soc_prozent = getattr(self.config, f"{self.prefix}_soc_mint") - elif self.parameters is not None: - # Setup by parameters - # Battery capacity in Wh - self.kapazitaet_wh = self.parameters.kapazitaet_wh - # Initial state of charge in Wh - self.start_soc_prozent = self.parameters.start_soc_prozent - # Charge and discharge efficiency - self.lade_effizienz = self.parameters.lade_effizienz - self.entlade_effizienz = self.parameters.entlade_effizienz - self.max_ladeleistung_w = self.parameters.max_ladeleistung_w + self.min_soc_percentage = 0 + + self.max_soc_percentage = getattr( + self.config, + f"{self.prefix}_soc_max", + ) + elif self.parameters: + # Setup from parameters + self.capacity_wh = self.parameters.capacity_wh + self.initial_soc_percentage = self.parameters.initial_soc_percentage + self.charging_efficiency = self.parameters.charging_efficiency + self.discharging_efficiency = self.parameters.discharging_efficiency + self.max_charge_power_w = self.parameters.max_charge_power_w # Only assign for storage battery - self.min_soc_prozent = ( - self.parameters.min_soc_prozent - if isinstance(self.parameters, PVAkkuParameters) + self.min_soc_percentage = ( + self.parameters.min_soc_percentage + if isinstance(self.parameters, SolarPanelBatteryParameters) else 0 ) - self.max_soc_prozent = self.parameters.max_soc_prozent + self.max_soc_percentage = self.parameters.max_soc_percentage else: - error_msg = "Parameters and provider ID missing. Can't instantiate." + error_msg = "Parameters and provider ID are missing. Cannot instantiate." logger.error(error_msg) raise ValueError(error_msg) - # init - if self.max_ladeleistung_w is None: - self.max_ladeleistung_w = self.kapazitaet_wh + # Initialize state of charge + if self.max_charge_power_w is None: + self.max_charge_power_w = self.capacity_wh # TODO this should not be equal capacity_wh self.discharge_array = np.full(self.hours, 1) self.charge_array = np.full(self.hours, 1) - # Calculate start, min and max SoC in Wh - self.soc_wh = (self.start_soc_prozent / 100) * self.kapazitaet_wh - self.min_soc_wh = (self.min_soc_prozent / 100) * self.kapazitaet_wh - self.max_soc_wh = (self.max_soc_prozent / 100) * self.kapazitaet_wh + self.soc_wh = (self.initial_soc_percentage / 100) * self.capacity_wh + self.min_soc_wh = (self.min_soc_percentage / 100) * self.capacity_wh + self.max_soc_wh = (self.max_soc_percentage / 100) * self.capacity_wh self.initialised = True def to_dict(self) -> dict[str, Any]: + """Converts the object to a dictionary representation.""" return { - "kapazitaet_wh": self.kapazitaet_wh, - "start_soc_prozent": self.start_soc_prozent, + "capacity_wh": self.capacity_wh, + "initial_soc_percentage": self.initial_soc_percentage, "soc_wh": self.soc_wh, "hours": self.hours, "discharge_array": self.discharge_array, "charge_array": self.charge_array, - "lade_effizienz": self.lade_effizienz, - "entlade_effizienz": self.entlade_effizienz, - "max_ladeleistung_w": self.max_ladeleistung_w, + "charging_efficiency": self.charging_efficiency, + "discharging_efficiency": self.discharging_efficiency, + "max_charge_power_w": self.max_charge_power_w, } def reset(self) -> None: - self.soc_wh = (self.start_soc_prozent / 100) * self.kapazitaet_wh - # Ensure soc_wh is within min and max limits + """Resets the battery state to its initial values.""" + self.soc_wh = (self.initial_soc_percentage / 100) * self.capacity_wh self.soc_wh = min(max(self.soc_wh, self.min_soc_wh), self.max_soc_wh) - self.discharge_array = np.full(self.hours, 1) self.charge_array = np.full(self.hours, 1) def set_discharge_per_hour(self, discharge_array: np.ndarray) -> None: - assert len(discharge_array) == self.hours + """Sets the discharge values for each hour.""" + if len(discharge_array) != self.hours: + raise ValueError(f"Discharge array must have exactly {self.hours} elements.") self.discharge_array = np.array(discharge_array) def set_charge_per_hour(self, charge_array: np.ndarray) -> None: - assert len(charge_array) == self.hours + """Sets the charge values for each hour.""" + if len(charge_array) != self.hours: + raise ValueError(f"Charge array must have exactly {self.hours} elements.") self.charge_array = np.array(charge_array) def set_charge_allowed_for_hour(self, charge: float, hour: int) -> None: - assert hour < self.hours + """Sets the charge for a specific hour.""" + if hour >= self.hours: + raise ValueError(f"Hour {hour} is out of range. Must be less than {self.hours}.") self.charge_array[hour] = charge - def ladezustand_in_prozent(self) -> float: - return (self.soc_wh / self.kapazitaet_wh) * 100 + def current_soc_percentage(self) -> float: + """Calculates the current state of charge in percentage.""" + return (self.soc_wh / self.capacity_wh) * 100 - def energie_abgeben(self, wh: float, hour: int) -> tuple[float, float]: + def discharge_energy(self, wh: float, hour: int) -> tuple[float, float]: + """Discharges energy from the battery.""" if self.discharge_array[hour] == 0: - return 0.0, 0.0 # No energy discharge and no losses + return 0.0, 0.0 - # Calculate the maximum energy that can be discharged considering min_soc and efficiency - max_possible_discharge_wh = (self.soc_wh - self.min_soc_wh) * self.entlade_effizienz - max_possible_discharge_wh = max(max_possible_discharge_wh, 0.0) # Ensure non-negative + max_possible_discharge_wh = (self.soc_wh - self.min_soc_wh) * self.discharging_efficiency + max_possible_discharge_wh = max(max_possible_discharge_wh, 0.0) - # Consider the maximum discharge power of the battery - max_abgebbar_wh = min(max_possible_discharge_wh, self.max_ladeleistung_w) + max_possible_discharge_wh = min( + max_possible_discharge_wh, self.max_charge_power_w + ) # TODO make a new cfg variable max_discharge_power_w - # The actually discharged energy cannot exceed requested energy or maximum discharge - tatsaechlich_abgegeben_wh = min(wh, max_abgebbar_wh) + actual_discharge_wh = min(wh, max_possible_discharge_wh) + actual_withdrawal_wh = ( + actual_discharge_wh / self.discharging_efficiency + if self.discharging_efficiency > 0 + else 0.0 + ) - # Calculate the actual amount withdrawn from the battery (before efficiency loss) - if self.entlade_effizienz > 0: - tatsaechliche_entnahme_wh = tatsaechlich_abgegeben_wh / self.entlade_effizienz - else: - tatsaechliche_entnahme_wh = 0.0 - - # Update the state of charge considering the actual withdrawal - self.soc_wh -= tatsaechliche_entnahme_wh - # Ensure soc_wh does not go below min_soc_wh + self.soc_wh -= actual_withdrawal_wh self.soc_wh = max(self.soc_wh, self.min_soc_wh) - # Calculate losses due to efficiency - verluste_wh = tatsaechliche_entnahme_wh - tatsaechlich_abgegeben_wh + losses_wh = actual_withdrawal_wh - actual_discharge_wh + return actual_discharge_wh, losses_wh - # Return the actually discharged energy and the losses - return tatsaechlich_abgegeben_wh, verluste_wh - - def energie_laden( + def charge_energy( self, wh: Optional[float], hour: int, relative_power: float = 0.0 ) -> tuple[float, float]: + """Charges energy into the battery.""" if hour is not None and self.charge_array[hour] == 0: return 0.0, 0.0 # Charging not allowed in this hour + if relative_power > 0.0: - wh = self.max_ladeleistung_w * relative_power - # If no value for wh is given, use the maximum charging power - wh = wh if wh is not None else self.max_ladeleistung_w + wh = self.max_charge_power_w * relative_power - # Calculate the maximum energy that can be charged considering max_soc and efficiency - if self.lade_effizienz > 0: - max_possible_charge_wh = (self.max_soc_wh - self.soc_wh) / self.lade_effizienz - else: - max_possible_charge_wh = 0.0 - max_possible_charge_wh = max(max_possible_charge_wh, 0.0) # Ensure non-negative + wh = wh if wh is not None else self.max_charge_power_w - # The actually charged energy cannot exceed requested energy, charging power, or maximum possible charge - effektive_lademenge = min(wh, max_possible_charge_wh) + max_possible_charge_wh = ( + (self.max_soc_wh - self.soc_wh) / self.charging_efficiency + if self.charging_efficiency > 0 + else 0.0 + ) + max_possible_charge_wh = max(max_possible_charge_wh, 0.0) - # Energy actually stored in the battery - geladene_menge = effektive_lademenge * self.lade_effizienz + effective_charge_wh = min(wh, max_possible_charge_wh) + charged_wh = effective_charge_wh * self.charging_efficiency - # Update soc_wh - self.soc_wh += geladene_menge - # Ensure soc_wh does not exceed max_soc_wh + self.soc_wh += charged_wh self.soc_wh = min(self.soc_wh, self.max_soc_wh) - # Calculate losses - verluste_wh = effektive_lademenge - geladene_menge - return geladene_menge, verluste_wh + losses_wh = effective_charge_wh - charged_wh + return charged_wh, losses_wh - def aktueller_energieinhalt(self) -> float: - """This method returns the current remaining energy considering efficiency. - - It accounts for both charging and discharging efficiency. - """ - # Calculate remaining energy considering discharge efficiency - nutzbare_energie = (self.soc_wh - self.min_soc_wh) * self.entlade_effizienz - return max(nutzbare_energie, 0.0) - - -if __name__ == "__main__": - # Test battery discharge below min_soc - print("Test: Discharge below min_soc") - akku = PVAkku( - PVAkkuParameters( - kapazitaet_wh=10000, - start_soc_prozent=50, - min_soc_prozent=20, - max_soc_prozent=80, - ), - hours=1, - ) - akku.reset() - print(f"Initial SoC: {akku.ladezustand_in_prozent()}%") - - # Try to discharge 5000 Wh - abgegeben_wh, verlust_wh = akku.energie_abgeben(5000, 0) - print(f"Energy discharged: {abgegeben_wh} Wh, Losses: {verlust_wh} Wh") - print(f"SoC after discharge: {akku.ladezustand_in_prozent()}%") - print(f"Expected min SoC: {akku.min_soc_prozent}%") - - # Test battery charge above max_soc - print("\nTest: Charge above max_soc") - akku = PVAkku( - PVAkkuParameters( - kapazitaet_wh=10000, - start_soc_prozent=50, - min_soc_prozent=20, - max_soc_prozent=80, - ), - hours=1, - ) - akku.reset() - print(f"Initial SoC: {akku.ladezustand_in_prozent()}%") - - # Try to charge 5000 Wh - geladen_wh, verlust_wh = akku.energie_laden(5000, 0) - print(f"Energy charged: {geladen_wh} Wh, Losses: {verlust_wh} Wh") - print(f"SoC after charge: {akku.ladezustand_in_prozent()}%") - print(f"Expected max SoC: {akku.max_soc_prozent}%") - - # Test charging when battery is at max_soc - print("\nTest: Charging when at max_soc") - akku = PVAkku( - PVAkkuParameters( - kapazitaet_wh=10000, - start_soc_prozent=80, - min_soc_prozent=20, - max_soc_prozent=80, - ), - hours=1, - ) - akku.reset() - print(f"Initial SoC: {akku.ladezustand_in_prozent()}%") - - geladen_wh, verlust_wh = akku.energie_laden(5000, 0) - print(f"Energy charged: {geladen_wh} Wh, Losses: {verlust_wh} Wh") - print(f"SoC after charge: {akku.ladezustand_in_prozent()}%") - - # Test discharging when battery is at min_soc - print("\nTest: Discharging when at min_soc") - akku = PVAkku( - PVAkkuParameters( - kapazitaet_wh=10000, - start_soc_prozent=20, - min_soc_prozent=20, - max_soc_prozent=80, - ), - hours=1, - ) - akku.reset() - print(f"Initial SoC: {akku.ladezustand_in_prozent()}%") - - abgegeben_wh, verlust_wh = akku.energie_abgeben(5000, 0) - print(f"Energy discharged: {abgegeben_wh} Wh, Losses: {verlust_wh} Wh") - print(f"SoC after discharge: {akku.ladezustand_in_prozent()}%") + def current_energy_content(self) -> float: + """Returns the current usable energy in the battery.""" + usable_energy = (self.soc_wh - self.min_soc_wh) * self.discharging_efficiency + return max(usable_energy, 0.0) diff --git a/src/akkudoktoreos/devices/devices.py b/src/akkudoktoreos/devices/devices.py index 6194b67..90a6f9e 100644 --- a/src/akkudoktoreos/devices/devices.py +++ b/src/akkudoktoreos/devices/devices.py @@ -6,7 +6,7 @@ from pydantic import Field, computed_field from akkudoktoreos.config.configabc import SettingsBaseModel from akkudoktoreos.core.coreabc import SingletonMixin -from akkudoktoreos.devices.battery import PVAkku +from akkudoktoreos.devices.battery import Battery from akkudoktoreos.devices.devicesabc import DevicesBase from akkudoktoreos.devices.generic import HomeAppliance from akkudoktoreos.devices.inverter import Inverter @@ -25,7 +25,7 @@ class DevicesCommonSettings(SettingsBaseModel): default=None, description="Id of Battery simulation provider." ) battery_capacity: Optional[int] = Field(default=None, description="Battery capacity [Wh].") - battery_soc_start: Optional[int] = Field( + battery_initial_soc: Optional[int] = Field( default=None, description="Battery initial state of charge [%]." ) battery_soc_min: Optional[int] = Field( @@ -34,13 +34,13 @@ class DevicesCommonSettings(SettingsBaseModel): battery_soc_max: Optional[int] = Field( default=None, description="Battery maximum state of charge [%]." ) - battery_charge_efficiency: Optional[float] = Field( + battery_charging_efficiency: Optional[float] = Field( default=None, description="Battery charging efficiency [%]." ) - battery_discharge_efficiency: Optional[float] = Field( + battery_discharging_efficiency: Optional[float] = Field( default=None, description="Battery discharging efficiency [%]." ) - battery_charge_power_max: Optional[int] = Field( + battery_max_charging_power: Optional[int] = Field( default=None, description="Battery maximum charge power [W]." ) @@ -52,19 +52,19 @@ class DevicesCommonSettings(SettingsBaseModel): bev_capacity: Optional[int] = Field( default=None, description="Battery Electric Vehicle capacity [Wh]." ) - bev_soc_start: Optional[int] = Field( + bev_initial_soc: Optional[int] = Field( default=None, description="Battery Electric Vehicle initial state of charge [%]." ) bev_soc_max: Optional[int] = Field( default=None, description="Battery Electric Vehicle maximum state of charge [%]." ) - bev_charge_efficiency: Optional[float] = Field( + bev_charging_efficiency: Optional[float] = Field( default=None, description="Battery Electric Vehicle charging efficiency [%]." ) - bev_discharge_efficiency: Optional[float] = Field( + bev_discharging_efficiency: Optional[float] = Field( default=None, description="Battery Electric Vehicle discharging efficiency [%]." ) - bev_charge_power_max: Optional[int] = Field( + bev_max_charging_power: Optional[int] = Field( default=None, description="Battery Electric Vehicle maximum charge power [W]." ) @@ -159,8 +159,8 @@ class Devices(SingletonMixin, DevicesBase): # Devices # TODO: Make devices class a container of device simulation providers. # Device simulations to be used are then enabled in the configuration. - akku: ClassVar[PVAkku] = PVAkku(provider_id="GenericBattery") - eauto: ClassVar[PVAkku] = PVAkku(provider_id="GenericBEV") + akku: ClassVar[Battery] = Battery(provider_id="GenericBattery") + eauto: ClassVar[Battery] = Battery(provider_id="GenericBEV") home_appliance: ClassVar[HomeAppliance] = HomeAppliance(provider_id="GenericDishWasher") inverter: ClassVar[Inverter] = Inverter(akku=akku, provider_id="GenericInverter") @@ -186,9 +186,9 @@ class Devices(SingletonMixin, DevicesBase): # Set initial state simulation_step = to_duration("1 hour") if self.akku: - self.akku_soc_pro_stunde[0] = self.akku.ladezustand_in_prozent() + self.akku_soc_pro_stunde[0] = self.akku.current_soc_percentage() if self.eauto: - self.eauto_soc_pro_stunde[0] = self.eauto.ladezustand_in_prozent() + self.eauto_soc_pro_stunde[0] = self.eauto.current_soc_percentage() # Get predictions for full device simulation time range # gesamtlast[stunde] @@ -232,12 +232,12 @@ class Devices(SingletonMixin, DevicesBase): # E-Auto handling if self.eauto: if self.ev_charge_hours[hour] > 0: - geladene_menge_eauto, verluste_eauto = self.eauto.energie_laden( + geladene_menge_eauto, verluste_eauto = self.eauto.charge_energy( None, hour, relative_power=self.ev_charge_hours[hour] ) consumption += geladene_menge_eauto self.verluste_wh_pro_stunde[stunde_since_now] += verluste_eauto - self.eauto_soc_pro_stunde[stunde_since_now] = self.eauto.ladezustand_in_prozent() + self.eauto_soc_pro_stunde[stunde_since_now] = self.eauto.current_soc_percentage() # Process inverter logic grid_export, grid_import, losses, self_consumption = (0.0, 0.0, 0.0, 0.0) @@ -252,10 +252,10 @@ class Devices(SingletonMixin, DevicesBase): # AC PV Battery Charge if self.akku and self.ac_charge_hours[hour] > 0.0: self.akku.set_charge_allowed_for_hour(1, hour) - geladene_menge, verluste_wh = self.akku.energie_laden( + geladene_menge, verluste_wh = self.akku.charge_energy( None, hour, relative_power=self.ac_charge_hours[hour] ) - # print(stunde, " ", geladene_menge, " ",self.ac_charge_hours[stunde]," ",self.akku.ladezustand_in_prozent()) + # print(stunde, " ", geladene_menge, " ",self.ac_charge_hours[stunde]," ",self.akku.current_soc_percentage()) consumption += geladene_menge grid_import += geladene_menge self.verluste_wh_pro_stunde[stunde_since_now] += verluste_wh @@ -275,7 +275,7 @@ class Devices(SingletonMixin, DevicesBase): # Akku SOC tracking if self.akku: - self.akku_soc_pro_stunde[stunde_since_now] = self.akku.ladezustand_in_prozent() + self.akku_soc_pro_stunde[stunde_since_now] = self.akku.current_soc_percentage() else: self.akku_soc_pro_stunde[stunde_since_now] = 0.0 diff --git a/src/akkudoktoreos/devices/inverter.py b/src/akkudoktoreos/devices/inverter.py index a3b2c0d..9e1d508 100644 --- a/src/akkudoktoreos/devices/inverter.py +++ b/src/akkudoktoreos/devices/inverter.py @@ -2,7 +2,7 @@ from typing import Optional, Tuple from pydantic import BaseModel, Field -from akkudoktoreos.devices.battery import PVAkku +from akkudoktoreos.devices.battery import Battery from akkudoktoreos.devices.devicesabc import DeviceBase from akkudoktoreos.utils.logutil import get_logger @@ -17,7 +17,7 @@ class Inverter(DeviceBase): def __init__( self, parameters: Optional[InverterParameters] = None, - akku: Optional[PVAkku] = None, + akku: Optional[Battery] = None, provider_id: Optional[str] = None, ): # Configuration initialisation @@ -70,7 +70,7 @@ class Inverter(DeviceBase): remaining_energy = generation - actual_consumption # Charge battery with excess energy - charged_energy, charging_losses = self.akku.energie_laden(remaining_energy, hour) + charged_energy, charging_losses = self.akku.charge_energy(remaining_energy, hour) losses += charging_losses # Calculate remaining surplus after battery charge @@ -87,7 +87,7 @@ class Inverter(DeviceBase): available_ac_power = max(self.max_power_wh - generation, 0) # Discharge battery to cover shortfall, if possible - battery_discharge, discharge_losses = self.akku.energie_abgeben( + battery_discharge, discharge_losses = self.akku.discharge_energy( min(shortfall, available_ac_power), hour ) losses += discharge_losses diff --git a/src/akkudoktoreos/optimization/genetic.py b/src/akkudoktoreos/optimization/genetic.py index ef0b298..af0dac4 100644 --- a/src/akkudoktoreos/optimization/genetic.py +++ b/src/akkudoktoreos/optimization/genetic.py @@ -13,10 +13,10 @@ from akkudoktoreos.core.coreabc import ( ) from akkudoktoreos.core.ems import EnergieManagementSystemParameters, SimulationResult from akkudoktoreos.devices.battery import ( - EAutoParameters, - EAutoResult, - PVAkku, - PVAkkuParameters, + Battery, + ElectricVehicleParameters, + ElectricVehicleResult, + SolarPanelBatteryParameters, ) from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters from akkudoktoreos.devices.inverter import Inverter, InverterParameters @@ -26,9 +26,9 @@ from akkudoktoreos.visualize import visualisiere_ergebnisse class OptimizationParameters(BaseModel): ems: EnergieManagementSystemParameters - pv_akku: PVAkkuParameters + pv_akku: SolarPanelBatteryParameters inverter: InverterParameters = InverterParameters() - eauto: Optional[EAutoParameters] + eauto: Optional[ElectricVehicleParameters] dishwasher: Optional[HomeApplianceParameters] = None temperature_forecast: Optional[list[float]] = Field( default=None, @@ -68,7 +68,7 @@ class OptimizeResponse(BaseModel): ) eautocharge_hours_float: Optional[list[float]] = Field(description="TBD") result: SimulationResult - eauto_obj: Optional[EAutoResult] + eauto_obj: Optional[ElectricVehicleResult] start_solution: Optional[list[float]] = Field( default=None, description="An array of binary values (0 or 1) representing a possible starting solution for the simulation.", @@ -92,8 +92,8 @@ class OptimizeResponse(BaseModel): mode="before", ) def convert_eauto(cls, field: Any) -> Any: - if isinstance(field, PVAkku): - return EAutoResult(**field.to_dict()) + if isinstance(field, Battery): + return ElectricVehicleResult(**field.to_dict()) return field @@ -367,7 +367,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi ) # Penalty for not meeting the minimum SOC (State of Charge) requirement - # if parameters.eauto_min_soc_prozent - ems.eauto.ladezustand_in_prozent() <= 0.0 and self.optimize_ev: + # if parameters.eauto_min_soc_prozent - ems.eauto.current_soc_percentage() <= 0.0 and self.optimize_ev: # gesamtbilanz += sum( # self.config.optimization_penalty for ladeleistung in eautocharge_hours_float if ladeleistung != 0.0 # ) @@ -375,7 +375,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi individual.extra_data = ( # type: ignore[attr-defined] o["Gesamtbilanz_Euro"], o["Gesamt_Verluste"], - parameters.eauto.min_soc_prozent - self.ems.eauto.ladezustand_in_prozent() + parameters.eauto.min_soc_percentage - self.ems.eauto.current_soc_percentage() if parameters.eauto and self.ems.eauto else 0, ) @@ -383,16 +383,16 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi # Adjust total balance with battery value and penalties for unmet SOC restwert_akku = ( - self.ems.akku.aktueller_energieinhalt() * parameters.ems.preis_euro_pro_wh_akku + self.ems.akku.current_energy_content() * parameters.ems.preis_euro_pro_wh_akku ) - # print(ems.akku.aktueller_energieinhalt()," * ", parameters.ems.preis_euro_pro_wh_akku , " ", restwert_akku, " ", gesamtbilanz) + # print(ems.akku.current_energy_content()," * ", parameters.ems.preis_euro_pro_wh_akku , " ", restwert_akku, " ", gesamtbilanz) gesamtbilanz += -restwert_akku # print(gesamtbilanz) if self.optimize_ev: gesamtbilanz += max( 0, ( - parameters.eauto.min_soc_prozent - self.ems.eauto.ladezustand_in_prozent() + parameters.eauto.min_soc_percentage - self.ems.eauto.current_soc_percentage() if parameters.eauto and self.ems.eauto else 0 ) @@ -458,21 +458,21 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi ) # Initialize PV and EV batteries - akku = PVAkku( + akku = Battery( parameters.pv_akku, hours=self.config.prediction_hours, ) akku.set_charge_per_hour(np.full(self.config.prediction_hours, 1)) - eauto: Optional[PVAkku] = None + eauto: Optional[Battery] = None if parameters.eauto: - eauto = PVAkku( + eauto = Battery( parameters.eauto, hours=self.config.prediction_hours, ) eauto.set_charge_per_hour(np.full(self.config.prediction_hours, 1)) self.optimize_ev = ( - parameters.eauto.min_soc_prozent - parameters.eauto.start_soc_prozent >= 0 + parameters.eauto.min_soc_percentage - parameters.eauto.initial_soc_percentage >= 0 ) else: self.optimize_ev = False diff --git a/tests/test_battery.py b/tests/test_battery.py new file mode 100644 index 0000000..3bd0bba --- /dev/null +++ b/tests/test_battery.py @@ -0,0 +1,219 @@ +import numpy as np +import pytest + +from akkudoktoreos.devices.battery import Battery, SolarPanelBatteryParameters + + +@pytest.fixture +def setup_pv_battery(): + params = SolarPanelBatteryParameters( + capacity_wh=10000, + initial_soc_percentage=50, + min_soc_percentage=20, + max_soc_percentage=80, + max_charge_power_w=8000, + ) + battery = Battery(params, hours=24) + battery.reset() + return battery + + +def test_initial_state_of_charge(setup_pv_battery): + battery = setup_pv_battery + assert battery.current_soc_percentage() == 50.0, "Initial SoC should be 50%" + + +def test_battery_discharge_below_min_soc(setup_pv_battery): + battery = setup_pv_battery + discharged_wh, loss_wh = battery.discharge_energy(5000, 0) + + # Ensure it discharges energy and stops at the min SOC + assert discharged_wh > 0 + print(discharged_wh, loss_wh, battery.current_soc_percentage(), battery.min_soc_percentage) + assert battery.current_soc_percentage() >= 20 # Ensure it's above min_soc_percentage + assert loss_wh >= 0 # Losses should not be negative + assert discharged_wh == 2640.0, "The energy discharged should be limited by min_soc" + + +def test_battery_charge_above_max_soc(setup_pv_battery): + battery = setup_pv_battery + charged_wh, loss_wh = battery.charge_energy(5000, 0) + + # Ensure it charges energy and stops at the max SOC + assert charged_wh > 0 + assert battery.current_soc_percentage() <= 80 # Ensure it's below max_soc_percentage + assert loss_wh >= 0 # Losses should not be negative + assert charged_wh == 3000.0, "The energy charged should be limited by max_soc" + + +def test_battery_charge_when_full(setup_pv_battery): + battery = setup_pv_battery + battery.soc_wh = battery.max_soc_wh # Set battery to full + charged_wh, loss_wh = battery.charge_energy(5000, 0) + + # No charging should happen if battery is full + assert charged_wh == 0 + assert loss_wh == 0 + assert battery.current_soc_percentage() == 80, "SoC should remain at max_soc" + + +def test_battery_discharge_when_empty(setup_pv_battery): + battery = setup_pv_battery + battery.soc_wh = battery.min_soc_wh # Set battery to minimum SOC + discharged_wh, loss_wh = battery.discharge_energy(5000, 0) + + # No discharge should happen if battery is at min SOC + assert discharged_wh == 0 + assert loss_wh == 0 + assert battery.current_soc_percentage() == 20, "SoC should remain at min_soc" + + +def test_battery_discharge_exactly_min_soc(setup_pv_battery): + battery = setup_pv_battery + battery.soc_wh = battery.min_soc_wh # Set battery to exactly min SOC + discharged_wh, loss_wh = battery.discharge_energy(1000, 0) + + # Battery should not go below the min SOC + assert discharged_wh == 0 + assert battery.current_soc_percentage() == 20 # SOC should remain at min_SOC + + +def test_battery_charge_exactly_max_soc(setup_pv_battery): + battery = setup_pv_battery + battery.soc_wh = battery.max_soc_wh # Set battery to exactly max SOC + charged_wh, loss_wh = battery.charge_energy(1000, 0) + + # Battery should not exceed the max SOC + assert charged_wh == 0 + assert battery.current_soc_percentage() == 80 # SOC should remain at max_SOC + + +def test_battery_reset_function(setup_pv_battery): + battery = setup_pv_battery + battery.soc_wh = 8000 # Change the SOC to some value + battery.reset() + + # After reset, SOC should be equal to the initial value + assert battery.current_soc_percentage() == battery.initial_soc_percentage + + +def test_soc_limits(setup_pv_battery): + battery = setup_pv_battery + + # Manually set SoC above max limit + battery.soc_wh = battery.max_soc_wh + 1000 + battery.soc_wh = min(battery.soc_wh, battery.max_soc_wh) + assert battery.current_soc_percentage() <= 80, "SoC should not exceed max_soc" + + # Manually set SoC below min limit + battery.soc_wh = battery.min_soc_wh - 1000 + battery.soc_wh = max(battery.soc_wh, battery.min_soc_wh) + assert battery.current_soc_percentage() >= 20, "SoC should not drop below min_soc" + + +def test_max_charge_power_w(setup_pv_battery): + battery = setup_pv_battery + battery.setup() + assert ( + battery.parameters.max_charge_power_w == 8000 + ), "Default max charge power should be 5000W, We ask for 8000W here" + + +def test_charge_energy_within_limits(setup_pv_battery): + battery = setup_pv_battery + battery.setup() + initial_soc_wh = battery.soc_wh + + charged_wh, losses_wh = battery.charge_energy(wh=4000, hour=1) + + assert charged_wh > 0, "Charging should add energy" + assert losses_wh >= 0, "Losses should not be negative" + assert battery.soc_wh > initial_soc_wh, "State of charge should increase after charging" + assert battery.soc_wh <= battery.max_soc_wh, "SOC should not exceed max SOC" + + +def test_charge_energy_exceeds_capacity(setup_pv_battery): + battery = setup_pv_battery + battery.setup() + initial_soc_wh = battery.soc_wh + + # Try to overcharge beyond max capacity + charged_wh, losses_wh = battery.charge_energy(wh=20000, hour=2) + + assert ( + charged_wh + initial_soc_wh <= battery.max_soc_wh + ), "Charging should not exceed max capacity" + assert losses_wh >= 0, "Losses should not be negative" + assert battery.soc_wh == battery.max_soc_wh, "SOC should be at max after overcharge attempt" + + +def test_charge_energy_not_allowed_hour(setup_pv_battery): + battery = setup_pv_battery + battery.setup() + + # Disable charging for all hours + battery.set_charge_per_hour(np.zeros(battery.hours)) + + charged_wh, losses_wh = battery.charge_energy(wh=4000, hour=3) + + assert charged_wh == 0, "No energy should be charged in disallowed hours" + assert losses_wh == 0, "No losses should occur if charging is not allowed" + assert ( + battery.soc_wh == (battery.parameters.initial_soc_percentage / 100) * battery.capacity_wh + ), "SOC should remain unchanged" + + +def test_charge_energy_relative_power(setup_pv_battery): + battery = setup_pv_battery + battery.setup() + + relative_power = 0.5 # 50% of max charge power + charged_wh, losses_wh = battery.charge_energy(wh=None, hour=4, relative_power=relative_power) + + assert charged_wh > 0, "Charging should occur with relative power" + assert losses_wh >= 0, "Losses should not be negative" + assert ( + charged_wh <= battery.max_charge_power_w * relative_power + ), "Charging should respect relative power limit" + assert battery.soc_wh > 0, "SOC should increase after charging" + + +@pytest.fixture +def setup_car_battery(): + from akkudoktoreos.devices.battery import ElectricVehicleParameters + + params = ElectricVehicleParameters( + capacity_wh=40000, + initial_soc_percentage=60, + min_soc_percentage=10, + max_soc_percentage=90, + max_charge_power_w=7000, + ) + battery = Battery(params, hours=24) + battery.reset() + return battery + + +def test_car_and_pv_battery_discharge_and_max_charge_power(setup_pv_battery, setup_car_battery): + pv_battery = setup_pv_battery + car_battery = setup_car_battery + + # Test discharge for PV battery + pv_discharged_wh, pv_loss_wh = pv_battery.discharge_energy(3000, 5) + assert pv_discharged_wh > 0, "PV battery should discharge energy" + assert ( + pv_battery.current_soc_percentage() >= pv_battery.parameters.min_soc_percentage + ), "PV battery SOC should stay above min SOC" + assert ( + pv_battery.parameters.max_charge_power_w == 8000 + ), "PV battery max charge power should remain as defined" + + # Test discharge for car battery + car_discharged_wh, car_loss_wh = car_battery.discharge_energy(5000, 10) + assert car_discharged_wh > 0, "Car battery should discharge energy" + assert ( + car_battery.current_soc_percentage() >= car_battery.parameters.min_soc_percentage + ), "Car battery SOC should stay above min SOC" + assert ( + car_battery.parameters.max_charge_power_w == 7000 + ), "Car battery max charge power should remain as defined" diff --git a/tests/test_class_akku.py b/tests/test_class_akku.py deleted file mode 100644 index b9144d1..0000000 --- a/tests/test_class_akku.py +++ /dev/null @@ -1,143 +0,0 @@ -import unittest - -from akkudoktoreos.devices.battery import PVAkku, PVAkkuParameters - - -class TestPVAkku(unittest.TestCase): - def setUp(self): - # Initializing common parameters for tests - self.kapazitaet_wh = 10000 # 10,000 Wh capacity - self.lade_effizienz = 0.88 - self.entlade_effizienz = 0.88 - self.min_soc_prozent = 20 # Minimum SoC is 20% - self.max_soc_prozent = 80 # Maximum SoC is 80% - - def test_initial_state_of_charge(self): - akku = PVAkku( - PVAkkuParameters( - kapazitaet_wh=self.kapazitaet_wh, - start_soc_prozent=50, - min_soc_prozent=self.min_soc_prozent, - max_soc_prozent=self.max_soc_prozent, - ), - hours=1, - ) - self.assertEqual(akku.ladezustand_in_prozent(), 50.0, "Initial SoC should be 50%") - - def test_discharge_below_min_soc(self): - akku = PVAkku( - PVAkkuParameters( - kapazitaet_wh=self.kapazitaet_wh, - start_soc_prozent=50, - min_soc_prozent=self.min_soc_prozent, - max_soc_prozent=self.max_soc_prozent, - ), - hours=1, - ) - akku.reset() - # Try to discharge more energy than available above min_soc - abgegeben_wh, verlust_wh = akku.energie_abgeben(5000, 0) # Try to discharge 5000 Wh - expected_soc = self.min_soc_prozent # SoC should not drop below min_soc - self.assertEqual( - akku.ladezustand_in_prozent(), - expected_soc, - "SoC should not drop below min_soc after discharge", - ) - self.assertEqual(abgegeben_wh, 2640.0, "The energy discharged should be limited by min_soc") - - def test_charge_above_max_soc(self): - akku = PVAkku( - PVAkkuParameters( - kapazitaet_wh=self.kapazitaet_wh, - start_soc_prozent=50, - min_soc_prozent=self.min_soc_prozent, - max_soc_prozent=self.max_soc_prozent, - ), - hours=1, - ) - akku.reset() - # Try to charge more energy than available up to max_soc - geladen_wh, verlust_wh = akku.energie_laden(5000, 0) # Try to charge 5000 Wh - expected_soc = self.max_soc_prozent # SoC should not exceed max_soc - self.assertEqual( - akku.ladezustand_in_prozent(), - expected_soc, - "SoC should not exceed max_soc after charge", - ) - self.assertEqual(geladen_wh, 3000.0, "The energy charged should be limited by max_soc") - - def test_charging_at_max_soc(self): - akku = PVAkku( - PVAkkuParameters( - kapazitaet_wh=self.kapazitaet_wh, - start_soc_prozent=80, - min_soc_prozent=self.min_soc_prozent, - max_soc_prozent=self.max_soc_prozent, - ), - hours=1, - ) - akku.reset() - # Try to charge when SoC is already at max_soc - geladen_wh, verlust_wh = akku.energie_laden(5000, 0) - self.assertEqual(geladen_wh, 0.0, "No energy should be charged when at max_soc") - self.assertEqual( - akku.ladezustand_in_prozent(), - self.max_soc_prozent, - "SoC should remain at max_soc", - ) - - def test_discharging_at_min_soc(self): - akku = PVAkku( - PVAkkuParameters( - kapazitaet_wh=self.kapazitaet_wh, - start_soc_prozent=20, - min_soc_prozent=self.min_soc_prozent, - max_soc_prozent=self.max_soc_prozent, - ), - hours=1, - ) - akku.reset() - # Try to discharge when SoC is already at min_soc - abgegeben_wh, verlust_wh = akku.energie_abgeben(5000, 0) - self.assertEqual(abgegeben_wh, 0.0, "No energy should be discharged when at min_soc") - self.assertEqual( - akku.ladezustand_in_prozent(), - self.min_soc_prozent, - "SoC should remain at min_soc", - ) - - def test_soc_limits(self): - # Test to ensure that SoC never exceeds max_soc or drops below min_soc - akku = PVAkku( - PVAkkuParameters( - kapazitaet_wh=self.kapazitaet_wh, - start_soc_prozent=50, - min_soc_prozent=self.min_soc_prozent, - max_soc_prozent=self.max_soc_prozent, - ), - hours=1, - ) - akku.reset() - akku.soc_wh = ( - self.max_soc_prozent / 100 - ) * self.kapazitaet_wh + 1000 # Manually set SoC above max limit - akku.soc_wh = min(akku.soc_wh, akku.max_soc_wh) - self.assertLessEqual( - akku.ladezustand_in_prozent(), - self.max_soc_prozent, - "SoC should not exceed max_soc", - ) - - akku.soc_wh = ( - self.min_soc_prozent / 100 - ) * self.kapazitaet_wh - 1000 # Manually set SoC below min limit - akku.soc_wh = max(akku.soc_wh, akku.min_soc_wh) - self.assertGreaterEqual( - akku.ladezustand_in_prozent(), - self.min_soc_prozent, - "SoC should not drop below min_soc", - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_class_ems.py b/tests/test_class_ems.py index e4d1a9f..429e33c 100644 --- a/tests/test_class_ems.py +++ b/tests/test_class_ems.py @@ -8,7 +8,11 @@ from akkudoktoreos.core.ems import ( SimulationResult, get_ems, ) -from akkudoktoreos.devices.battery import EAutoParameters, PVAkku, PVAkkuParameters +from akkudoktoreos.devices.battery import ( + Battery, + ElectricVehicleParameters, + SolarPanelBatteryParameters, +) from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters from akkudoktoreos.devices.inverter import Inverter, InverterParameters @@ -25,8 +29,10 @@ def create_ems_instance() -> EnergieManagementSystem: assert config_eos.prediction_hours is not None # Initialize the battery and the inverter - akku = PVAkku( - PVAkkuParameters(kapazitaet_wh=5000, start_soc_prozent=80, min_soc_prozent=10), + akku = Battery( + SolarPanelBatteryParameters( + capacity_wh=5000, initial_soc_percentage=80, min_soc_percentage=10 + ), hours=config_eos.prediction_hours, ) akku.reset() @@ -43,8 +49,10 @@ def create_ems_instance() -> EnergieManagementSystem: home_appliance.set_starting_time(2) # Example initialization of electric car battery - eauto = PVAkku( - EAutoParameters(kapazitaet_wh=26400, start_soc_prozent=10, min_soc_prozent=10), + eauto = Battery( + ElectricVehicleParameters( + capacity_wh=26400, initial_soc_percentage=10, min_soc_percentage=10 + ), hours=config_eos.prediction_hours, ) eauto.set_charge_per_hour(np.full(config_eos.prediction_hours, 1)) diff --git a/tests/test_class_ems_2.py b/tests/test_class_ems_2.py index fe1993d..edc32de 100644 --- a/tests/test_class_ems_2.py +++ b/tests/test_class_ems_2.py @@ -7,7 +7,11 @@ from akkudoktoreos.core.ems import ( EnergieManagementSystemParameters, get_ems, ) -from akkudoktoreos.devices.battery import EAutoParameters, PVAkku, PVAkkuParameters +from akkudoktoreos.devices.battery import ( + Battery, + ElectricVehicleParameters, + SolarPanelBatteryParameters, +) from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters from akkudoktoreos.devices.inverter import Inverter, InverterParameters @@ -24,8 +28,10 @@ def create_ems_instance() -> EnergieManagementSystem: assert config_eos.prediction_hours is not None # Initialize the battery and the inverter - akku = PVAkku( - PVAkkuParameters(kapazitaet_wh=5000, start_soc_prozent=80, min_soc_prozent=10), + akku = Battery( + SolarPanelBatteryParameters( + capacity_wh=5000, initial_soc_percentage=80, min_soc_percentage=10 + ), hours=config_eos.prediction_hours, ) akku.reset() @@ -42,8 +48,10 @@ def create_ems_instance() -> EnergieManagementSystem: home_appliance.set_starting_time(2) # Example initialization of electric car battery - eauto = PVAkku( - EAutoParameters(kapazitaet_wh=26400, start_soc_prozent=100, min_soc_prozent=100), + eauto = Battery( + ElectricVehicleParameters( + capacity_wh=26400, initial_soc_percentage=100, min_soc_percentage=100 + ), hours=config_eos.prediction_hours, ) diff --git a/tests/test_inverter.py b/tests/test_inverter.py index 4d7b76b..6c70155 100644 --- a/tests/test_inverter.py +++ b/tests/test_inverter.py @@ -8,8 +8,8 @@ from akkudoktoreos.devices.inverter import Inverter, InverterParameters @pytest.fixture def mock_battery(): mock_battery = Mock() - mock_battery.energie_laden = Mock(return_value=(0.0, 0.0)) - mock_battery.energie_abgeben = Mock(return_value=(0.0, 0.0)) + mock_battery.charge_energy = Mock(return_value=(0.0, 0.0)) + mock_battery.discharge_energy = Mock(return_value=(0.0, 0.0)) return mock_battery @@ -20,7 +20,7 @@ def inverter(mock_battery): def test_process_energy_excess_generation(inverter, mock_battery): # Battery charges 100 Wh with 10 Wh loss - mock_battery.energie_laden.return_value = (100.0, 10.0) + mock_battery.charge_energy.return_value = (100.0, 10.0) generation = 600.0 consumption = 200.0 hour = 12 @@ -33,7 +33,7 @@ def test_process_energy_excess_generation(inverter, mock_battery): assert grid_import == 0.0 # No grid draw assert losses == 10.0 # Battery charging losses assert self_consumption == 200.0 # All consumption is met - mock_battery.energie_laden.assert_called_once_with(400.0, hour) + mock_battery.charge_energy.assert_called_once_with(400.0, hour) def test_process_energy_generation_equals_consumption(inverter, mock_battery): @@ -50,12 +50,12 @@ def test_process_energy_generation_equals_consumption(inverter, mock_battery): assert losses == 0.0 # No losses assert self_consumption == 300.0 # All consumption is met with generation - mock_battery.energie_laden.assert_called_once_with(0.0, hour) + mock_battery.charge_energy.assert_called_once_with(0.0, hour) def test_process_energy_battery_discharges(inverter, mock_battery): # Battery discharges 100 Wh with 10 Wh loss already accounted for in the discharge - mock_battery.energie_abgeben.return_value = (100.0, 10.0) + mock_battery.discharge_energy.return_value = (100.0, 10.0) generation = 100.0 consumption = 250.0 hour = 12 @@ -70,12 +70,12 @@ def test_process_energy_battery_discharges(inverter, mock_battery): ) # Grid supplies remaining shortfall after battery discharge assert losses == 10.0 # Discharge losses assert self_consumption == 200.0 # Generation + battery discharge - mock_battery.energie_abgeben.assert_called_once_with(150.0, hour) + mock_battery.discharge_energy.assert_called_once_with(150.0, hour) def test_process_energy_battery_empty(inverter, mock_battery): # Battery is empty, so no energy can be discharged - mock_battery.energie_abgeben.return_value = (0.0, 0.0) + mock_battery.discharge_energy.return_value = (0.0, 0.0) generation = 100.0 consumption = 300.0 hour = 12 @@ -88,12 +88,12 @@ def test_process_energy_battery_empty(inverter, mock_battery): assert grid_import == pytest.approx(200.0, rel=1e-2) # Grid has to cover the full shortfall assert losses == 0.0 # No losses as the battery didn't discharge assert self_consumption == 100.0 # Only generation is consumed - mock_battery.energie_abgeben.assert_called_once_with(200.0, hour) + mock_battery.discharge_energy.assert_called_once_with(200.0, hour) def test_process_energy_battery_full_at_start(inverter, mock_battery): # Battery is full, so no charging happens - mock_battery.energie_laden.return_value = (0.0, 0.0) + mock_battery.charge_energy.return_value = (0.0, 0.0) generation = 500.0 consumption = 200.0 hour = 12 @@ -108,12 +108,12 @@ def test_process_energy_battery_full_at_start(inverter, mock_battery): assert grid_import == 0.0 # No grid draw assert losses == 0.0 # No losses assert self_consumption == 200.0 # Only consumption is met - mock_battery.energie_laden.assert_called_once_with(300.0, hour) + mock_battery.charge_energy.assert_called_once_with(300.0, hour) def test_process_energy_insufficient_generation_no_battery(inverter, mock_battery): # Insufficient generation and no battery discharge - mock_battery.energie_abgeben.return_value = (0.0, 0.0) + mock_battery.discharge_energy.return_value = (0.0, 0.0) generation = 100.0 consumption = 500.0 hour = 12 @@ -126,12 +126,12 @@ def test_process_energy_insufficient_generation_no_battery(inverter, mock_batter assert grid_import == pytest.approx(400.0, rel=1e-2) # Grid supplies the shortfall assert losses == 0.0 # No losses assert self_consumption == 100.0 # Only generation is consumed - mock_battery.energie_abgeben.assert_called_once_with(400.0, hour) + mock_battery.discharge_energy.assert_called_once_with(400.0, hour) def test_process_energy_insufficient_generation_battery_assists(inverter, mock_battery): # Battery assists with some discharge to cover the shortfall - mock_battery.energie_abgeben.return_value = ( + mock_battery.discharge_energy.return_value = ( 50.0, 5.0, ) # Battery discharges 50 Wh with 5 Wh loss @@ -149,12 +149,12 @@ def test_process_energy_insufficient_generation_battery_assists(inverter, mock_b ) # Grid supplies the remaining shortfall after battery discharge assert losses == 5.0 # Discharge losses assert self_consumption == 250.0 # Generation + battery discharge - mock_battery.energie_abgeben.assert_called_once_with(200.0, hour) + mock_battery.discharge_energy.assert_called_once_with(200.0, hour) def test_process_energy_zero_generation(inverter, mock_battery): # Zero generation, full reliance on battery and grid - mock_battery.energie_abgeben.return_value = ( + mock_battery.discharge_energy.return_value = ( 100.0, 5.0, ) # Battery discharges 100 Wh with 5 Wh loss @@ -170,12 +170,12 @@ def test_process_energy_zero_generation(inverter, mock_battery): assert grid_import == pytest.approx(200.0, rel=1e-2) # Grid supplies the remaining shortfall assert losses == 5.0 # Discharge losses assert self_consumption == 100.0 # Only battery discharge is consumed - mock_battery.energie_abgeben.assert_called_once_with(300.0, hour) + mock_battery.discharge_energy.assert_called_once_with(300.0, hour) def test_process_energy_zero_consumption(inverter, mock_battery): # Generation exceeds consumption, but consumption is zero - mock_battery.energie_laden.return_value = (100.0, 10.0) + mock_battery.charge_energy.return_value = (100.0, 10.0) generation = 500.0 consumption = 0.0 hour = 12 @@ -188,7 +188,7 @@ def test_process_energy_zero_consumption(inverter, mock_battery): assert grid_import == 0.0 # No grid draw as no consumption assert losses == 10.0 # Charging losses assert self_consumption == 0.0 # Zero consumption - mock_battery.energie_laden.assert_called_once_with(500.0, hour) + mock_battery.charge_energy.assert_called_once_with(500.0, hour) def test_process_energy_zero_generation_zero_consumption(inverter, mock_battery): @@ -207,7 +207,7 @@ def test_process_energy_zero_generation_zero_consumption(inverter, mock_battery) def test_process_energy_partial_battery_discharge(inverter, mock_battery): - mock_battery.energie_abgeben.return_value = (50.0, 5.0) + mock_battery.discharge_energy.return_value = (50.0, 5.0) generation = 200.0 consumption = 400.0 hour = 12 @@ -226,7 +226,7 @@ def test_process_energy_partial_battery_discharge(inverter, mock_battery): def test_process_energy_consumption_exceeds_max_no_battery(inverter, mock_battery): # Battery is empty, and consumption is much higher than the inverter's max power - mock_battery.energie_abgeben.return_value = (0.0, 0.0) + mock_battery.discharge_energy.return_value = (0.0, 0.0) generation = 100.0 consumption = 1000.0 # Exceeds the inverter's max power hour = 12 @@ -239,12 +239,12 @@ def test_process_energy_consumption_exceeds_max_no_battery(inverter, mock_batter assert grid_import == pytest.approx(900.0, rel=1e-2) # Grid covers the remaining shortfall assert losses == 0.0 # No losses as the battery didn’t assist assert self_consumption == 100.0 # Only the generation is consumed, maxing out the inverter - mock_battery.energie_abgeben.assert_called_once_with(400.0, hour) + mock_battery.discharge_energy.assert_called_once_with(400.0, hour) def test_process_energy_zero_generation_full_battery_high_consumption(inverter, mock_battery): # Full battery, no generation, and high consumption - mock_battery.energie_abgeben.return_value = (500.0, 10.0) + mock_battery.discharge_energy.return_value = (500.0, 10.0) generation = 0.0 consumption = 600.0 hour = 12 @@ -259,4 +259,4 @@ def test_process_energy_zero_generation_full_battery_high_consumption(inverter, ) # Grid covers remaining shortfall after battery discharge assert losses == 10.0 # Battery discharge losses assert self_consumption == 500.0 # Battery fully discharges to meet consumption - mock_battery.energie_abgeben.assert_called_once_with(500.0, hour) + mock_battery.discharge_energy.assert_called_once_with(500.0, hour) diff --git a/tests/testdata/optimize_input_1.json b/tests/testdata/optimize_input_1.json index e1dab61..a7ba9fc 100644 --- a/tests/testdata/optimize_input_1.json +++ b/tests/testdata/optimize_input_1.json @@ -26,21 +26,21 @@ ] }, "pv_akku": { - "kapazitaet_wh": 26400, - "max_ladeleistung_w": 5000, - "start_soc_prozent": 80, - "min_soc_prozent": 15 + "capacity_wh": 26400, + "max_charge_power_w": 5000, + "initial_soc_percentage": 80, + "min_soc_percentage": 15 }, - "inverter": { - "max_power_wh": 10000 + "wechselrichter": { + "max_leistung_wh": 10000 }, "eauto": { - "kapazitaet_wh": 60000, - "lade_effizienz": 0.95, - "entlade_effizienz": 1.0, - "max_ladeleistung_w": 11040, - "start_soc_prozent": 54, - "min_soc_prozent": 0 + "capacity_wh": 60000, + "charging_efficiency": 0.95, + "discharging_efficiency": 1.0, + "max_charge_power_w": 11040, + "initial_soc_percentage": 54, + "min_soc_percentage": 0 }, "temperature_forecast": [ 18.3, 17.8, 16.9, 16.2, 15.6, 15.1, 14.6, 14.2, 14.3, 14.8, 15.7, 16.7, 17.4, diff --git a/tests/testdata/optimize_input_2.json b/tests/testdata/optimize_input_2.json index 9a715d6..930d210 100644 --- a/tests/testdata/optimize_input_2.json +++ b/tests/testdata/optimize_input_2.json @@ -26,16 +26,16 @@ ] }, "pv_akku": { - "kapazitaet_wh": 26400, - "start_soc_prozent": 80, - "min_soc_prozent": 15 + "capacity_wh": 26400, + "initial_soc_percentage": 80, + "min_soc_percentage": 15 }, "eauto": { - "kapazitaet_wh": 60000, - "lade_effizienz": 0.95, - "max_ladeleistung_w": 11040, - "start_soc_prozent": 5, - "min_soc_prozent": 80 + "capacity_wh": 60000, + "charging_efficiency": 0.95, + "max_charge_power_w": 11040, + "initial_soc_percentage": 5, + "min_soc_percentage": 80 }, "dishwasher" :{ "consumption_wh": 5000, diff --git a/tests/testdata/optimize_result_1.json b/tests/testdata/optimize_result_1.json index fc823d0..86906dd 100644 --- a/tests/testdata/optimize_result_1.json +++ b/tests/testdata/optimize_result_1.json @@ -617,13 +617,13 @@ 1, 1 ], - "entlade_effizienz": 1.0, + "discharging_efficiency": 1.0, "hours": 48, - "kapazitaet_wh": 60000, - "lade_effizienz": 0.95, - "max_ladeleistung_w": 11040, + "capacity_wh": 60000, + "charging_efficiency": 0.95, + "max_charge_power_w": 11040, "soc_wh": 32400.000000000004, - "start_soc_prozent": 54 + "initial_soc_percentage": 54 }, "start_solution": [ 18.0, @@ -676,4 +676,4 @@ 10.0 ], "washingstart": null -} \ No newline at end of file +} diff --git a/tests/testdata/optimize_result_2.json b/tests/testdata/optimize_result_2.json index 4c3b3a8..3db1748 100644 --- a/tests/testdata/optimize_result_2.json +++ b/tests/testdata/optimize_result_2.json @@ -666,13 +666,13 @@ 1, 1 ], - "entlade_effizienz": 1.0, + "discharging_efficiency": 1.0, "hours": 48, - "kapazitaet_wh": 60000, - "lade_effizienz": 0.95, - "max_ladeleistung_w": 11040, + "capacity_wh": 60000, + "charging_efficiency": 0.95, + "max_charge_power_w": 11040, "soc_wh": 60000.0, - "start_soc_prozent": 5 + "initial_soc_percentage": 5 }, "start_solution": [ 0.0, @@ -774,4 +774,4 @@ 14.0 ], "washingstart": 14 -} \ No newline at end of file +} diff --git a/tests/testdata/optimize_result_2_full.json b/tests/testdata/optimize_result_2_full.json index ad0bef1..23d799a 100644 --- a/tests/testdata/optimize_result_2_full.json +++ b/tests/testdata/optimize_result_2_full.json @@ -666,13 +666,13 @@ 1, 1 ], - "entlade_effizienz": 1.0, + "discharging_efficiency": 1.0, "hours": 48, - "kapazitaet_wh": 60000, - "lade_effizienz": 0.95, - "max_ladeleistung_w": 11040, + "capacity_wh": 60000, + "charging_efficiency": 0.95, + "max_charge_power_w": 11040, "soc_wh": 60000.0, - "start_soc_prozent": 5 + "initial_soc_percentage": 5 }, "start_solution": [ 12.0, @@ -774,4 +774,4 @@ 14.0 ], "washingstart": 14 -} \ No newline at end of file +}