mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-08-25 06:52:23 +00:00
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:
@@ -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
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
)
|
||||
|
Reference in New Issue
Block a user