2024-12-19 14:45:20 +01:00
|
|
|
from typing import Optional
|
2024-12-15 14:40:03 +01:00
|
|
|
|
2024-11-15 22:27:25 +01:00
|
|
|
from pydantic import BaseModel, Field
|
2024-12-19 14:45:20 +01:00
|
|
|
from scipy.interpolate import RegularGridInterpolator
|
2024-11-15 22:27:25 +01:00
|
|
|
|
2025-01-05 14:41:07 +01:00
|
|
|
from akkudoktoreos.core.logging import get_logger
|
2024-12-19 14:50:19 +01:00
|
|
|
from akkudoktoreos.devices.battery import Battery
|
2024-12-15 14:40:03 +01:00
|
|
|
from akkudoktoreos.devices.devicesabc import DeviceBase
|
|
|
|
|
|
|
|
logger = get_logger(__name__)
|
2024-11-15 22:27:25 +01:00
|
|
|
|
|
|
|
|
2024-12-16 15:33:00 +01:00
|
|
|
class InverterParameters(BaseModel):
|
|
|
|
max_power_wh: float = Field(default=10000, gt=0)
|
2024-11-15 22:27:25 +01:00
|
|
|
|
|
|
|
|
2024-12-16 15:33:00 +01:00
|
|
|
class Inverter(DeviceBase):
|
2024-12-15 14:40:03 +01:00
|
|
|
def __init__(
|
|
|
|
self,
|
2024-12-19 14:45:20 +01:00
|
|
|
self_consumption_predictor: RegularGridInterpolator,
|
2024-12-16 15:33:00 +01:00
|
|
|
parameters: Optional[InverterParameters] = None,
|
2024-12-22 12:03:48 +01:00
|
|
|
battery: Optional[Battery] = None,
|
2024-12-15 14:40:03 +01:00
|
|
|
provider_id: Optional[str] = None,
|
|
|
|
):
|
|
|
|
# Configuration initialisation
|
|
|
|
self.provider_id = provider_id
|
|
|
|
self.prefix = "<invalid>"
|
|
|
|
if self.provider_id == "GenericInverter":
|
|
|
|
self.prefix = "inverter"
|
|
|
|
# Parameter initialisiation
|
|
|
|
self.parameters = parameters
|
2024-12-22 12:03:48 +01:00
|
|
|
if battery is None:
|
2024-12-15 14:40:03 +01:00
|
|
|
# For the moment raise exception
|
2024-12-22 12:03:48 +01:00
|
|
|
# TODO: Make battery configurable by config
|
2024-12-15 14:40:03 +01:00
|
|
|
error_msg = "Battery for PV inverter is mandatory."
|
|
|
|
logger.error(error_msg)
|
|
|
|
raise NotImplementedError(error_msg)
|
2024-12-22 12:03:48 +01:00
|
|
|
self.battery = battery # Connection to a battery object
|
2024-12-19 14:45:20 +01:00
|
|
|
self.self_consumption_predictor = self_consumption_predictor
|
2024-05-02 10:27:33 +02:00
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
self.initialised = False
|
|
|
|
# Run setup if parameters are given, otherwise setup() has to be called later when the config is initialised.
|
|
|
|
if self.parameters is not None:
|
|
|
|
self.setup()
|
|
|
|
|
|
|
|
def setup(self) -> None:
|
|
|
|
if self.initialised:
|
|
|
|
return
|
|
|
|
if self.provider_id is not None:
|
|
|
|
# Setup by configuration
|
2024-12-16 15:33:00 +01:00
|
|
|
self.max_power_wh = getattr(self.config, f"{self.prefix}_power_max")
|
2024-12-15 14:40:03 +01:00
|
|
|
elif self.parameters is not None:
|
|
|
|
# Setup by parameters
|
2024-12-16 15:33:00 +01:00
|
|
|
self.max_power_wh = (
|
|
|
|
self.parameters.max_power_wh # Maximum power that the inverter can handle
|
2024-12-15 14:40:03 +01:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
error_msg = "Parameters and provider ID missing. Can't instantiate."
|
|
|
|
logger.error(error_msg)
|
|
|
|
raise ValueError(error_msg)
|
|
|
|
|
2024-12-16 15:33:00 +01:00
|
|
|
def process_energy(
|
|
|
|
self, generation: float, consumption: float, hour: int
|
2024-12-19 14:45:20 +01:00
|
|
|
) -> tuple[float, float, float, float]:
|
2024-12-16 15:33:00 +01:00
|
|
|
losses = 0.0
|
|
|
|
grid_export = 0.0
|
|
|
|
grid_import = 0.0
|
|
|
|
self_consumption = 0.0
|
2024-05-02 10:27:33 +02:00
|
|
|
|
2024-12-16 15:33:00 +01:00
|
|
|
if generation >= consumption:
|
2024-12-19 14:45:20 +01:00
|
|
|
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
|
2024-12-22 12:03:48 +01:00
|
|
|
from_battery, discharge_losses = self.battery.discharge_energy(
|
2024-12-19 14:45:20 +01:00
|
|
|
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
|
2024-12-22 12:03:48 +01:00
|
|
|
charged_energie, charge_losses = self.battery.charge_energy(
|
|
|
|
remaining_power, hour
|
|
|
|
)
|
2024-12-19 14:45:20 +01:00
|
|
|
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
|
|
|
|
2024-12-16 15:33:00 +01: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
|
2024-12-22 12:03:48 +01:00
|
|
|
battery_discharge, discharge_losses = self.battery.discharge_energy(
|
2024-12-16 15:33:00 +01:00
|
|
|
min(shortfall, available_ac_power), hour
|
|
|
|
)
|
|
|
|
losses += discharge_losses
|
2024-05-02 10:27:33 +02:00
|
|
|
|
2024-12-16 15:33:00 +01:00
|
|
|
# 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
|
2024-05-02 10:27:33 +02:00
|
|
|
|
2024-12-16 15:33:00 +01:00
|
|
|
return grid_export, grid_import, losses, self_consumption
|