mirror of
				https://github.com/Akkudoktor-EOS/EOS.git
				synced 2025-10-30 22:36:21 +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