Inverter v2 (#245)

* inverter class rewritten second try
* cleanup
* inverter section of decives.py translation
* open api fix
* fix openapi v2
* renamed the class itself
* ruff fix
* Update genetic.py
* cleanup
* reverted indent
This commit is contained in:
Normann 2024-12-16 15:33:00 +01:00 committed by GitHub
parent 763926d8e8
commit 810cc17c0b
9 changed files with 381 additions and 174 deletions

View File

@ -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"
}
}
}
}
}

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

262
tests/test_inverter.py Normal file
View File

@ -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 didnt 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)

View File

@ -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
]
}