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", "title": "Optimization Ev Available Charge Rates Percent",
"description": "Charge rates available for the EV in percent of maximum charge.", "description": "Charge rates available for the EV in percent of maximum charge.",
"default": [ "default": [0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0]
0.0,
0.375,
0.5,
0.625,
0.75,
0.875,
1.0
]
}, },
"battery_provider": { "battery_provider": {
"anyOf": [ "anyOf": [
@ -2780,9 +2772,7 @@
} }
}, },
"type": "object", "type": "object",
"required": [ "required": ["kapazitaet_wh"],
"kapazitaet_wh"
],
"title": "EAutoParameters" "title": "EAutoParameters"
}, },
"EAutoResult": { "EAutoResult": {
@ -2929,10 +2919,7 @@
} }
}, },
"type": "object", "type": "object",
"required": [ "required": ["temperature", "pvpower"],
"temperature",
"pvpower"
],
"title": "ForecastResponse" "title": "ForecastResponse"
}, },
"GesamtlastRequest": { "GesamtlastRequest": {
@ -2954,11 +2941,7 @@
} }
}, },
"type": "object", "type": "object",
"required": [ "required": ["year_energy", "measured_data", "hours"],
"year_energy",
"measured_data",
"hours"
],
"title": "GesamtlastRequest" "title": "GesamtlastRequest"
}, },
"HTTPValidationError": { "HTTPValidationError": {
@ -2990,12 +2973,21 @@
} }
}, },
"type": "object", "type": "object",
"required": [ "required": ["consumption_wh", "duration_h"],
"consumption_wh",
"duration_h"
],
"title": "HomeApplianceParameters" "title": "HomeApplianceParameters"
}, },
"InverterParameters": {
"properties": {
"max_power_wh": {
"type": "number",
"exclusiveMinimum": 0.0,
"title": "Max Power Wh",
"default": 10000
}
},
"type": "object",
"title": "InverterParameters"
},
"OptimizationParameters": { "OptimizationParameters": {
"properties": { "properties": {
"ems": { "ems": {
@ -3004,10 +2996,10 @@
"pv_akku": { "pv_akku": {
"$ref": "#/components/schemas/PVAkkuParameters" "$ref": "#/components/schemas/PVAkkuParameters"
}, },
"wechselrichter": { "inverter": {
"$ref": "#/components/schemas/WechselrichterParameters", "$ref": "#/components/schemas/InverterParameters",
"default": { "default": {
"max_leistung_wh": 10000.0 "max_power_wh": 10000.0
} }
}, },
"eauto": { "eauto": {
@ -3062,11 +3054,7 @@
} }
}, },
"type": "object", "type": "object",
"required": [ "required": ["ems", "pv_akku", "eauto"],
"ems",
"pv_akku",
"eauto"
],
"title": "OptimizationParameters" "title": "OptimizationParameters"
}, },
"OptimizeResponse": { "OptimizeResponse": {
@ -3225,9 +3213,7 @@
} }
}, },
"type": "object", "type": "object",
"required": [ "required": ["kapazitaet_wh"],
"kapazitaet_wh"
],
"title": "PVAkkuParameters" "title": "PVAkkuParameters"
}, },
"SettingsEOS": { "SettingsEOS": {
@ -4994,15 +4980,7 @@
], ],
"title": "Optimization Ev Available Charge Rates Percent", "title": "Optimization Ev Available Charge Rates Percent",
"description": "Charge rates available for the EV in percent of maximum charge.", "description": "Charge rates available for the EV in percent of maximum charge.",
"default": [ "default": [0.0, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0]
0.0,
0.375,
0.5,
0.625,
0.75,
0.875,
1.0
]
}, },
"battery_provider": { "battery_provider": {
"anyOf": [ "anyOf": [
@ -5493,24 +5471,8 @@
} }
}, },
"type": "object", "type": "object",
"required": [ "required": ["loc", "msg", "type"],
"loc",
"msg",
"type"
],
"title": "ValidationError" "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.core.pydantic import PydanticBaseModel
from akkudoktoreos.devices.battery import PVAkku from akkudoktoreos.devices.battery import PVAkku
from akkudoktoreos.devices.generic import HomeAppliance 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.datetimeutil import to_datetime
from akkudoktoreos.utils.logutil import get_logger from akkudoktoreos.utils.logutil import get_logger
from akkudoktoreos.utils.utils import NumpyEncoder from akkudoktoreos.utils.utils import NumpyEncoder
@ -155,7 +155,7 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda
akku: Optional[PVAkku] = Field(default=None, description="TBD.") akku: Optional[PVAkku] = Field(default=None, description="TBD.")
eauto: Optional[PVAkku] = Field(default=None, description="TBD.") eauto: Optional[PVAkku] = Field(default=None, description="TBD.")
home_appliance: Optional[HomeAppliance] = 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 # TODO: Move to devices
@ -170,7 +170,7 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda
parameters: EnergieManagementSystemParameters, parameters: EnergieManagementSystemParameters,
eauto: Optional[PVAkku] = None, eauto: Optional[PVAkku] = None,
home_appliance: Optional[HomeAppliance] = None, home_appliance: Optional[HomeAppliance] = None,
wechselrichter: Optional[Wechselrichter] = None, inverter: Optional[Inverter] = None,
) -> None: ) -> None:
self.gesamtlast = np.array(parameters.gesamtlast, float) self.gesamtlast = np.array(parameters.gesamtlast, float)
self.pv_prognose_wh = np.array(parameters.pv_prognose_wh, 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) if isinstance(parameters.einspeiseverguetung_euro_pro_wh, list)
else np.full(len(self.gesamtlast), parameters.einspeiseverguetung_euro_pro_wh, float) else np.full(len(self.gesamtlast), parameters.einspeiseverguetung_euro_pro_wh, float)
) )
if wechselrichter is not None: if inverter is not None:
self.akku = wechselrichter.akku self.akku = inverter.akku
else: else:
self.akku = None self.akku = None
self.eauto = eauto self.eauto = eauto
self.home_appliance = home_appliance self.home_appliance = home_appliance
self.wechselrichter = wechselrichter self.inverter = inverter
self.ac_charge_hours = np.full(self.config.prediction_hours, 0.0) 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.dc_charge_hours = np.full(self.config.prediction_hours, 1.0)
self.ev_charge_hours = np.full(self.config.prediction_hours, 0.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) netzeinspeisung, netzbezug, verluste, eigenverbrauch = (0.0, 0.0, 0.0, 0.0)
if self.akku: if self.akku:
self.akku.set_charge_allowed_for_hour(self.dc_charge_hours[stunde], stunde) 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] erzeugung = self.pv_prognose_wh[stunde]
netzeinspeisung, netzbezug, verluste, eigenverbrauch = ( netzeinspeisung, netzbezug, verluste, eigenverbrauch = self.inverter.process_energy(
self.wechselrichter.energie_verarbeiten(erzeugung, verbrauch, stunde) erzeugung, verbrauch, stunde
) )
# AC PV Battery Charge # 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.battery import PVAkku
from akkudoktoreos.devices.devicesabc import DevicesBase from akkudoktoreos.devices.devicesabc import DevicesBase
from akkudoktoreos.devices.generic import HomeAppliance 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.datetimeutil import to_duration
from akkudoktoreos.utils.logutil import get_logger from akkudoktoreos.utils.logutil import get_logger
@ -111,10 +111,10 @@ class Devices(SingletonMixin, DevicesBase):
kosten_euro_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field( kosten_euro_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field(
default=None, description="The costs in euros per hour." 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." 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." default=None, description="The energy fed into the grid in watt-hours per hour."
) )
verluste_wh_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field( verluste_wh_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field(
@ -162,9 +162,7 @@ class Devices(SingletonMixin, DevicesBase):
akku: ClassVar[PVAkku] = PVAkku(provider_id="GenericBattery") akku: ClassVar[PVAkku] = PVAkku(provider_id="GenericBattery")
eauto: ClassVar[PVAkku] = PVAkku(provider_id="GenericBEV") eauto: ClassVar[PVAkku] = PVAkku(provider_id="GenericBEV")
home_appliance: ClassVar[HomeAppliance] = HomeAppliance(provider_id="GenericDishWasher") home_appliance: ClassVar[HomeAppliance] = HomeAppliance(provider_id="GenericDishWasher")
wechselrichter: ClassVar[Wechselrichter] = Wechselrichter( inverter: ClassVar[Inverter] = Inverter(akku=akku, provider_id="GenericInverter")
akku=akku, provider_id="GenericInverter"
)
def update_data(self) -> None: def update_data(self) -> None:
"""Update device simulation data.""" """Update device simulation data."""
@ -172,12 +170,12 @@ class Devices(SingletonMixin, DevicesBase):
self.akku.setup() self.akku.setup()
self.eauto.setup() self.eauto.setup()
self.home_appliance.setup() self.home_appliance.setup()
self.wechselrichter.setup() self.inverter.setup()
# Pre-allocate arrays for the results, optimized for speed # Pre-allocate arrays for the results, optimized for speed
self.last_wh_pro_stunde = np.full((self.total_hours), np.nan) 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.grid_export_wh_pro_stunde = np.full((self.total_hours), np.nan)
self.netzbezug_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.kosten_euro_pro_stunde = np.full((self.total_hours), np.nan)
self.einnahmen_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) 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) einspeiseverguetung_euro_pro_wh_arr = np.full((self.total_hours), 0.078)
for stunde_since_now in range(0, self.total_hours): 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 # 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 self.verluste_wh_pro_stunde[stunde_since_now] = 0.0
# Home appliances # Home appliances
if self.home_appliance: if self.home_appliance:
ha_load = self.home_appliance.get_load_for_hour(stunde) ha_load = self.home_appliance.get_load_for_hour(hour)
verbrauch += ha_load consumption += ha_load
self.home_appliance_wh_per_hour[stunde_since_now] = ha_load self.home_appliance_wh_per_hour[stunde_since_now] = ha_load
# E-Auto handling # E-Auto handling
if self.eauto: 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( 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.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.ladezustand_in_prozent()
# Process inverter logic # 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: if self.akku:
self.akku.set_charge_allowed_for_hour(self.dc_charge_hours[stunde], stunde) self.akku.set_charge_allowed_for_hour(self.dc_charge_hours[hour], hour)
if self.wechselrichter: if self.inverter:
erzeugung = pvforecast_ac_power[stunde] generation = pvforecast_ac_power[hour]
netzeinspeisung, netzbezug, verluste, eigenverbrauch = ( grid_export, grid_import, losses, self_consumption = self.inverter.process_energy(
self.wechselrichter.energie_verarbeiten(erzeugung, verbrauch, stunde) generation, consumption, hour
) )
# AC PV Battery Charge # AC PV Battery Charge
if self.akku and self.ac_charge_hours[stunde] > 0.0: if self.akku and self.ac_charge_hours[hour] > 0.0:
self.akku.set_charge_allowed_for_hour(1, stunde) self.akku.set_charge_allowed_for_hour(1, hour)
geladene_menge, verluste_wh = self.akku.energie_laden( 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()) # print(stunde, " ", geladene_menge, " ",self.ac_charge_hours[stunde]," ",self.akku.ladezustand_in_prozent())
verbrauch += geladene_menge consumption += geladene_menge
netzbezug += geladene_menge grid_import += geladene_menge
self.verluste_wh_pro_stunde[stunde_since_now] += verluste_wh self.verluste_wh_pro_stunde[stunde_since_now] += verluste_wh
self.netzeinspeisung_wh_pro_stunde[stunde_since_now] = netzeinspeisung self.grid_export_wh_pro_stunde[stunde_since_now] = grid_export
self.netzbezug_wh_pro_stunde[stunde_since_now] = netzbezug self.grid_import_wh_pro_stunde[stunde_since_now] = grid_import
self.verluste_wh_pro_stunde[stunde_since_now] += verluste self.verluste_wh_pro_stunde[stunde_since_now] += losses
self.last_wh_pro_stunde[stunde_since_now] = verbrauch self.last_wh_pro_stunde[stunde_since_now] = consumption
# Financial calculations # Financial calculations
self.kosten_euro_pro_stunde[stunde_since_now] = ( 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] = ( 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 # Akku SOC tracking
@ -285,8 +283,8 @@ class Devices(SingletonMixin, DevicesBase):
"""Provides devices simulation output as a dictionary.""" """Provides devices simulation output as a dictionary."""
out: Dict[str, Optional[Union[np.ndarray, float]]] = { out: Dict[str, Optional[Union[np.ndarray, float]]] = {
"Last_Wh_pro_Stunde": self.last_wh_pro_stunde, "Last_Wh_pro_Stunde": self.last_wh_pro_stunde,
"Netzeinspeisung_Wh_pro_Stunde": self.netzeinspeisung_wh_pro_stunde, "grid_export_Wh_pro_Stunde": self.grid_export_wh_pro_stunde,
"Netzbezug_Wh_pro_Stunde": self.netzbezug_wh_pro_stunde, "grid_import_Wh_pro_Stunde": self.grid_import_wh_pro_stunde,
"Kosten_Euro_pro_Stunde": self.kosten_euro_pro_stunde, "Kosten_Euro_pro_Stunde": self.kosten_euro_pro_stunde,
"akku_soc_pro_stunde": self.akku_soc_pro_stunde, "akku_soc_pro_stunde": self.akku_soc_pro_stunde,
"Einnahmen_Euro_pro_Stunde": self.einnahmen_euro_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 from pydantic import BaseModel, Field
@ -9,14 +9,14 @@ from akkudoktoreos.utils.logutil import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
class WechselrichterParameters(BaseModel): class InverterParameters(BaseModel):
max_leistung_wh: float = Field(default=10000, gt=0) max_power_wh: float = Field(default=10000, gt=0)
class Wechselrichter(DeviceBase): class Inverter(DeviceBase):
def __init__( def __init__(
self, self,
parameters: Optional[WechselrichterParameters] = None, parameters: Optional[InverterParameters] = None,
akku: Optional[PVAkku] = None, akku: Optional[PVAkku] = None,
provider_id: Optional[str] = None, provider_id: Optional[str] = None,
): ):
@ -45,69 +45,55 @@ class Wechselrichter(DeviceBase):
return return
if self.provider_id is not None: if self.provider_id is not None:
# Setup by configuration # 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: elif self.parameters is not None:
# Setup by parameters # Setup by parameters
self.max_leistung_wh = ( self.max_power_wh = (
self.parameters.max_leistung_wh # Maximum power that the inverter can handle self.parameters.max_power_wh # Maximum power that the inverter can handle
) )
else: else:
error_msg = "Parameters and provider ID missing. Can't instantiate." error_msg = "Parameters and provider ID missing. Can't instantiate."
logger.error(error_msg) logger.error(error_msg)
raise ValueError(error_msg) raise ValueError(error_msg)
def energie_verarbeiten( def process_energy(
self, erzeugung: float, verbrauch: float, hour: int self, generation: float, consumption: float, hour: int
) -> tuple[float, float, float, float]: ) -> Tuple[float, float, float, float]:
verluste = 0.0 # Losses during processing losses = 0.0
netzeinspeisung = 0.0 # Grid feed-in grid_export = 0.0
netzbezug = 0.0 # Grid draw grid_import = 0.0
eigenverbrauch = 0.0 # Self-consumption self_consumption = 0.0
if erzeugung >= verbrauch: if generation >= consumption:
if verbrauch > self.max_leistung_wh: # Case 1: Sufficient or excess generation
# If consumption exceeds maximum inverter power actual_consumption = min(consumption, self.max_power_wh)
verluste += erzeugung - self.max_leistung_wh remaining_energy = generation - actual_consumption
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
# Load battery with excess energy # Charge battery with excess energy
geladene_energie, verluste_laden_akku = self.akku.energie_laden( charged_energy, charging_losses = self.akku.energie_laden(remaining_energy, hour)
restleistung_nach_verbrauch, hour losses += charging_losses
)
rest_überschuss = restleistung_nach_verbrauch - (
geladene_energie + verluste_laden_akku
)
# Feed-in to the grid based on remaining capacity # Calculate remaining surplus after battery charge
if rest_überschuss > self.max_leistung_wh - verbrauch: remaining_surplus = remaining_energy - (charged_energy + charging_losses)
netzeinspeisung = self.max_leistung_wh - verbrauch grid_export = min(remaining_surplus, self.max_power_wh - actual_consumption)
verluste += rest_überschuss - netzeinspeisung
else:
netzeinspeisung = rest_überschuss
verluste += verluste_laden_akku # If any remaining surplus can't be fed to the grid, count as losses
eigenverbrauch = verbrauch # Self-consumption is equal to the load losses += max(remaining_surplus - grid_export, 0)
self_consumption = actual_consumption
else: else:
benötigte_energie = verbrauch - erzeugung # Energy needed from external sources # Case 2: Insufficient generation, cover shortfall
max_akku_leistung = self.akku.max_ladeleistung_w # Maximum battery discharge power shortfall = consumption - generation
available_ac_power = max(self.max_power_wh - generation, 0)
# Calculate remaining AC power available # Discharge battery to cover shortfall, if possible
rest_ac_leistung = max(self.max_leistung_wh - erzeugung, 0) 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 # Draw remaining required power from the grid (discharge_losses are already substraved in the battery)
if benötigte_energie < rest_ac_leistung: grid_import = shortfall - battery_discharge
aus_akku, akku_entladeverluste = self.akku.energie_abgeben(benötigte_energie, hour) self_consumption = generation + battery_discharge
else:
aus_akku, akku_entladeverluste = self.akku.energie_abgeben(rest_ac_leistung, hour)
verluste += akku_entladeverluste # Include losses from battery discharge return grid_export, grid_import, losses, self_consumption
netzbezug = benötigte_energie - aus_akku # Energy drawn from the grid
eigenverbrauch = erzeugung + aus_akku # Total self-consumption
return netzeinspeisung, netzbezug, verluste, eigenverbrauch

View File

@ -19,7 +19,7 @@ from akkudoktoreos.devices.battery import (
PVAkkuParameters, PVAkkuParameters,
) )
from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters 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.utils.utils import NumpyEncoder
from akkudoktoreos.visualize import visualisiere_ergebnisse from akkudoktoreos.visualize import visualisiere_ergebnisse
@ -27,7 +27,7 @@ from akkudoktoreos.visualize import visualisiere_ergebnisse
class OptimizationParameters(BaseModel): class OptimizationParameters(BaseModel):
ems: EnergieManagementSystemParameters ems: EnergieManagementSystemParameters
pv_akku: PVAkkuParameters pv_akku: PVAkkuParameters
wechselrichter: WechselrichterParameters = WechselrichterParameters() inverter: InverterParameters = InverterParameters()
eauto: Optional[EAutoParameters] eauto: Optional[EAutoParameters]
dishwasher: Optional[HomeApplianceParameters] = None dishwasher: Optional[HomeApplianceParameters] = None
temperature_forecast: Optional[list[float]] = Field( temperature_forecast: Optional[list[float]] = Field(
@ -488,10 +488,9 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
) )
# Initialize the inverter and energy management system # Initialize the inverter and energy management system
wr = Wechselrichter(parameters.wechselrichter, akku)
self.ems.set_parameters( self.ems.set_parameters(
parameters.ems, parameters.ems,
wechselrichter=wr, inverter=Inverter(parameters.inverter, akku),
eauto=eauto, eauto=eauto,
home_appliance=dishwasher, 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.battery import EAutoParameters, PVAkku, PVAkkuParameters
from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters
from akkudoktoreos.devices.inverter import Wechselrichter, WechselrichterParameters from akkudoktoreos.devices.inverter import Inverter, InverterParameters
start_hour = 1 start_hour = 1
@ -30,7 +30,7 @@ def create_ems_instance() -> EnergieManagementSystem:
hours=config_eos.prediction_hours, hours=config_eos.prediction_hours,
) )
akku.reset() 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) # Household device (currently not used, set to None)
home_appliance = HomeAppliance( home_appliance = HomeAppliance(
@ -216,7 +216,7 @@ def create_ems_instance() -> EnergieManagementSystem:
preis_euro_pro_wh_akku=preis_euro_pro_wh_akku, preis_euro_pro_wh_akku=preis_euro_pro_wh_akku,
gesamtlast=gesamtlast, gesamtlast=gesamtlast,
), ),
wechselrichter=wechselrichter, inverter=inverter,
eauto=eauto, eauto=eauto,
home_appliance=home_appliance, 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.battery import EAutoParameters, PVAkku, PVAkkuParameters
from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters
from akkudoktoreos.devices.inverter import Wechselrichter, WechselrichterParameters from akkudoktoreos.devices.inverter import Inverter, InverterParameters
start_hour = 0 start_hour = 0
@ -29,7 +29,7 @@ def create_ems_instance() -> EnergieManagementSystem:
hours=config_eos.prediction_hours, hours=config_eos.prediction_hours,
) )
akku.reset() 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) # Household device (currently not used, set to None)
home_appliance = HomeAppliance( home_appliance = HomeAppliance(
@ -121,7 +121,7 @@ def create_ems_instance() -> EnergieManagementSystem:
preis_euro_pro_wh_akku=preis_euro_pro_wh_akku, preis_euro_pro_wh_akku=preis_euro_pro_wh_akku,
gesamtlast=gesamtlast, gesamtlast=gesamtlast,
), ),
wechselrichter=wechselrichter, inverter=inverter,
eauto=eauto, eauto=eauto,
home_appliance=home_appliance, 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, "start_soc_prozent": 80,
"min_soc_prozent": 15 "min_soc_prozent": 15
}, },
"wechselrichter": { "inverter": {
"max_leistung_wh": 10000 "max_power_wh": 10000
}, },
"eauto": { "eauto": {
"kapazitaet_wh": 60000, "kapazitaet_wh": 60000,