Files
EOS/src/akkudoktoreos/devices/inverter.py

122 lines
4.9 KiB
Python
Raw Normal View History

from typing import Optional
from pydantic import Field
from akkudoktoreos.core.logging import get_logger
from akkudoktoreos.devices.devicesabc import DeviceBase, DeviceParameters
from akkudoktoreos.prediction.interpolator import get_eos_load_interpolator
logger = get_logger(__name__)
class InverterParameters(DeviceParameters):
"""Inverter Device Simulation Configuration."""
device_id: str = Field(description="ID of inverter", examples=["inverter1"])
max_power_wh: float = Field(gt=0, examples=[10000])
battery: Optional[str] = Field(
default=None, description="ID of battery", examples=[None, "battery1"]
)
class Inverter(DeviceBase):
def __init__(
self,
parameters: Optional[InverterParameters] = None,
):
self.parameters: Optional[InverterParameters] = None
super().__init__(parameters)
def _setup(self) -> None:
assert self.parameters is not None
if self.parameters.battery is None:
# For the moment raise exception
# TODO: Make battery configurable by config
error_msg = "Battery for PV inverter is mandatory."
logger.error(error_msg)
raise NotImplementedError(error_msg)
self.self_consumption_predictor = get_eos_load_interpolator()
self.max_power_wh = (
self.parameters.max_power_wh
) # Maximum power that the inverter can handle
def _post_setup(self) -> None:
assert self.parameters is not None
self.battery = self.devices.get_device_by_id(self.parameters.battery)
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 generation >= consumption:
if consumption > self.max_power_wh:
# If consumption exceeds maximum inverter power
losses += generation - self.max_power_wh
remaining_power = self.max_power_wh - consumption
grid_import = -remaining_power # Negative indicates feeding into the grid
self_consumption = self.max_power_wh
else:
scr = self.self_consumption_predictor.calculate_self_consumption(
consumption, generation
)
# Remaining power after consumption
remaining_power = (generation - consumption) * scr # EVQ
# Remaining load Self Consumption not perfect
remaining_load_evq = (generation - consumption) * (1.0 - scr)
if remaining_load_evq > 0:
# Akku muss den Restverbrauch decken
from_battery, discharge_losses = self.battery.discharge_energy(
remaining_load_evq, hour
)
remaining_load_evq -= from_battery # Restverbrauch nach Akkuentladung
losses += discharge_losses
# Wenn der Akku den Restverbrauch nicht vollständig decken kann, wird der Rest ins Netz gezogen
if remaining_load_evq > 0:
grid_import += remaining_load_evq
remaining_load_evq = 0
else:
from_battery = 0.0
if remaining_power > 0:
# Load battery with excess energy
charged_energie, charge_losses = self.battery.charge_energy(
remaining_power, hour
)
remaining_surplus = remaining_power - (charged_energie + charge_losses)
# Feed-in to the grid based on remaining capacity
if remaining_surplus > self.max_power_wh - consumption:
grid_export = self.max_power_wh - consumption
losses += remaining_surplus - grid_export
else:
grid_export = remaining_surplus
losses += charge_losses
self_consumption = (
consumption + from_battery
) # Self-consumption is equal to the load
2024-10-03 11:05:44 +02:00
else:
# Case 2: Insufficient generation, cover shortfall
shortfall = consumption - generation
available_ac_power = max(self.max_power_wh - generation, 0)
# Discharge battery to cover shortfall, if possible
battery_discharge, discharge_losses = self.battery.discharge_energy(
min(shortfall, available_ac_power), hour
)
losses += discharge_losses
# 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
return grid_export, grid_import, losses, self_consumption