diff --git a/docs/akkudoktoreos/openapi.json b/docs/akkudoktoreos/openapi.json index 79f2083..d8f5929 100644 --- a/docs/akkudoktoreos/openapi.json +++ b/docs/akkudoktoreos/openapi.json @@ -2275,15 +2275,7 @@ ], "title": "Optimization Ev Available Charge Rates Percent", "description": "Charge rates available for the EV in percent of maximum charge.", - "default": [ - 0.0, - 0.375, - 0.5, - 0.625, - 0.75, - 0.875, - 1.0 - ] + "default": [0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0] }, "battery_provider": { "anyOf": [ @@ -2780,9 +2772,7 @@ } }, "type": "object", - "required": [ - "kapazitaet_wh" - ], + "required": ["kapazitaet_wh"], "title": "EAutoParameters" }, "EAutoResult": { @@ -2929,10 +2919,7 @@ } }, "type": "object", - "required": [ - "temperature", - "pvpower" - ], + "required": ["temperature", "pvpower"], "title": "ForecastResponse" }, "GesamtlastRequest": { @@ -2954,11 +2941,7 @@ } }, "type": "object", - "required": [ - "year_energy", - "measured_data", - "hours" - ], + "required": ["year_energy", "measured_data", "hours"], "title": "GesamtlastRequest" }, "HTTPValidationError": { @@ -2990,12 +2973,21 @@ } }, "type": "object", - "required": [ - "consumption_wh", - "duration_h" - ], + "required": ["consumption_wh", "duration_h"], "title": "HomeApplianceParameters" }, + "InverterParameters": { + "properties": { + "max_power_wh": { + "type": "number", + "exclusiveMinimum": 0.0, + "title": "Max Power Wh", + "default": 10000 + } + }, + "type": "object", + "title": "InverterParameters" + }, "OptimizationParameters": { "properties": { "ems": { @@ -3004,10 +2996,10 @@ "pv_akku": { "$ref": "#/components/schemas/PVAkkuParameters" }, - "wechselrichter": { - "$ref": "#/components/schemas/WechselrichterParameters", + "inverter": { + "$ref": "#/components/schemas/InverterParameters", "default": { - "max_leistung_wh": 10000.0 + "max_power_wh": 10000.0 } }, "eauto": { @@ -3062,11 +3054,7 @@ } }, "type": "object", - "required": [ - "ems", - "pv_akku", - "eauto" - ], + "required": ["ems", "pv_akku", "eauto"], "title": "OptimizationParameters" }, "OptimizeResponse": { @@ -3225,9 +3213,7 @@ } }, "type": "object", - "required": [ - "kapazitaet_wh" - ], + "required": ["kapazitaet_wh"], "title": "PVAkkuParameters" }, "SettingsEOS": { @@ -4994,15 +4980,7 @@ ], "title": "Optimization Ev Available Charge Rates Percent", "description": "Charge rates available for the EV in percent of maximum charge.", - "default": [ - 0.0, - 0.375, - 0.5, - 0.625, - 0.75, - 0.875, - 1.0 - ] + "default": [0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0] }, "battery_provider": { "anyOf": [ @@ -5493,25 +5471,9 @@ } }, "type": "object", - "required": [ - "loc", - "msg", - "type" - ], + "required": ["loc", "msg", "type"], "title": "ValidationError" - }, - "WechselrichterParameters": { - "properties": { - "max_leistung_wh": { - "type": "number", - "exclusiveMinimum": 0.0, - "title": "Max Leistung Wh", - "default": 10000 - } - }, - "type": "object", - "title": "WechselrichterParameters" } } } -} \ No newline at end of file +} diff --git a/src/akkudoktoreos/core/ems.py b/src/akkudoktoreos/core/ems.py index e54dde0..1d274c9 100644 --- a/src/akkudoktoreos/core/ems.py +++ b/src/akkudoktoreos/core/ems.py @@ -10,7 +10,7 @@ from akkudoktoreos.core.coreabc import ConfigMixin, PredictionMixin, SingletonMi from akkudoktoreos.core.pydantic import PydanticBaseModel from akkudoktoreos.devices.battery import PVAkku from akkudoktoreos.devices.generic import HomeAppliance -from akkudoktoreos.devices.inverter import Wechselrichter +from akkudoktoreos.devices.inverter import Inverter from akkudoktoreos.utils.datetimeutil import to_datetime from akkudoktoreos.utils.logutil import get_logger from akkudoktoreos.utils.utils import NumpyEncoder @@ -155,7 +155,7 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda akku: Optional[PVAkku] = Field(default=None, description="TBD.") eauto: Optional[PVAkku] = Field(default=None, description="TBD.") home_appliance: Optional[HomeAppliance] = Field(default=None, description="TBD.") - wechselrichter: Optional[Wechselrichter] = Field(default=None, description="TBD.") + inverter: Optional[Inverter] = Field(default=None, description="TBD.") # ------------------------- # TODO: Move to devices @@ -170,7 +170,7 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda parameters: EnergieManagementSystemParameters, eauto: Optional[PVAkku] = None, home_appliance: Optional[HomeAppliance] = None, - wechselrichter: Optional[Wechselrichter] = None, + inverter: Optional[Inverter] = None, ) -> None: self.gesamtlast = np.array(parameters.gesamtlast, float) self.pv_prognose_wh = np.array(parameters.pv_prognose_wh, float) @@ -180,13 +180,13 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda if isinstance(parameters.einspeiseverguetung_euro_pro_wh, list) else np.full(len(self.gesamtlast), parameters.einspeiseverguetung_euro_pro_wh, float) ) - if wechselrichter is not None: - self.akku = wechselrichter.akku + if inverter is not None: + self.akku = inverter.akku else: self.akku = None self.eauto = eauto self.home_appliance = home_appliance - self.wechselrichter = wechselrichter + self.inverter = inverter self.ac_charge_hours = np.full(self.config.prediction_hours, 0.0) self.dc_charge_hours = np.full(self.config.prediction_hours, 1.0) self.ev_charge_hours = np.full(self.config.prediction_hours, 0.0) @@ -354,10 +354,10 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda netzeinspeisung, netzbezug, verluste, eigenverbrauch = (0.0, 0.0, 0.0, 0.0) if self.akku: self.akku.set_charge_allowed_for_hour(self.dc_charge_hours[stunde], stunde) - if self.wechselrichter: + if self.inverter: erzeugung = self.pv_prognose_wh[stunde] - netzeinspeisung, netzbezug, verluste, eigenverbrauch = ( - self.wechselrichter.energie_verarbeiten(erzeugung, verbrauch, stunde) + netzeinspeisung, netzbezug, verluste, eigenverbrauch = self.inverter.process_energy( + erzeugung, verbrauch, stunde ) # AC PV Battery Charge diff --git a/src/akkudoktoreos/devices/devices.py b/src/akkudoktoreos/devices/devices.py index 3cba64e..6194b67 100644 --- a/src/akkudoktoreos/devices/devices.py +++ b/src/akkudoktoreos/devices/devices.py @@ -9,7 +9,7 @@ from akkudoktoreos.core.coreabc import SingletonMixin from akkudoktoreos.devices.battery import PVAkku from akkudoktoreos.devices.devicesabc import DevicesBase from akkudoktoreos.devices.generic import HomeAppliance -from akkudoktoreos.devices.inverter import Wechselrichter +from akkudoktoreos.devices.inverter import Inverter from akkudoktoreos.utils.datetimeutil import to_duration from akkudoktoreos.utils.logutil import get_logger @@ -111,10 +111,10 @@ class Devices(SingletonMixin, DevicesBase): kosten_euro_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field( default=None, description="The costs in euros per hour." ) - netzbezug_wh_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field( + grid_import_wh_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field( default=None, description="The grid energy drawn in watt-hours per hour." ) - netzeinspeisung_wh_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field( + grid_export_wh_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field( default=None, description="The energy fed into the grid in watt-hours per hour." ) verluste_wh_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field( @@ -162,9 +162,7 @@ class Devices(SingletonMixin, DevicesBase): akku: ClassVar[PVAkku] = PVAkku(provider_id="GenericBattery") eauto: ClassVar[PVAkku] = PVAkku(provider_id="GenericBEV") home_appliance: ClassVar[HomeAppliance] = HomeAppliance(provider_id="GenericDishWasher") - wechselrichter: ClassVar[Wechselrichter] = Wechselrichter( - akku=akku, provider_id="GenericInverter" - ) + inverter: ClassVar[Inverter] = Inverter(akku=akku, provider_id="GenericInverter") def update_data(self) -> None: """Update device simulation data.""" @@ -172,12 +170,12 @@ class Devices(SingletonMixin, DevicesBase): self.akku.setup() self.eauto.setup() self.home_appliance.setup() - self.wechselrichter.setup() + self.inverter.setup() # Pre-allocate arrays for the results, optimized for speed self.last_wh_pro_stunde = np.full((self.total_hours), np.nan) - self.netzeinspeisung_wh_pro_stunde = np.full((self.total_hours), np.nan) - self.netzbezug_wh_pro_stunde = np.full((self.total_hours), np.nan) + self.grid_export_wh_pro_stunde = np.full((self.total_hours), np.nan) + self.grid_import_wh_pro_stunde = np.full((self.total_hours), np.nan) self.kosten_euro_pro_stunde = np.full((self.total_hours), np.nan) self.einnahmen_euro_pro_stunde = np.full((self.total_hours), np.nan) self.akku_soc_pro_stunde = np.full((self.total_hours), np.nan) @@ -219,60 +217,60 @@ class Devices(SingletonMixin, DevicesBase): einspeiseverguetung_euro_pro_wh_arr = np.full((self.total_hours), 0.078) for stunde_since_now in range(0, self.total_hours): - stunde = self.start_datetime.hour + stunde_since_now + hour = self.start_datetime.hour + stunde_since_now # Accumulate loads and PV generation - verbrauch = load_total_mean[stunde_since_now] + consumption = load_total_mean[stunde_since_now] self.verluste_wh_pro_stunde[stunde_since_now] = 0.0 # Home appliances if self.home_appliance: - ha_load = self.home_appliance.get_load_for_hour(stunde) - verbrauch += ha_load + ha_load = self.home_appliance.get_load_for_hour(hour) + consumption += ha_load self.home_appliance_wh_per_hour[stunde_since_now] = ha_load # E-Auto handling if self.eauto: - if self.ev_charge_hours[stunde] > 0: + if self.ev_charge_hours[hour] > 0: geladene_menge_eauto, verluste_eauto = self.eauto.energie_laden( - None, stunde, relative_power=self.ev_charge_hours[stunde] + None, hour, relative_power=self.ev_charge_hours[hour] ) - verbrauch += geladene_menge_eauto + 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() # Process inverter logic - netzeinspeisung, netzbezug, verluste, eigenverbrauch = (0.0, 0.0, 0.0, 0.0) + grid_export, grid_import, losses, self_consumption = (0.0, 0.0, 0.0, 0.0) if self.akku: - self.akku.set_charge_allowed_for_hour(self.dc_charge_hours[stunde], stunde) - if self.wechselrichter: - erzeugung = pvforecast_ac_power[stunde] - netzeinspeisung, netzbezug, verluste, eigenverbrauch = ( - self.wechselrichter.energie_verarbeiten(erzeugung, verbrauch, stunde) + self.akku.set_charge_allowed_for_hour(self.dc_charge_hours[hour], hour) + if self.inverter: + generation = pvforecast_ac_power[hour] + grid_export, grid_import, losses, self_consumption = self.inverter.process_energy( + generation, consumption, hour ) # AC PV Battery Charge - if self.akku and self.ac_charge_hours[stunde] > 0.0: - self.akku.set_charge_allowed_for_hour(1, stunde) + 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( - None, stunde, relative_power=self.ac_charge_hours[stunde] + None, hour, relative_power=self.ac_charge_hours[hour] ) # print(stunde, " ", geladene_menge, " ",self.ac_charge_hours[stunde]," ",self.akku.ladezustand_in_prozent()) - verbrauch += geladene_menge - netzbezug += geladene_menge + consumption += geladene_menge + grid_import += geladene_menge self.verluste_wh_pro_stunde[stunde_since_now] += verluste_wh - self.netzeinspeisung_wh_pro_stunde[stunde_since_now] = netzeinspeisung - self.netzbezug_wh_pro_stunde[stunde_since_now] = netzbezug - self.verluste_wh_pro_stunde[stunde_since_now] += verluste - self.last_wh_pro_stunde[stunde_since_now] = verbrauch + self.grid_export_wh_pro_stunde[stunde_since_now] = grid_export + self.grid_import_wh_pro_stunde[stunde_since_now] = grid_import + self.verluste_wh_pro_stunde[stunde_since_now] += losses + self.last_wh_pro_stunde[stunde_since_now] = consumption # Financial calculations self.kosten_euro_pro_stunde[stunde_since_now] = ( - netzbezug * self.strompreis_euro_pro_wh[stunde] + grid_import * self.strompreis_euro_pro_wh[hour] ) self.einnahmen_euro_pro_stunde[stunde_since_now] = ( - netzeinspeisung * self.einspeiseverguetung_euro_pro_wh_arr[stunde] + grid_export * self.einspeiseverguetung_euro_pro_wh_arr[hour] ) # Akku SOC tracking @@ -285,8 +283,8 @@ class Devices(SingletonMixin, DevicesBase): """Provides devices simulation output as a dictionary.""" out: Dict[str, Optional[Union[np.ndarray, float]]] = { "Last_Wh_pro_Stunde": self.last_wh_pro_stunde, - "Netzeinspeisung_Wh_pro_Stunde": self.netzeinspeisung_wh_pro_stunde, - "Netzbezug_Wh_pro_Stunde": self.netzbezug_wh_pro_stunde, + "grid_export_Wh_pro_Stunde": self.grid_export_wh_pro_stunde, + "grid_import_Wh_pro_Stunde": self.grid_import_wh_pro_stunde, "Kosten_Euro_pro_Stunde": self.kosten_euro_pro_stunde, "akku_soc_pro_stunde": self.akku_soc_pro_stunde, "Einnahmen_Euro_pro_Stunde": self.einnahmen_euro_pro_stunde, diff --git a/src/akkudoktoreos/devices/inverter.py b/src/akkudoktoreos/devices/inverter.py index 7dca389..a3b2c0d 100644 --- a/src/akkudoktoreos/devices/inverter.py +++ b/src/akkudoktoreos/devices/inverter.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Tuple from pydantic import BaseModel, Field @@ -9,14 +9,14 @@ from akkudoktoreos.utils.logutil import get_logger logger = get_logger(__name__) -class WechselrichterParameters(BaseModel): - max_leistung_wh: float = Field(default=10000, gt=0) +class InverterParameters(BaseModel): + max_power_wh: float = Field(default=10000, gt=0) -class Wechselrichter(DeviceBase): +class Inverter(DeviceBase): def __init__( self, - parameters: Optional[WechselrichterParameters] = None, + parameters: Optional[InverterParameters] = None, akku: Optional[PVAkku] = None, provider_id: Optional[str] = None, ): @@ -45,69 +45,55 @@ class Wechselrichter(DeviceBase): return if self.provider_id is not None: # Setup by configuration - self.max_leistung_wh = getattr(self.config, f"{self.prefix}_power_max") + self.max_power_wh = getattr(self.config, f"{self.prefix}_power_max") elif self.parameters is not None: # Setup by parameters - self.max_leistung_wh = ( - self.parameters.max_leistung_wh # Maximum power that the inverter can handle + self.max_power_wh = ( + self.parameters.max_power_wh # Maximum power that the inverter can handle ) else: error_msg = "Parameters and provider ID missing. Can't instantiate." logger.error(error_msg) raise ValueError(error_msg) - def energie_verarbeiten( - self, erzeugung: float, verbrauch: float, hour: int - ) -> tuple[float, float, float, float]: - verluste = 0.0 # Losses during processing - netzeinspeisung = 0.0 # Grid feed-in - netzbezug = 0.0 # Grid draw - eigenverbrauch = 0.0 # Self-consumption + def process_energy( + self, generation: float, consumption: float, hour: int + ) -> Tuple[float, float, float, float]: + losses = 0.0 + grid_export = 0.0 + grid_import = 0.0 + self_consumption = 0.0 - if erzeugung >= verbrauch: - if verbrauch > self.max_leistung_wh: - # If consumption exceeds maximum inverter power - verluste += erzeugung - self.max_leistung_wh - restleistung_nach_verbrauch = self.max_leistung_wh - verbrauch - netzbezug = -restleistung_nach_verbrauch # Negative indicates feeding into the grid - eigenverbrauch = self.max_leistung_wh - else: - # Remaining power after consumption - restleistung_nach_verbrauch = erzeugung - verbrauch + if generation >= consumption: + # Case 1: Sufficient or excess generation + actual_consumption = min(consumption, self.max_power_wh) + remaining_energy = generation - actual_consumption - # Load battery with excess energy - geladene_energie, verluste_laden_akku = self.akku.energie_laden( - restleistung_nach_verbrauch, hour - ) - rest_überschuss = restleistung_nach_verbrauch - ( - geladene_energie + verluste_laden_akku - ) + # Charge battery with excess energy + charged_energy, charging_losses = self.akku.energie_laden(remaining_energy, hour) + losses += charging_losses - # Feed-in to the grid based on remaining capacity - if rest_überschuss > self.max_leistung_wh - verbrauch: - netzeinspeisung = self.max_leistung_wh - verbrauch - verluste += rest_überschuss - netzeinspeisung - else: - netzeinspeisung = rest_überschuss + # Calculate remaining surplus after battery charge + remaining_surplus = remaining_energy - (charged_energy + charging_losses) + grid_export = min(remaining_surplus, self.max_power_wh - actual_consumption) - verluste += verluste_laden_akku - eigenverbrauch = verbrauch # Self-consumption is equal to the load + # If any remaining surplus can't be fed to the grid, count as losses + losses += max(remaining_surplus - grid_export, 0) + self_consumption = actual_consumption else: - benötigte_energie = verbrauch - erzeugung # Energy needed from external sources - max_akku_leistung = self.akku.max_ladeleistung_w # Maximum battery discharge power + # Case 2: Insufficient generation, cover shortfall + shortfall = consumption - generation + available_ac_power = max(self.max_power_wh - generation, 0) - # Calculate remaining AC power available - rest_ac_leistung = max(self.max_leistung_wh - erzeugung, 0) + # Discharge battery to cover shortfall, if possible + battery_discharge, discharge_losses = self.akku.energie_abgeben( + min(shortfall, available_ac_power), hour + ) + losses += discharge_losses - # Discharge energy from the battery based on need - if benötigte_energie < rest_ac_leistung: - aus_akku, akku_entladeverluste = self.akku.energie_abgeben(benötigte_energie, hour) - else: - aus_akku, akku_entladeverluste = self.akku.energie_abgeben(rest_ac_leistung, hour) + # Draw remaining required power from the grid (discharge_losses are already substraved in the battery) + grid_import = shortfall - battery_discharge + self_consumption = generation + battery_discharge - verluste += akku_entladeverluste # Include losses from battery discharge - netzbezug = benötigte_energie - aus_akku # Energy drawn from the grid - eigenverbrauch = erzeugung + aus_akku # Total self-consumption - - return netzeinspeisung, netzbezug, verluste, eigenverbrauch + return grid_export, grid_import, losses, self_consumption diff --git a/src/akkudoktoreos/optimization/genetic.py b/src/akkudoktoreos/optimization/genetic.py index 96fc5f9..ef0b298 100644 --- a/src/akkudoktoreos/optimization/genetic.py +++ b/src/akkudoktoreos/optimization/genetic.py @@ -19,7 +19,7 @@ from akkudoktoreos.devices.battery import ( PVAkkuParameters, ) from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters -from akkudoktoreos.devices.inverter import Wechselrichter, WechselrichterParameters +from akkudoktoreos.devices.inverter import Inverter, InverterParameters from akkudoktoreos.utils.utils import NumpyEncoder from akkudoktoreos.visualize import visualisiere_ergebnisse @@ -27,7 +27,7 @@ from akkudoktoreos.visualize import visualisiere_ergebnisse class OptimizationParameters(BaseModel): ems: EnergieManagementSystemParameters pv_akku: PVAkkuParameters - wechselrichter: WechselrichterParameters = WechselrichterParameters() + inverter: InverterParameters = InverterParameters() eauto: Optional[EAutoParameters] dishwasher: Optional[HomeApplianceParameters] = None temperature_forecast: Optional[list[float]] = Field( @@ -488,10 +488,9 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi ) # Initialize the inverter and energy management system - wr = Wechselrichter(parameters.wechselrichter, akku) self.ems.set_parameters( parameters.ems, - wechselrichter=wr, + inverter=Inverter(parameters.inverter, akku), eauto=eauto, home_appliance=dishwasher, ) diff --git a/tests/test_class_ems.py b/tests/test_class_ems.py index 4b06af2..e4d1a9f 100644 --- a/tests/test_class_ems.py +++ b/tests/test_class_ems.py @@ -10,7 +10,7 @@ from akkudoktoreos.core.ems import ( ) from akkudoktoreos.devices.battery import EAutoParameters, PVAkku, PVAkkuParameters from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters -from akkudoktoreos.devices.inverter import Wechselrichter, WechselrichterParameters +from akkudoktoreos.devices.inverter import Inverter, InverterParameters start_hour = 1 @@ -30,7 +30,7 @@ def create_ems_instance() -> EnergieManagementSystem: hours=config_eos.prediction_hours, ) akku.reset() - wechselrichter = Wechselrichter(WechselrichterParameters(max_leistung_wh=10000), akku) + inverter = Inverter(InverterParameters(max_power_wh=10000), akku) # Household device (currently not used, set to None) home_appliance = HomeAppliance( @@ -216,7 +216,7 @@ def create_ems_instance() -> EnergieManagementSystem: preis_euro_pro_wh_akku=preis_euro_pro_wh_akku, gesamtlast=gesamtlast, ), - wechselrichter=wechselrichter, + inverter=inverter, eauto=eauto, home_appliance=home_appliance, ) diff --git a/tests/test_class_ems_2.py b/tests/test_class_ems_2.py index 2f7b05f..fe1993d 100644 --- a/tests/test_class_ems_2.py +++ b/tests/test_class_ems_2.py @@ -9,7 +9,7 @@ from akkudoktoreos.core.ems import ( ) from akkudoktoreos.devices.battery import EAutoParameters, PVAkku, PVAkkuParameters from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters -from akkudoktoreos.devices.inverter import Wechselrichter, WechselrichterParameters +from akkudoktoreos.devices.inverter import Inverter, InverterParameters start_hour = 0 @@ -29,7 +29,7 @@ def create_ems_instance() -> EnergieManagementSystem: hours=config_eos.prediction_hours, ) akku.reset() - wechselrichter = Wechselrichter(WechselrichterParameters(max_leistung_wh=10000), akku) + inverter = Inverter(InverterParameters(max_power_wh=10000), akku) # Household device (currently not used, set to None) home_appliance = HomeAppliance( @@ -121,7 +121,7 @@ def create_ems_instance() -> EnergieManagementSystem: preis_euro_pro_wh_akku=preis_euro_pro_wh_akku, gesamtlast=gesamtlast, ), - wechselrichter=wechselrichter, + inverter=inverter, eauto=eauto, home_appliance=home_appliance, ) diff --git a/tests/test_inverter.py b/tests/test_inverter.py new file mode 100644 index 0000000..4d7b76b --- /dev/null +++ b/tests/test_inverter.py @@ -0,0 +1,262 @@ +from unittest.mock import Mock + +import pytest + +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)) + return mock_battery + + +@pytest.fixture +def inverter(mock_battery): + return Inverter(InverterParameters(max_power_wh=500.0), akku=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) + generation = 600.0 + consumption = 200.0 + hour = 12 + + grid_export, grid_import, losses, self_consumption = inverter.process_energy( + generation, consumption, hour + ) + + assert grid_export == pytest.approx(290.0, rel=1e-2) # 290 Wh feed-in after battery charges + 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) + + +def test_process_energy_generation_equals_consumption(inverter, mock_battery): + generation = 300.0 + consumption = 300.0 + hour = 12 + + grid_export, grid_import, losses, self_consumption = inverter.process_energy( + generation, consumption, hour + ) + + assert grid_export == 0.0 # No feed-in as generation equals consumption + assert grid_import == 0.0 # No grid draw + 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) + + +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) + generation = 100.0 + consumption = 250.0 + hour = 12 + + grid_export, grid_import, losses, self_consumption = inverter.process_energy( + generation, consumption, hour + ) + + assert grid_export == 0.0 # No feed-in as generation is insufficient + assert grid_import == pytest.approx( + 50.0, rel=1e-2 + ) # 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) + + +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) + generation = 100.0 + consumption = 300.0 + hour = 12 + + grid_export, grid_import, losses, self_consumption = inverter.process_energy( + generation, consumption, hour + ) + + assert grid_export == 0.0 # No feed-in as generation is insufficient + 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) + + +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) + generation = 500.0 + consumption = 200.0 + hour = 12 + + grid_export, grid_import, losses, self_consumption = inverter.process_energy( + generation, consumption, hour + ) + + assert grid_export == pytest.approx( + 300.0, rel=1e-2 + ) # All excess energy should be fed into the grid + 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) + + +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) + generation = 100.0 + consumption = 500.0 + hour = 12 + + grid_export, grid_import, losses, self_consumption = inverter.process_energy( + generation, consumption, hour + ) + + assert grid_export == 0.0 # No feed-in as generation is insufficient + 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) + + +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 = ( + 50.0, + 5.0, + ) # Battery discharges 50 Wh with 5 Wh loss + generation = 200.0 + consumption = 400.0 + hour = 12 + + grid_export, grid_import, losses, self_consumption = inverter.process_energy( + generation, consumption, hour + ) + + assert grid_export == 0.0 # No feed-in as generation is insufficient + assert grid_import == pytest.approx( + 150.0, rel=1e-2 + ) # 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) + + +def test_process_energy_zero_generation(inverter, mock_battery): + # Zero generation, full reliance on battery and grid + mock_battery.energie_abgeben.return_value = ( + 100.0, + 5.0, + ) # Battery discharges 100 Wh with 5 Wh loss + generation = 0.0 + consumption = 300.0 + hour = 12 + + grid_export, grid_import, losses, self_consumption = inverter.process_energy( + generation, consumption, hour + ) + + assert grid_export == 0.0 # No feed-in as there is zero generation + 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) + + +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) + generation = 500.0 + consumption = 0.0 + hour = 12 + + grid_export, grid_import, losses, self_consumption = inverter.process_energy( + generation, consumption, hour + ) + + assert grid_export == pytest.approx(390.0, rel=1e-2) # Excess energy after battery charges + 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) + + +def test_process_energy_zero_generation_zero_consumption(inverter, mock_battery): + generation = 0.0 + consumption = 0.0 + hour = 12 + + grid_export, grid_import, losses, self_consumption = inverter.process_energy( + generation, consumption, hour + ) + + assert grid_export == 0.0 # No feed-in + assert grid_import == 0.0 # No grid draw + assert losses == 0.0 # No losses + assert self_consumption == 0.0 # No consumption + + +def test_process_energy_partial_battery_discharge(inverter, mock_battery): + mock_battery.energie_abgeben.return_value = (50.0, 5.0) + generation = 200.0 + consumption = 400.0 + hour = 12 + + grid_export, grid_import, losses, self_consumption = inverter.process_energy( + generation, consumption, hour + ) + + assert grid_export == 0.0 # No feed-in due to insufficient generation + assert grid_import == pytest.approx( + 150.0, rel=1e-2 + ) # Grid supplies the shortfall after battery assist + assert losses == 5.0 # Discharge losses + assert self_consumption == 250.0 # Generation + battery discharge + + +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) + generation = 100.0 + consumption = 1000.0 # Exceeds the inverter's max power + hour = 12 + + grid_export, grid_import, losses, self_consumption = inverter.process_energy( + generation, consumption, hour + ) + + assert grid_export == 0.0 # No feed-in + 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) + + +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) + generation = 0.0 + consumption = 600.0 + hour = 12 + + grid_export, grid_import, losses, self_consumption = inverter.process_energy( + generation, consumption, hour + ) + + assert grid_export == 0.0 # No feed-in due to zero generation + assert grid_import == pytest.approx( + 100.0, rel=1e-2 + ) # 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) diff --git a/tests/testdata/optimize_input_1.json b/tests/testdata/optimize_input_1.json index 44a6d75..e1dab61 100644 --- a/tests/testdata/optimize_input_1.json +++ b/tests/testdata/optimize_input_1.json @@ -31,8 +31,8 @@ "start_soc_prozent": 80, "min_soc_prozent": 15 }, - "wechselrichter": { - "max_leistung_wh": 10000 + "inverter": { + "max_power_wh": 10000 }, "eauto": { "kapazitaet_wh": 60000, @@ -55,4 +55,4 @@ 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 ] } - \ No newline at end of file +