mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-08-25 06:52:23 +00:00
translation of battery.py v3 (#262)
This commit is contained in:
@@ -8,7 +8,7 @@ from typing_extensions import Self
|
||||
|
||||
from akkudoktoreos.core.coreabc import ConfigMixin, PredictionMixin, SingletonMixin
|
||||
from akkudoktoreos.core.pydantic import PydanticBaseModel
|
||||
from akkudoktoreos.devices.battery import PVAkku
|
||||
from akkudoktoreos.devices.battery import Battery
|
||||
from akkudoktoreos.devices.generic import HomeAppliance
|
||||
from akkudoktoreos.devices.inverter import Inverter
|
||||
from akkudoktoreos.utils.datetimeutil import to_datetime
|
||||
@@ -152,8 +152,8 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda
|
||||
# TODO: Move to devices
|
||||
# -------------------------
|
||||
|
||||
akku: Optional[PVAkku] = Field(default=None, description="TBD.")
|
||||
eauto: Optional[PVAkku] = Field(default=None, description="TBD.")
|
||||
akku: Optional[Battery] = Field(default=None, description="TBD.")
|
||||
eauto: Optional[Battery] = Field(default=None, description="TBD.")
|
||||
home_appliance: Optional[HomeAppliance] = Field(default=None, description="TBD.")
|
||||
inverter: Optional[Inverter] = Field(default=None, description="TBD.")
|
||||
|
||||
@@ -168,7 +168,7 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda
|
||||
def set_parameters(
|
||||
self,
|
||||
parameters: EnergieManagementSystemParameters,
|
||||
eauto: Optional[PVAkku] = None,
|
||||
eauto: Optional[Battery] = None,
|
||||
home_appliance: Optional[HomeAppliance] = None,
|
||||
inverter: Optional[Inverter] = None,
|
||||
) -> None:
|
||||
@@ -323,9 +323,9 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda
|
||||
|
||||
# Set initial state
|
||||
if self.akku:
|
||||
akku_soc_pro_stunde[0] = self.akku.ladezustand_in_prozent()
|
||||
akku_soc_pro_stunde[0] = self.akku.current_soc_percentage()
|
||||
if self.eauto:
|
||||
eauto_soc_pro_stunde[0] = self.eauto.ladezustand_in_prozent()
|
||||
eauto_soc_pro_stunde[0] = self.eauto.current_soc_percentage()
|
||||
|
||||
for stunde in range(start_stunde, ende):
|
||||
stunde_since_now = stunde - start_stunde
|
||||
@@ -343,12 +343,12 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda
|
||||
# E-Auto handling
|
||||
if self.eauto:
|
||||
if self.ev_charge_hours[stunde] > 0:
|
||||
geladene_menge_eauto, verluste_eauto = self.eauto.energie_laden(
|
||||
geladene_menge_eauto, verluste_eauto = self.eauto.charge_energy(
|
||||
None, stunde, relative_power=self.ev_charge_hours[stunde]
|
||||
)
|
||||
verbrauch += geladene_menge_eauto
|
||||
verluste_wh_pro_stunde[stunde_since_now] += verluste_eauto
|
||||
eauto_soc_pro_stunde[stunde_since_now] = self.eauto.ladezustand_in_prozent()
|
||||
eauto_soc_pro_stunde[stunde_since_now] = self.eauto.current_soc_percentage()
|
||||
|
||||
# Process inverter logic
|
||||
netzeinspeisung, netzbezug, verluste, eigenverbrauch = (0.0, 0.0, 0.0, 0.0)
|
||||
@@ -363,10 +363,10 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda
|
||||
# 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(
|
||||
geladene_menge, verluste_wh = self.akku.charge_energy(
|
||||
None, stunde, relative_power=self.ac_charge_hours[stunde]
|
||||
)
|
||||
# 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.current_soc_percentage())
|
||||
verbrauch += geladene_menge
|
||||
verbrauch += verluste_wh
|
||||
netzbezug += geladene_menge
|
||||
@@ -388,7 +388,7 @@ class EnergieManagementSystem(SingletonMixin, ConfigMixin, PredictionMixin, Pyda
|
||||
|
||||
# Akku SOC tracking
|
||||
if self.akku:
|
||||
akku_soc_pro_stunde[stunde_since_now] = self.akku.ladezustand_in_prozent()
|
||||
akku_soc_pro_stunde[stunde_since_now] = self.akku.current_soc_percentage()
|
||||
else:
|
||||
akku_soc_pro_stunde[stunde_since_now] = 0.0
|
||||
|
||||
|
@@ -10,345 +10,279 @@ from akkudoktoreos.utils.utils import NumpyEncoder
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def max_ladeleistung_w_field(default: Optional[float] = None) -> Optional[float]:
|
||||
def max_charging_power_field(description: Optional[str] = None) -> float:
|
||||
if description is None:
|
||||
description = "Maximum charging power in watts."
|
||||
return Field(
|
||||
default=default,
|
||||
default=5000,
|
||||
gt=0,
|
||||
description="An integer representing the charging power of the battery in watts.",
|
||||
description=description,
|
||||
)
|
||||
|
||||
|
||||
def start_soc_prozent_field(description: str) -> int:
|
||||
def initial_soc_percentage_field(description: str) -> int:
|
||||
return Field(default=0, ge=0, le=100, description=description)
|
||||
|
||||
|
||||
class BaseAkkuParameters(BaseModel):
|
||||
kapazitaet_wh: int = Field(
|
||||
class BaseBatteryParameters(BaseModel):
|
||||
"""Base class for battery parameters with fields for capacity, efficiency, and state of charge."""
|
||||
|
||||
capacity_wh: int = Field(
|
||||
gt=0, description="An integer representing the capacity of the battery in watt-hours."
|
||||
)
|
||||
lade_effizienz: float = Field(
|
||||
charging_efficiency: float = Field(
|
||||
default=0.88,
|
||||
gt=0,
|
||||
le=1,
|
||||
description="A float representing the charging efficiency of the battery.",
|
||||
)
|
||||
entlade_effizienz: float = Field(default=0.88, gt=0, le=1)
|
||||
max_ladeleistung_w: Optional[float] = max_ladeleistung_w_field()
|
||||
start_soc_prozent: int = start_soc_prozent_field(
|
||||
discharging_efficiency: float = Field(
|
||||
default=0.88,
|
||||
gt=0,
|
||||
le=1,
|
||||
description="A float representing the discharge efficiency of the battery.",
|
||||
)
|
||||
max_charge_power_w: Optional[float] = max_charging_power_field()
|
||||
initial_soc_percentage: int = initial_soc_percentage_field(
|
||||
"An integer representing the state of charge of the battery at the **start** of the current hour (not the current state)."
|
||||
)
|
||||
min_soc_prozent: int = Field(
|
||||
min_soc_percentage: int = Field(
|
||||
default=0,
|
||||
ge=0,
|
||||
le=100,
|
||||
description="An integer representing the minimum state of charge (SOC) of the battery in percentage.",
|
||||
)
|
||||
max_soc_prozent: int = Field(default=100, ge=0, le=100)
|
||||
max_soc_percentage: int = Field(
|
||||
default=100,
|
||||
ge=0,
|
||||
le=100,
|
||||
description="An integer representing the maximum state of charge (SOC) of the battery in percentage.",
|
||||
)
|
||||
|
||||
|
||||
class PVAkkuParameters(BaseAkkuParameters):
|
||||
max_ladeleistung_w: Optional[float] = max_ladeleistung_w_field(5000)
|
||||
class SolarPanelBatteryParameters(BaseBatteryParameters):
|
||||
max_charge_power_w: Optional[float] = max_charging_power_field()
|
||||
|
||||
|
||||
class EAutoParameters(BaseAkkuParameters):
|
||||
entlade_effizienz: float = 1.0
|
||||
start_soc_prozent: int = start_soc_prozent_field(
|
||||
class ElectricVehicleParameters(BaseBatteryParameters):
|
||||
"""Parameters specific to an electric vehicle (EV)."""
|
||||
|
||||
discharging_efficiency: float = 1.0
|
||||
initial_soc_percentage: int = initial_soc_percentage_field(
|
||||
"An integer representing the current state of charge (SOC) of the battery in percentage."
|
||||
)
|
||||
|
||||
|
||||
class EAutoResult(BaseModel):
|
||||
"""This object contains information related to the electric vehicle and its charging and discharging behavior."""
|
||||
class ElectricVehicleResult(BaseModel):
|
||||
"""Result class containing information related to the electric vehicle's charging and discharging behavior."""
|
||||
|
||||
charge_array: list[float] = Field(
|
||||
description="Indicates for each hour whether the EV is charging (`0` for no charging, `1` for charging)."
|
||||
description="Hourly charging status (0 for no charging, 1 for charging)."
|
||||
)
|
||||
discharge_array: list[int] = Field(
|
||||
description="Indicates for each hour whether the EV is discharging (`0` for no discharging, `1` for discharging)."
|
||||
description="Hourly discharging status (0 for no discharging, 1 for discharging)."
|
||||
)
|
||||
entlade_effizienz: float = Field(description="The discharge efficiency as a float.")
|
||||
hours: int = Field(description="Amount of hours the simulation is done for.")
|
||||
kapazitaet_wh: int = Field(description="The capacity of the EV’s battery in watt-hours.")
|
||||
lade_effizienz: float = Field(description="The charging efficiency as a float.")
|
||||
max_ladeleistung_w: int = Field(description="The maximum charging power of the EV in watts.")
|
||||
discharging_efficiency: float = Field(description="The discharge efficiency as a float..")
|
||||
hours: int = Field(description="Number of hours in the simulation.")
|
||||
capacity_wh: int = Field(description="Capacity of the EV’s battery in watt-hours.")
|
||||
charging_efficiency: float = Field(description="Charging efficiency as a float..")
|
||||
max_charge_power_w: int = Field(description="Maximum charging power in watts.")
|
||||
soc_wh: float = Field(
|
||||
description="The state of charge of the battery in watt-hours at the start of the simulation."
|
||||
description="State of charge of the battery in watt-hours at the start of the simulation."
|
||||
)
|
||||
start_soc_prozent: int = Field(
|
||||
description="The state of charge of the battery in percentage at the start of the simulation."
|
||||
initial_soc_percentage: int = Field(
|
||||
description="State of charge at the start of the simulation in percentage."
|
||||
)
|
||||
|
||||
@field_validator(
|
||||
"discharge_array",
|
||||
"charge_array",
|
||||
mode="before",
|
||||
)
|
||||
@field_validator("discharge_array", "charge_array", mode="before")
|
||||
def convert_numpy(cls, field: Any) -> Any:
|
||||
return NumpyEncoder.convert_numpy(field)[0]
|
||||
|
||||
|
||||
class PVAkku(DeviceBase):
|
||||
class Battery(DeviceBase):
|
||||
"""Represents a battery device with methods to simulate energy charging and discharging."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parameters: Optional[BaseAkkuParameters] = None,
|
||||
parameters: Optional[BaseBatteryParameters] = None,
|
||||
hours: Optional[int] = 24,
|
||||
provider_id: Optional[str] = None,
|
||||
):
|
||||
# Configuration initialisation
|
||||
# Initialize configuration and parameters
|
||||
self.provider_id = provider_id
|
||||
self.prefix = "<invalid>"
|
||||
if self.provider_id == "GenericBattery":
|
||||
self.prefix = "battery"
|
||||
elif self.provider_id == "GenericBEV":
|
||||
self.prefix = "bev"
|
||||
# Parameter initialisiation
|
||||
|
||||
self.parameters = parameters
|
||||
if hours is None:
|
||||
self.hours = self.total_hours
|
||||
self.hours = self.total_hours # TODO where does that come from?
|
||||
else:
|
||||
self.hours = hours
|
||||
|
||||
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:
|
||||
"""Sets up the battery parameters based on configuration or provided parameters."""
|
||||
if self.initialised:
|
||||
return
|
||||
if self.provider_id is not None:
|
||||
# Setup by configuration
|
||||
# Battery capacity in Wh
|
||||
self.kapazitaet_wh = getattr(self.config, f"{self.prefix}_capacity")
|
||||
# Initial state of charge in Wh
|
||||
self.start_soc_prozent = getattr(self.config, f"{self.prefix}_soc_start")
|
||||
self.hours = self.total_hours
|
||||
# Charge and discharge efficiency
|
||||
self.lade_effizienz = getattr(self.config, f"{self.prefix}_charge_efficiency")
|
||||
self.entlade_effizienz = getattr(self.config, f"{self.prefix}_discharge_efficiency")
|
||||
self.max_ladeleistung_w = getattr(self.config, f"{self.prefix}_charge_power_max")
|
||||
# Only assign for storage battery
|
||||
|
||||
if self.provider_id:
|
||||
# Setup from configuration
|
||||
self.capacity_wh = getattr(self.config, f"{self.prefix}_capacity")
|
||||
self.initial_soc_percentage = getattr(self.config, f"{self.prefix}_initial_soc")
|
||||
self.hours = self.total_hours # TODO where does that come from?
|
||||
self.charging_efficiency = getattr(self.config, f"{self.prefix}_charging_efficiency")
|
||||
self.discharging_efficiency = getattr(
|
||||
self.config, f"{self.prefix}_discharging_efficiency"
|
||||
)
|
||||
self.max_charge_power_w = getattr(self.config, f"{self.prefix}_max_charging_power")
|
||||
|
||||
if self.provider_id == "GenericBattery":
|
||||
self.min_soc_prozent = getattr(self.config, f"{self.prefix}_soc_mint")
|
||||
self.min_soc_percentage = getattr(
|
||||
self.config,
|
||||
f"{self.prefix}_soc_min",
|
||||
)
|
||||
else:
|
||||
self.min_soc_prozent = 0
|
||||
self.max_soc_prozent = getattr(self.config, f"{self.prefix}_soc_mint")
|
||||
elif self.parameters is not None:
|
||||
# Setup by parameters
|
||||
# Battery capacity in Wh
|
||||
self.kapazitaet_wh = self.parameters.kapazitaet_wh
|
||||
# Initial state of charge in Wh
|
||||
self.start_soc_prozent = self.parameters.start_soc_prozent
|
||||
# Charge and discharge efficiency
|
||||
self.lade_effizienz = self.parameters.lade_effizienz
|
||||
self.entlade_effizienz = self.parameters.entlade_effizienz
|
||||
self.max_ladeleistung_w = self.parameters.max_ladeleistung_w
|
||||
self.min_soc_percentage = 0
|
||||
|
||||
self.max_soc_percentage = getattr(
|
||||
self.config,
|
||||
f"{self.prefix}_soc_max",
|
||||
)
|
||||
elif self.parameters:
|
||||
# Setup from parameters
|
||||
self.capacity_wh = self.parameters.capacity_wh
|
||||
self.initial_soc_percentage = self.parameters.initial_soc_percentage
|
||||
self.charging_efficiency = self.parameters.charging_efficiency
|
||||
self.discharging_efficiency = self.parameters.discharging_efficiency
|
||||
self.max_charge_power_w = self.parameters.max_charge_power_w
|
||||
# Only assign for storage battery
|
||||
self.min_soc_prozent = (
|
||||
self.parameters.min_soc_prozent
|
||||
if isinstance(self.parameters, PVAkkuParameters)
|
||||
self.min_soc_percentage = (
|
||||
self.parameters.min_soc_percentage
|
||||
if isinstance(self.parameters, SolarPanelBatteryParameters)
|
||||
else 0
|
||||
)
|
||||
self.max_soc_prozent = self.parameters.max_soc_prozent
|
||||
self.max_soc_percentage = self.parameters.max_soc_percentage
|
||||
else:
|
||||
error_msg = "Parameters and provider ID missing. Can't instantiate."
|
||||
error_msg = "Parameters and provider ID are missing. Cannot instantiate."
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
# init
|
||||
if self.max_ladeleistung_w is None:
|
||||
self.max_ladeleistung_w = self.kapazitaet_wh
|
||||
# Initialize state of charge
|
||||
if self.max_charge_power_w is None:
|
||||
self.max_charge_power_w = self.capacity_wh # TODO this should not be equal capacity_wh
|
||||
self.discharge_array = np.full(self.hours, 1)
|
||||
self.charge_array = np.full(self.hours, 1)
|
||||
# Calculate start, min and max SoC in Wh
|
||||
self.soc_wh = (self.start_soc_prozent / 100) * self.kapazitaet_wh
|
||||
self.min_soc_wh = (self.min_soc_prozent / 100) * self.kapazitaet_wh
|
||||
self.max_soc_wh = (self.max_soc_prozent / 100) * self.kapazitaet_wh
|
||||
self.soc_wh = (self.initial_soc_percentage / 100) * self.capacity_wh
|
||||
self.min_soc_wh = (self.min_soc_percentage / 100) * self.capacity_wh
|
||||
self.max_soc_wh = (self.max_soc_percentage / 100) * self.capacity_wh
|
||||
|
||||
self.initialised = True
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Converts the object to a dictionary representation."""
|
||||
return {
|
||||
"kapazitaet_wh": self.kapazitaet_wh,
|
||||
"start_soc_prozent": self.start_soc_prozent,
|
||||
"capacity_wh": self.capacity_wh,
|
||||
"initial_soc_percentage": self.initial_soc_percentage,
|
||||
"soc_wh": self.soc_wh,
|
||||
"hours": self.hours,
|
||||
"discharge_array": self.discharge_array,
|
||||
"charge_array": self.charge_array,
|
||||
"lade_effizienz": self.lade_effizienz,
|
||||
"entlade_effizienz": self.entlade_effizienz,
|
||||
"max_ladeleistung_w": self.max_ladeleistung_w,
|
||||
"charging_efficiency": self.charging_efficiency,
|
||||
"discharging_efficiency": self.discharging_efficiency,
|
||||
"max_charge_power_w": self.max_charge_power_w,
|
||||
}
|
||||
|
||||
def reset(self) -> None:
|
||||
self.soc_wh = (self.start_soc_prozent / 100) * self.kapazitaet_wh
|
||||
# Ensure soc_wh is within min and max limits
|
||||
"""Resets the battery state to its initial values."""
|
||||
self.soc_wh = (self.initial_soc_percentage / 100) * self.capacity_wh
|
||||
self.soc_wh = min(max(self.soc_wh, self.min_soc_wh), self.max_soc_wh)
|
||||
|
||||
self.discharge_array = np.full(self.hours, 1)
|
||||
self.charge_array = np.full(self.hours, 1)
|
||||
|
||||
def set_discharge_per_hour(self, discharge_array: np.ndarray) -> None:
|
||||
assert len(discharge_array) == self.hours
|
||||
"""Sets the discharge values for each hour."""
|
||||
if len(discharge_array) != self.hours:
|
||||
raise ValueError(f"Discharge array must have exactly {self.hours} elements.")
|
||||
self.discharge_array = np.array(discharge_array)
|
||||
|
||||
def set_charge_per_hour(self, charge_array: np.ndarray) -> None:
|
||||
assert len(charge_array) == self.hours
|
||||
"""Sets the charge values for each hour."""
|
||||
if len(charge_array) != self.hours:
|
||||
raise ValueError(f"Charge array must have exactly {self.hours} elements.")
|
||||
self.charge_array = np.array(charge_array)
|
||||
|
||||
def set_charge_allowed_for_hour(self, charge: float, hour: int) -> None:
|
||||
assert hour < self.hours
|
||||
"""Sets the charge for a specific hour."""
|
||||
if hour >= self.hours:
|
||||
raise ValueError(f"Hour {hour} is out of range. Must be less than {self.hours}.")
|
||||
self.charge_array[hour] = charge
|
||||
|
||||
def ladezustand_in_prozent(self) -> float:
|
||||
return (self.soc_wh / self.kapazitaet_wh) * 100
|
||||
def current_soc_percentage(self) -> float:
|
||||
"""Calculates the current state of charge in percentage."""
|
||||
return (self.soc_wh / self.capacity_wh) * 100
|
||||
|
||||
def energie_abgeben(self, wh: float, hour: int) -> tuple[float, float]:
|
||||
def discharge_energy(self, wh: float, hour: int) -> tuple[float, float]:
|
||||
"""Discharges energy from the battery."""
|
||||
if self.discharge_array[hour] == 0:
|
||||
return 0.0, 0.0 # No energy discharge and no losses
|
||||
return 0.0, 0.0
|
||||
|
||||
# Calculate the maximum energy that can be discharged considering min_soc and efficiency
|
||||
max_possible_discharge_wh = (self.soc_wh - self.min_soc_wh) * self.entlade_effizienz
|
||||
max_possible_discharge_wh = max(max_possible_discharge_wh, 0.0) # Ensure non-negative
|
||||
max_possible_discharge_wh = (self.soc_wh - self.min_soc_wh) * self.discharging_efficiency
|
||||
max_possible_discharge_wh = max(max_possible_discharge_wh, 0.0)
|
||||
|
||||
# Consider the maximum discharge power of the battery
|
||||
max_abgebbar_wh = min(max_possible_discharge_wh, self.max_ladeleistung_w)
|
||||
max_possible_discharge_wh = min(
|
||||
max_possible_discharge_wh, self.max_charge_power_w
|
||||
) # TODO make a new cfg variable max_discharge_power_w
|
||||
|
||||
# The actually discharged energy cannot exceed requested energy or maximum discharge
|
||||
tatsaechlich_abgegeben_wh = min(wh, max_abgebbar_wh)
|
||||
actual_discharge_wh = min(wh, max_possible_discharge_wh)
|
||||
actual_withdrawal_wh = (
|
||||
actual_discharge_wh / self.discharging_efficiency
|
||||
if self.discharging_efficiency > 0
|
||||
else 0.0
|
||||
)
|
||||
|
||||
# Calculate the actual amount withdrawn from the battery (before efficiency loss)
|
||||
if self.entlade_effizienz > 0:
|
||||
tatsaechliche_entnahme_wh = tatsaechlich_abgegeben_wh / self.entlade_effizienz
|
||||
else:
|
||||
tatsaechliche_entnahme_wh = 0.0
|
||||
|
||||
# Update the state of charge considering the actual withdrawal
|
||||
self.soc_wh -= tatsaechliche_entnahme_wh
|
||||
# Ensure soc_wh does not go below min_soc_wh
|
||||
self.soc_wh -= actual_withdrawal_wh
|
||||
self.soc_wh = max(self.soc_wh, self.min_soc_wh)
|
||||
|
||||
# Calculate losses due to efficiency
|
||||
verluste_wh = tatsaechliche_entnahme_wh - tatsaechlich_abgegeben_wh
|
||||
losses_wh = actual_withdrawal_wh - actual_discharge_wh
|
||||
return actual_discharge_wh, losses_wh
|
||||
|
||||
# Return the actually discharged energy and the losses
|
||||
return tatsaechlich_abgegeben_wh, verluste_wh
|
||||
|
||||
def energie_laden(
|
||||
def charge_energy(
|
||||
self, wh: Optional[float], hour: int, relative_power: float = 0.0
|
||||
) -> tuple[float, float]:
|
||||
"""Charges energy into the battery."""
|
||||
if hour is not None and self.charge_array[hour] == 0:
|
||||
return 0.0, 0.0 # Charging not allowed in this hour
|
||||
|
||||
if relative_power > 0.0:
|
||||
wh = self.max_ladeleistung_w * relative_power
|
||||
# If no value for wh is given, use the maximum charging power
|
||||
wh = wh if wh is not None else self.max_ladeleistung_w
|
||||
wh = self.max_charge_power_w * relative_power
|
||||
|
||||
# Calculate the maximum energy that can be charged considering max_soc and efficiency
|
||||
if self.lade_effizienz > 0:
|
||||
max_possible_charge_wh = (self.max_soc_wh - self.soc_wh) / self.lade_effizienz
|
||||
else:
|
||||
max_possible_charge_wh = 0.0
|
||||
max_possible_charge_wh = max(max_possible_charge_wh, 0.0) # Ensure non-negative
|
||||
wh = wh if wh is not None else self.max_charge_power_w
|
||||
|
||||
# The actually charged energy cannot exceed requested energy, charging power, or maximum possible charge
|
||||
effektive_lademenge = min(wh, max_possible_charge_wh)
|
||||
max_possible_charge_wh = (
|
||||
(self.max_soc_wh - self.soc_wh) / self.charging_efficiency
|
||||
if self.charging_efficiency > 0
|
||||
else 0.0
|
||||
)
|
||||
max_possible_charge_wh = max(max_possible_charge_wh, 0.0)
|
||||
|
||||
# Energy actually stored in the battery
|
||||
geladene_menge = effektive_lademenge * self.lade_effizienz
|
||||
effective_charge_wh = min(wh, max_possible_charge_wh)
|
||||
charged_wh = effective_charge_wh * self.charging_efficiency
|
||||
|
||||
# Update soc_wh
|
||||
self.soc_wh += geladene_menge
|
||||
# Ensure soc_wh does not exceed max_soc_wh
|
||||
self.soc_wh += charged_wh
|
||||
self.soc_wh = min(self.soc_wh, self.max_soc_wh)
|
||||
|
||||
# Calculate losses
|
||||
verluste_wh = effektive_lademenge - geladene_menge
|
||||
return geladene_menge, verluste_wh
|
||||
losses_wh = effective_charge_wh - charged_wh
|
||||
return charged_wh, losses_wh
|
||||
|
||||
def aktueller_energieinhalt(self) -> float:
|
||||
"""This method returns the current remaining energy considering efficiency.
|
||||
|
||||
It accounts for both charging and discharging efficiency.
|
||||
"""
|
||||
# Calculate remaining energy considering discharge efficiency
|
||||
nutzbare_energie = (self.soc_wh - self.min_soc_wh) * self.entlade_effizienz
|
||||
return max(nutzbare_energie, 0.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test battery discharge below min_soc
|
||||
print("Test: Discharge below min_soc")
|
||||
akku = PVAkku(
|
||||
PVAkkuParameters(
|
||||
kapazitaet_wh=10000,
|
||||
start_soc_prozent=50,
|
||||
min_soc_prozent=20,
|
||||
max_soc_prozent=80,
|
||||
),
|
||||
hours=1,
|
||||
)
|
||||
akku.reset()
|
||||
print(f"Initial SoC: {akku.ladezustand_in_prozent()}%")
|
||||
|
||||
# Try to discharge 5000 Wh
|
||||
abgegeben_wh, verlust_wh = akku.energie_abgeben(5000, 0)
|
||||
print(f"Energy discharged: {abgegeben_wh} Wh, Losses: {verlust_wh} Wh")
|
||||
print(f"SoC after discharge: {akku.ladezustand_in_prozent()}%")
|
||||
print(f"Expected min SoC: {akku.min_soc_prozent}%")
|
||||
|
||||
# Test battery charge above max_soc
|
||||
print("\nTest: Charge above max_soc")
|
||||
akku = PVAkku(
|
||||
PVAkkuParameters(
|
||||
kapazitaet_wh=10000,
|
||||
start_soc_prozent=50,
|
||||
min_soc_prozent=20,
|
||||
max_soc_prozent=80,
|
||||
),
|
||||
hours=1,
|
||||
)
|
||||
akku.reset()
|
||||
print(f"Initial SoC: {akku.ladezustand_in_prozent()}%")
|
||||
|
||||
# Try to charge 5000 Wh
|
||||
geladen_wh, verlust_wh = akku.energie_laden(5000, 0)
|
||||
print(f"Energy charged: {geladen_wh} Wh, Losses: {verlust_wh} Wh")
|
||||
print(f"SoC after charge: {akku.ladezustand_in_prozent()}%")
|
||||
print(f"Expected max SoC: {akku.max_soc_prozent}%")
|
||||
|
||||
# Test charging when battery is at max_soc
|
||||
print("\nTest: Charging when at max_soc")
|
||||
akku = PVAkku(
|
||||
PVAkkuParameters(
|
||||
kapazitaet_wh=10000,
|
||||
start_soc_prozent=80,
|
||||
min_soc_prozent=20,
|
||||
max_soc_prozent=80,
|
||||
),
|
||||
hours=1,
|
||||
)
|
||||
akku.reset()
|
||||
print(f"Initial SoC: {akku.ladezustand_in_prozent()}%")
|
||||
|
||||
geladen_wh, verlust_wh = akku.energie_laden(5000, 0)
|
||||
print(f"Energy charged: {geladen_wh} Wh, Losses: {verlust_wh} Wh")
|
||||
print(f"SoC after charge: {akku.ladezustand_in_prozent()}%")
|
||||
|
||||
# Test discharging when battery is at min_soc
|
||||
print("\nTest: Discharging when at min_soc")
|
||||
akku = PVAkku(
|
||||
PVAkkuParameters(
|
||||
kapazitaet_wh=10000,
|
||||
start_soc_prozent=20,
|
||||
min_soc_prozent=20,
|
||||
max_soc_prozent=80,
|
||||
),
|
||||
hours=1,
|
||||
)
|
||||
akku.reset()
|
||||
print(f"Initial SoC: {akku.ladezustand_in_prozent()}%")
|
||||
|
||||
abgegeben_wh, verlust_wh = akku.energie_abgeben(5000, 0)
|
||||
print(f"Energy discharged: {abgegeben_wh} Wh, Losses: {verlust_wh} Wh")
|
||||
print(f"SoC after discharge: {akku.ladezustand_in_prozent()}%")
|
||||
def current_energy_content(self) -> float:
|
||||
"""Returns the current usable energy in the battery."""
|
||||
usable_energy = (self.soc_wh - self.min_soc_wh) * self.discharging_efficiency
|
||||
return max(usable_energy, 0.0)
|
||||
|
@@ -6,7 +6,7 @@ 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.battery import Battery
|
||||
from akkudoktoreos.devices.devicesabc import DevicesBase
|
||||
from akkudoktoreos.devices.generic import HomeAppliance
|
||||
from akkudoktoreos.devices.inverter import Inverter
|
||||
@@ -25,7 +25,7 @@ class DevicesCommonSettings(SettingsBaseModel):
|
||||
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(
|
||||
battery_initial_soc: Optional[int] = Field(
|
||||
default=None, description="Battery initial state of charge [%]."
|
||||
)
|
||||
battery_soc_min: Optional[int] = Field(
|
||||
@@ -34,13 +34,13 @@ class DevicesCommonSettings(SettingsBaseModel):
|
||||
battery_soc_max: Optional[int] = Field(
|
||||
default=None, description="Battery maximum state of charge [%]."
|
||||
)
|
||||
battery_charge_efficiency: Optional[float] = Field(
|
||||
battery_charging_efficiency: Optional[float] = Field(
|
||||
default=None, description="Battery charging efficiency [%]."
|
||||
)
|
||||
battery_discharge_efficiency: Optional[float] = Field(
|
||||
battery_discharging_efficiency: Optional[float] = Field(
|
||||
default=None, description="Battery discharging efficiency [%]."
|
||||
)
|
||||
battery_charge_power_max: Optional[int] = Field(
|
||||
battery_max_charging_power: Optional[int] = Field(
|
||||
default=None, description="Battery maximum charge power [W]."
|
||||
)
|
||||
|
||||
@@ -52,19 +52,19 @@ class DevicesCommonSettings(SettingsBaseModel):
|
||||
bev_capacity: Optional[int] = Field(
|
||||
default=None, description="Battery Electric Vehicle capacity [Wh]."
|
||||
)
|
||||
bev_soc_start: Optional[int] = Field(
|
||||
bev_initial_soc: 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(
|
||||
bev_charging_efficiency: Optional[float] = Field(
|
||||
default=None, description="Battery Electric Vehicle charging efficiency [%]."
|
||||
)
|
||||
bev_discharge_efficiency: Optional[float] = Field(
|
||||
bev_discharging_efficiency: Optional[float] = Field(
|
||||
default=None, description="Battery Electric Vehicle discharging efficiency [%]."
|
||||
)
|
||||
bev_charge_power_max: Optional[int] = Field(
|
||||
bev_max_charging_power: Optional[int] = Field(
|
||||
default=None, description="Battery Electric Vehicle maximum charge power [W]."
|
||||
)
|
||||
|
||||
@@ -159,8 +159,8 @@ class Devices(SingletonMixin, DevicesBase):
|
||||
# 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")
|
||||
akku: ClassVar[Battery] = Battery(provider_id="GenericBattery")
|
||||
eauto: ClassVar[Battery] = Battery(provider_id="GenericBEV")
|
||||
home_appliance: ClassVar[HomeAppliance] = HomeAppliance(provider_id="GenericDishWasher")
|
||||
inverter: ClassVar[Inverter] = Inverter(akku=akku, provider_id="GenericInverter")
|
||||
|
||||
@@ -186,9 +186,9 @@ class Devices(SingletonMixin, DevicesBase):
|
||||
# Set initial state
|
||||
simulation_step = to_duration("1 hour")
|
||||
if self.akku:
|
||||
self.akku_soc_pro_stunde[0] = self.akku.ladezustand_in_prozent()
|
||||
self.akku_soc_pro_stunde[0] = self.akku.current_soc_percentage()
|
||||
if self.eauto:
|
||||
self.eauto_soc_pro_stunde[0] = self.eauto.ladezustand_in_prozent()
|
||||
self.eauto_soc_pro_stunde[0] = self.eauto.current_soc_percentage()
|
||||
|
||||
# Get predictions for full device simulation time range
|
||||
# gesamtlast[stunde]
|
||||
@@ -232,12 +232,12 @@ class Devices(SingletonMixin, DevicesBase):
|
||||
# E-Auto handling
|
||||
if self.eauto:
|
||||
if self.ev_charge_hours[hour] > 0:
|
||||
geladene_menge_eauto, verluste_eauto = self.eauto.energie_laden(
|
||||
geladene_menge_eauto, verluste_eauto = self.eauto.charge_energy(
|
||||
None, hour, relative_power=self.ev_charge_hours[hour]
|
||||
)
|
||||
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()
|
||||
self.eauto_soc_pro_stunde[stunde_since_now] = self.eauto.current_soc_percentage()
|
||||
|
||||
# Process inverter logic
|
||||
grid_export, grid_import, losses, self_consumption = (0.0, 0.0, 0.0, 0.0)
|
||||
@@ -252,10 +252,10 @@ class Devices(SingletonMixin, DevicesBase):
|
||||
# AC PV Battery Charge
|
||||
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(
|
||||
geladene_menge, verluste_wh = self.akku.charge_energy(
|
||||
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.current_soc_percentage())
|
||||
consumption += geladene_menge
|
||||
grid_import += geladene_menge
|
||||
self.verluste_wh_pro_stunde[stunde_since_now] += verluste_wh
|
||||
@@ -275,7 +275,7 @@ class Devices(SingletonMixin, DevicesBase):
|
||||
|
||||
# Akku SOC tracking
|
||||
if self.akku:
|
||||
self.akku_soc_pro_stunde[stunde_since_now] = self.akku.ladezustand_in_prozent()
|
||||
self.akku_soc_pro_stunde[stunde_since_now] = self.akku.current_soc_percentage()
|
||||
else:
|
||||
self.akku_soc_pro_stunde[stunde_since_now] = 0.0
|
||||
|
||||
|
@@ -2,7 +2,7 @@ from typing import Optional, Tuple
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from akkudoktoreos.devices.battery import PVAkku
|
||||
from akkudoktoreos.devices.battery import Battery
|
||||
from akkudoktoreos.devices.devicesabc import DeviceBase
|
||||
from akkudoktoreos.utils.logutil import get_logger
|
||||
|
||||
@@ -17,7 +17,7 @@ class Inverter(DeviceBase):
|
||||
def __init__(
|
||||
self,
|
||||
parameters: Optional[InverterParameters] = None,
|
||||
akku: Optional[PVAkku] = None,
|
||||
akku: Optional[Battery] = None,
|
||||
provider_id: Optional[str] = None,
|
||||
):
|
||||
# Configuration initialisation
|
||||
@@ -70,7 +70,7 @@ class Inverter(DeviceBase):
|
||||
remaining_energy = generation - actual_consumption
|
||||
|
||||
# Charge battery with excess energy
|
||||
charged_energy, charging_losses = self.akku.energie_laden(remaining_energy, hour)
|
||||
charged_energy, charging_losses = self.akku.charge_energy(remaining_energy, hour)
|
||||
losses += charging_losses
|
||||
|
||||
# Calculate remaining surplus after battery charge
|
||||
@@ -87,7 +87,7 @@ class Inverter(DeviceBase):
|
||||
available_ac_power = max(self.max_power_wh - generation, 0)
|
||||
|
||||
# Discharge battery to cover shortfall, if possible
|
||||
battery_discharge, discharge_losses = self.akku.energie_abgeben(
|
||||
battery_discharge, discharge_losses = self.akku.discharge_energy(
|
||||
min(shortfall, available_ac_power), hour
|
||||
)
|
||||
losses += discharge_losses
|
||||
|
@@ -13,10 +13,10 @@ from akkudoktoreos.core.coreabc import (
|
||||
)
|
||||
from akkudoktoreos.core.ems import EnergieManagementSystemParameters, SimulationResult
|
||||
from akkudoktoreos.devices.battery import (
|
||||
EAutoParameters,
|
||||
EAutoResult,
|
||||
PVAkku,
|
||||
PVAkkuParameters,
|
||||
Battery,
|
||||
ElectricVehicleParameters,
|
||||
ElectricVehicleResult,
|
||||
SolarPanelBatteryParameters,
|
||||
)
|
||||
from akkudoktoreos.devices.generic import HomeAppliance, HomeApplianceParameters
|
||||
from akkudoktoreos.devices.inverter import Inverter, InverterParameters
|
||||
@@ -26,9 +26,9 @@ from akkudoktoreos.visualize import visualisiere_ergebnisse
|
||||
|
||||
class OptimizationParameters(BaseModel):
|
||||
ems: EnergieManagementSystemParameters
|
||||
pv_akku: PVAkkuParameters
|
||||
pv_akku: SolarPanelBatteryParameters
|
||||
inverter: InverterParameters = InverterParameters()
|
||||
eauto: Optional[EAutoParameters]
|
||||
eauto: Optional[ElectricVehicleParameters]
|
||||
dishwasher: Optional[HomeApplianceParameters] = None
|
||||
temperature_forecast: Optional[list[float]] = Field(
|
||||
default=None,
|
||||
@@ -68,7 +68,7 @@ class OptimizeResponse(BaseModel):
|
||||
)
|
||||
eautocharge_hours_float: Optional[list[float]] = Field(description="TBD")
|
||||
result: SimulationResult
|
||||
eauto_obj: Optional[EAutoResult]
|
||||
eauto_obj: Optional[ElectricVehicleResult]
|
||||
start_solution: Optional[list[float]] = Field(
|
||||
default=None,
|
||||
description="An array of binary values (0 or 1) representing a possible starting solution for the simulation.",
|
||||
@@ -92,8 +92,8 @@ class OptimizeResponse(BaseModel):
|
||||
mode="before",
|
||||
)
|
||||
def convert_eauto(cls, field: Any) -> Any:
|
||||
if isinstance(field, PVAkku):
|
||||
return EAutoResult(**field.to_dict())
|
||||
if isinstance(field, Battery):
|
||||
return ElectricVehicleResult(**field.to_dict())
|
||||
return field
|
||||
|
||||
|
||||
@@ -367,7 +367,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
|
||||
)
|
||||
|
||||
# Penalty for not meeting the minimum SOC (State of Charge) requirement
|
||||
# if parameters.eauto_min_soc_prozent - ems.eauto.ladezustand_in_prozent() <= 0.0 and self.optimize_ev:
|
||||
# if parameters.eauto_min_soc_prozent - ems.eauto.current_soc_percentage() <= 0.0 and self.optimize_ev:
|
||||
# gesamtbilanz += sum(
|
||||
# self.config.optimization_penalty for ladeleistung in eautocharge_hours_float if ladeleistung != 0.0
|
||||
# )
|
||||
@@ -375,7 +375,7 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
|
||||
individual.extra_data = ( # type: ignore[attr-defined]
|
||||
o["Gesamtbilanz_Euro"],
|
||||
o["Gesamt_Verluste"],
|
||||
parameters.eauto.min_soc_prozent - self.ems.eauto.ladezustand_in_prozent()
|
||||
parameters.eauto.min_soc_percentage - self.ems.eauto.current_soc_percentage()
|
||||
if parameters.eauto and self.ems.eauto
|
||||
else 0,
|
||||
)
|
||||
@@ -383,16 +383,16 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
|
||||
# Adjust total balance with battery value and penalties for unmet SOC
|
||||
|
||||
restwert_akku = (
|
||||
self.ems.akku.aktueller_energieinhalt() * parameters.ems.preis_euro_pro_wh_akku
|
||||
self.ems.akku.current_energy_content() * parameters.ems.preis_euro_pro_wh_akku
|
||||
)
|
||||
# print(ems.akku.aktueller_energieinhalt()," * ", parameters.ems.preis_euro_pro_wh_akku , " ", restwert_akku, " ", gesamtbilanz)
|
||||
# print(ems.akku.current_energy_content()," * ", parameters.ems.preis_euro_pro_wh_akku , " ", restwert_akku, " ", gesamtbilanz)
|
||||
gesamtbilanz += -restwert_akku
|
||||
# print(gesamtbilanz)
|
||||
if self.optimize_ev:
|
||||
gesamtbilanz += max(
|
||||
0,
|
||||
(
|
||||
parameters.eauto.min_soc_prozent - self.ems.eauto.ladezustand_in_prozent()
|
||||
parameters.eauto.min_soc_percentage - self.ems.eauto.current_soc_percentage()
|
||||
if parameters.eauto and self.ems.eauto
|
||||
else 0
|
||||
)
|
||||
@@ -458,21 +458,21 @@ class optimization_problem(ConfigMixin, DevicesMixin, EnergyManagementSystemMixi
|
||||
)
|
||||
|
||||
# Initialize PV and EV batteries
|
||||
akku = PVAkku(
|
||||
akku = Battery(
|
||||
parameters.pv_akku,
|
||||
hours=self.config.prediction_hours,
|
||||
)
|
||||
akku.set_charge_per_hour(np.full(self.config.prediction_hours, 1))
|
||||
|
||||
eauto: Optional[PVAkku] = None
|
||||
eauto: Optional[Battery] = None
|
||||
if parameters.eauto:
|
||||
eauto = PVAkku(
|
||||
eauto = Battery(
|
||||
parameters.eauto,
|
||||
hours=self.config.prediction_hours,
|
||||
)
|
||||
eauto.set_charge_per_hour(np.full(self.config.prediction_hours, 1))
|
||||
self.optimize_ev = (
|
||||
parameters.eauto.min_soc_prozent - parameters.eauto.start_soc_prozent >= 0
|
||||
parameters.eauto.min_soc_percentage - parameters.eauto.initial_soc_percentage >= 0
|
||||
)
|
||||
else:
|
||||
self.optimize_ev = False
|
||||
|
Reference in New Issue
Block a user