mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-04-19 08:55:15 +00:00
311 lines
13 KiB
Python
311 lines
13 KiB
Python
|
from typing import Any, ClassVar, Dict, Optional, Union
|
||
|
|
||
|
import numpy as np
|
||
|
from numpydantic import NDArray, Shape
|
||
|
from pydantic import Field, computed_field
|
||
|
|
||
|
from akkudoktoreos.config.configabc import SettingsBaseModel
|
||
|
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.utils.datetimeutil import to_duration
|
||
|
from akkudoktoreos.utils.logutil import get_logger
|
||
|
|
||
|
logger = get_logger(__name__)
|
||
|
|
||
|
|
||
|
class DevicesCommonSettings(SettingsBaseModel):
|
||
|
"""Base configuration for devices simulation settings."""
|
||
|
|
||
|
# Battery
|
||
|
# -------
|
||
|
battery_provider: Optional[str] = Field(
|
||
|
default=None, description="Id of Battery simulation provider."
|
||
|
)
|
||
|
battery_capacity: Optional[int] = Field(default=None, description="Battery capacity [Wh].")
|
||
|
battery_soc_start: Optional[int] = Field(
|
||
|
default=None, description="Battery initial state of charge [%]."
|
||
|
)
|
||
|
battery_soc_min: Optional[int] = Field(
|
||
|
default=None, description="Battery minimum state of charge [%]."
|
||
|
)
|
||
|
battery_soc_max: Optional[int] = Field(
|
||
|
default=None, description="Battery maximum state of charge [%]."
|
||
|
)
|
||
|
battery_charge_efficiency: Optional[float] = Field(
|
||
|
default=None, description="Battery charging efficiency [%]."
|
||
|
)
|
||
|
battery_discharge_efficiency: Optional[float] = Field(
|
||
|
default=None, description="Battery discharging efficiency [%]."
|
||
|
)
|
||
|
battery_charge_power_max: Optional[int] = Field(
|
||
|
default=None, description="Battery maximum charge power [W]."
|
||
|
)
|
||
|
|
||
|
# Battery Electric Vehicle
|
||
|
# ------------------------
|
||
|
bev_provider: Optional[str] = Field(
|
||
|
default=None, description="Id of Battery Electric Vehicle simulation provider."
|
||
|
)
|
||
|
bev_capacity: Optional[int] = Field(
|
||
|
default=None, description="Battery Electric Vehicle capacity [Wh]."
|
||
|
)
|
||
|
bev_soc_start: Optional[int] = Field(
|
||
|
default=None, description="Battery Electric Vehicle initial state of charge [%]."
|
||
|
)
|
||
|
bev_soc_max: Optional[int] = Field(
|
||
|
default=None, description="Battery Electric Vehicle maximum state of charge [%]."
|
||
|
)
|
||
|
bev_charge_efficiency: Optional[float] = Field(
|
||
|
default=None, description="Battery Electric Vehicle charging efficiency [%]."
|
||
|
)
|
||
|
bev_discharge_efficiency: Optional[float] = Field(
|
||
|
default=None, description="Battery Electric Vehicle discharging efficiency [%]."
|
||
|
)
|
||
|
bev_charge_power_max: Optional[int] = Field(
|
||
|
default=None, description="Battery Electric Vehicle maximum charge power [W]."
|
||
|
)
|
||
|
|
||
|
# Home Appliance - Dish Washer
|
||
|
# ----------------------------
|
||
|
dishwasher_provider: Optional[str] = Field(
|
||
|
default=None, description="Id of Dish Washer simulation provider."
|
||
|
)
|
||
|
dishwasher_consumption: Optional[int] = Field(
|
||
|
default=None, description="Dish Washer energy consumption [Wh]."
|
||
|
)
|
||
|
dishwasher_duration: Optional[int] = Field(
|
||
|
default=None, description="Dish Washer usage duration [h]."
|
||
|
)
|
||
|
|
||
|
# PV Inverter
|
||
|
# -----------
|
||
|
inverter_provider: Optional[str] = Field(
|
||
|
default=None, description="Id of PV Inverter simulation provider."
|
||
|
)
|
||
|
inverter_power_max: Optional[float] = Field(
|
||
|
default=None, description="Inverter maximum power [W]."
|
||
|
)
|
||
|
|
||
|
|
||
|
class Devices(SingletonMixin, DevicesBase):
|
||
|
# Results of the devices simulation and
|
||
|
# insights into various parameters over the entire forecast period.
|
||
|
# -----------------------------------------------------------------
|
||
|
last_wh_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field(
|
||
|
default=None, description="The load in watt-hours per hour."
|
||
|
)
|
||
|
eauto_soc_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field(
|
||
|
default=None, description="The state of charge of the EV for each hour."
|
||
|
)
|
||
|
einnahmen_euro_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field(
|
||
|
default=None,
|
||
|
description="The revenue from grid feed-in or other sources in euros per hour.",
|
||
|
)
|
||
|
home_appliance_wh_per_hour: Optional[NDArray[Shape["*"], float]] = Field(
|
||
|
default=None,
|
||
|
description="The energy consumption of a household appliance in watt-hours per hour.",
|
||
|
)
|
||
|
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(
|
||
|
default=None, description="The grid energy drawn in watt-hours per hour."
|
||
|
)
|
||
|
netzeinspeisung_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(
|
||
|
default=None, description="The losses in watt-hours per hour."
|
||
|
)
|
||
|
akku_soc_pro_stunde: Optional[NDArray[Shape["*"], float]] = Field(
|
||
|
default=None,
|
||
|
description="The state of charge of the battery (not the EV) in percentage per hour.",
|
||
|
)
|
||
|
|
||
|
# Computed fields
|
||
|
@computed_field # type: ignore[prop-decorator]
|
||
|
@property
|
||
|
def total_balance_euro(self) -> float:
|
||
|
"""The total balance of revenues minus costs in euros."""
|
||
|
return self.total_revenues_euro - self.total_costs_euro
|
||
|
|
||
|
@computed_field # type: ignore[prop-decorator]
|
||
|
@property
|
||
|
def total_revenues_euro(self) -> float:
|
||
|
"""The total revenues in euros."""
|
||
|
if self.einnahmen_euro_pro_stunde is None:
|
||
|
return 0
|
||
|
return np.nansum(self.einnahmen_euro_pro_stunde)
|
||
|
|
||
|
@computed_field # type: ignore[prop-decorator]
|
||
|
@property
|
||
|
def total_costs_euro(self) -> float:
|
||
|
"""The total costs in euros."""
|
||
|
if self.kosten_euro_pro_stunde is None:
|
||
|
return 0
|
||
|
return np.nansum(self.kosten_euro_pro_stunde)
|
||
|
|
||
|
@computed_field # type: ignore[prop-decorator]
|
||
|
@property
|
||
|
def total_losses_wh(self) -> float:
|
||
|
"""The total losses in watt-hours over the entire period."""
|
||
|
if self.verluste_wh_pro_stunde is None:
|
||
|
return 0
|
||
|
return np.nansum(self.verluste_wh_pro_stunde)
|
||
|
|
||
|
# Devices
|
||
|
# TODO: Make devices class a container of device simulation providers.
|
||
|
# Device simulations to be used are then enabled in the configuration.
|
||
|
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"
|
||
|
)
|
||
|
|
||
|
def update_data(self) -> None:
|
||
|
"""Update device simulation data."""
|
||
|
# Assure devices are set up
|
||
|
self.akku.setup()
|
||
|
self.eauto.setup()
|
||
|
self.home_appliance.setup()
|
||
|
self.wechselrichter.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.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)
|
||
|
self.eauto_soc_pro_stunde = np.full((self.total_hours), np.nan)
|
||
|
self.verluste_wh_pro_stunde = np.full((self.total_hours), np.nan)
|
||
|
self.home_appliance_wh_per_hour = np.full((self.total_hours), np.nan)
|
||
|
|
||
|
# Set initial state
|
||
|
simulation_step = to_duration("1 hour")
|
||
|
if self.akku:
|
||
|
self.akku_soc_pro_stunde[0] = self.akku.ladezustand_in_prozent()
|
||
|
if self.eauto:
|
||
|
self.eauto_soc_pro_stunde[0] = self.eauto.ladezustand_in_prozent()
|
||
|
|
||
|
# Get predictions for full device simulation time range
|
||
|
# gesamtlast[stunde]
|
||
|
load_total_mean = self.prediction.key_to_array(
|
||
|
"load_total_mean",
|
||
|
start_datetime=self.start_datetime,
|
||
|
end_datetime=self.end_datetime,
|
||
|
interval=simulation_step,
|
||
|
)
|
||
|
# pv_prognose_wh[stunde]
|
||
|
pvforecast_ac_power = self.prediction.key_to_array(
|
||
|
"pvforecast_ac_power",
|
||
|
start_datetime=self.start_datetime,
|
||
|
end_datetime=self.end_datetime,
|
||
|
interval=simulation_step,
|
||
|
)
|
||
|
# strompreis_euro_pro_wh[stunde]
|
||
|
elecprice_marketprice = self.prediction.key_to_array(
|
||
|
"elecprice_marketprice",
|
||
|
start_datetime=self.start_datetime,
|
||
|
end_datetime=self.end_datetime,
|
||
|
interval=simulation_step,
|
||
|
)
|
||
|
# einspeiseverguetung_euro_pro_wh_arr[stunde]
|
||
|
# TODO: Create prediction for einspeiseverguetung_euro_pro_wh_arr
|
||
|
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
|
||
|
|
||
|
# Accumulate loads and PV generation
|
||
|
verbrauch = 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
|
||
|
self.home_appliance_wh_per_hour[stunde_since_now] = ha_load
|
||
|
|
||
|
# E-Auto handling
|
||
|
if self.eauto:
|
||
|
if self.ev_charge_hours[stunde] > 0:
|
||
|
geladene_menge_eauto, verluste_eauto = self.eauto.energie_laden(
|
||
|
None, stunde, relative_power=self.ev_charge_hours[stunde]
|
||
|
)
|
||
|
verbrauch += 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)
|
||
|
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)
|
||
|
)
|
||
|
|
||
|
# AC PV Battery Charge
|
||
|
if self.akku and self.ac_charge_hours[stunde] > 0.0:
|
||
|
self.akku.set_charge_allowed_for_hour(1, stunde)
|
||
|
geladene_menge, verluste_wh = self.akku.energie_laden(
|
||
|
None, stunde, relative_power=self.ac_charge_hours[stunde]
|
||
|
)
|
||
|
# print(stunde, " ", geladene_menge, " ",self.ac_charge_hours[stunde]," ",self.akku.ladezustand_in_prozent())
|
||
|
verbrauch += geladene_menge
|
||
|
netzbezug += 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
|
||
|
|
||
|
# Financial calculations
|
||
|
self.kosten_euro_pro_stunde[stunde_since_now] = (
|
||
|
netzbezug * self.strompreis_euro_pro_wh[stunde]
|
||
|
)
|
||
|
self.einnahmen_euro_pro_stunde[stunde_since_now] = (
|
||
|
netzeinspeisung * self.einspeiseverguetung_euro_pro_wh_arr[stunde]
|
||
|
)
|
||
|
|
||
|
# Akku SOC tracking
|
||
|
if self.akku:
|
||
|
self.akku_soc_pro_stunde[stunde_since_now] = self.akku.ladezustand_in_prozent()
|
||
|
else:
|
||
|
self.akku_soc_pro_stunde[stunde_since_now] = 0.0
|
||
|
|
||
|
def report_dict(self) -> Dict[str, Any]:
|
||
|
"""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,
|
||
|
"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,
|
||
|
"Gesamtbilanz_Euro": self.total_balance_euro,
|
||
|
"EAuto_SoC_pro_Stunde": self.eauto_soc_pro_stunde,
|
||
|
"Gesamteinnahmen_Euro": self.total_revenues_euro,
|
||
|
"Gesamtkosten_Euro": self.total_costs_euro,
|
||
|
"Verluste_Pro_Stunde": self.verluste_wh_pro_stunde,
|
||
|
"Gesamt_Verluste": self.total_losses_wh,
|
||
|
"Home_appliance_wh_per_hour": self.home_appliance_wh_per_hour,
|
||
|
}
|
||
|
return out
|
||
|
|
||
|
|
||
|
# Initialize the Devices simulation, it is a singleton.
|
||
|
devices = Devices()
|
||
|
|
||
|
|
||
|
def get_devices() -> Devices:
|
||
|
"""Gets the EOS Devices simulation."""
|
||
|
return devices
|