2024-11-26 22:28:05 +01:00
|
|
|
|
from typing import Any, Optional
|
2024-11-15 22:27:25 +01:00
|
|
|
|
|
2024-02-18 15:53:29 +01:00
|
|
|
|
import numpy as np
|
2024-11-26 22:28:05 +01:00
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
2024-11-15 22:27:25 +01:00
|
|
|
|
|
2025-01-05 14:41:07 +01:00
|
|
|
|
from akkudoktoreos.core.logging import get_logger
|
2025-01-13 21:44:17 +01:00
|
|
|
|
from akkudoktoreos.core.pydantic import ParametersBaseModel
|
2024-12-15 14:40:03 +01:00
|
|
|
|
from akkudoktoreos.devices.devicesabc import DeviceBase
|
2024-11-26 22:28:05 +01:00
|
|
|
|
from akkudoktoreos.utils.utils import NumpyEncoder
|
2024-11-15 22:27:25 +01:00
|
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
2024-11-26 22:28:05 +01:00
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
def max_charging_power_field(description: Optional[str] = None) -> float:
|
|
|
|
|
if description is None:
|
|
|
|
|
description = "Maximum charging power in watts."
|
2024-11-15 22:27:25 +01:00
|
|
|
|
return Field(
|
2024-12-19 14:50:19 +01:00
|
|
|
|
default=5000,
|
2024-11-15 22:27:25 +01:00
|
|
|
|
gt=0,
|
2024-12-19 14:50:19 +01:00
|
|
|
|
description=description,
|
2024-11-15 22:27:25 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
def initial_soc_percentage_field(description: str) -> int:
|
2024-11-26 22:28:05 +01:00
|
|
|
|
return Field(default=0, ge=0, le=100, description=description)
|
2024-11-15 22:27:25 +01:00
|
|
|
|
|
|
|
|
|
|
2025-01-13 21:44:17 +01:00
|
|
|
|
class BaseBatteryParameters(ParametersBaseModel):
|
2024-12-19 14:50:19 +01:00
|
|
|
|
"""Base class for battery parameters with fields for capacity, efficiency, and state of charge."""
|
|
|
|
|
|
|
|
|
|
capacity_wh: int = Field(
|
2024-11-15 22:27:25 +01:00
|
|
|
|
gt=0, description="An integer representing the capacity of the battery in watt-hours."
|
|
|
|
|
)
|
2024-12-19 14:50:19 +01:00
|
|
|
|
charging_efficiency: float = Field(
|
2024-11-26 22:28:05 +01:00
|
|
|
|
default=0.88,
|
|
|
|
|
gt=0,
|
|
|
|
|
le=1,
|
|
|
|
|
description="A float representing the charging efficiency of the battery.",
|
2024-11-15 22:27:25 +01:00
|
|
|
|
)
|
2024-12-19 14:50:19 +01:00
|
|
|
|
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(
|
2024-11-15 22:27:25 +01:00
|
|
|
|
"An integer representing the state of charge of the battery at the **start** of the current hour (not the current state)."
|
|
|
|
|
)
|
2024-12-19 14:50:19 +01:00
|
|
|
|
min_soc_percentage: int = Field(
|
2024-11-26 22:28:05 +01:00
|
|
|
|
default=0,
|
2024-11-15 22:27:25 +01:00
|
|
|
|
ge=0,
|
|
|
|
|
le=100,
|
|
|
|
|
description="An integer representing the minimum state of charge (SOC) of the battery in percentage.",
|
|
|
|
|
)
|
2024-12-19 14:50:19 +01:00
|
|
|
|
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.",
|
|
|
|
|
)
|
2024-11-15 22:27:25 +01:00
|
|
|
|
|
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
class SolarPanelBatteryParameters(BaseBatteryParameters):
|
|
|
|
|
max_charge_power_w: Optional[float] = max_charging_power_field()
|
2024-11-15 22:27:25 +01:00
|
|
|
|
|
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
class ElectricVehicleParameters(BaseBatteryParameters):
|
|
|
|
|
"""Parameters specific to an electric vehicle (EV)."""
|
|
|
|
|
|
|
|
|
|
discharging_efficiency: float = 1.0
|
|
|
|
|
initial_soc_percentage: int = initial_soc_percentage_field(
|
2024-11-15 22:27:25 +01:00
|
|
|
|
"An integer representing the current state of charge (SOC) of the battery in percentage."
|
|
|
|
|
)
|
2024-09-20 12:02:31 +02:00
|
|
|
|
|
2024-10-03 11:05:44 +02:00
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
class ElectricVehicleResult(BaseModel):
|
|
|
|
|
"""Result class containing information related to the electric vehicle's charging and discharging behavior."""
|
2024-11-26 22:28:05 +01:00
|
|
|
|
|
|
|
|
|
charge_array: list[float] = Field(
|
2024-12-19 14:50:19 +01:00
|
|
|
|
description="Hourly charging status (0 for no charging, 1 for charging)."
|
2024-11-26 22:28:05 +01:00
|
|
|
|
)
|
|
|
|
|
discharge_array: list[int] = Field(
|
2024-12-19 14:50:19 +01:00
|
|
|
|
description="Hourly discharging status (0 for no discharging, 1 for discharging)."
|
2024-11-26 22:28:05 +01:00
|
|
|
|
)
|
2024-12-19 14:50:19 +01:00
|
|
|
|
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.")
|
2024-11-26 22:28:05 +01:00
|
|
|
|
soc_wh: float = Field(
|
2024-12-19 14:50:19 +01:00
|
|
|
|
description="State of charge of the battery in watt-hours at the start of the simulation."
|
2024-11-26 22:28:05 +01:00
|
|
|
|
)
|
2024-12-19 14:50:19 +01:00
|
|
|
|
initial_soc_percentage: int = Field(
|
|
|
|
|
description="State of charge at the start of the simulation in percentage."
|
2024-11-26 22:28:05 +01:00
|
|
|
|
)
|
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
@field_validator("discharge_array", "charge_array", mode="before")
|
2024-11-26 22:28:05 +01:00
|
|
|
|
def convert_numpy(cls, field: Any) -> Any:
|
|
|
|
|
return NumpyEncoder.convert_numpy(field)[0]
|
|
|
|
|
|
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
class Battery(DeviceBase):
|
|
|
|
|
"""Represents a battery device with methods to simulate energy charging and discharging."""
|
|
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
2024-12-19 14:50:19 +01:00
|
|
|
|
parameters: Optional[BaseBatteryParameters] = None,
|
2024-12-15 14:40:03 +01:00
|
|
|
|
hours: Optional[int] = 24,
|
|
|
|
|
provider_id: Optional[str] = None,
|
|
|
|
|
):
|
2024-12-19 14:50:19 +01:00
|
|
|
|
# Initialize configuration and parameters
|
2024-12-15 14:40:03 +01:00
|
|
|
|
self.provider_id = provider_id
|
|
|
|
|
self.prefix = "<invalid>"
|
|
|
|
|
if self.provider_id == "GenericBattery":
|
|
|
|
|
self.prefix = "battery"
|
|
|
|
|
elif self.provider_id == "GenericBEV":
|
|
|
|
|
self.prefix = "bev"
|
2024-12-19 14:50:19 +01:00
|
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
|
self.parameters = parameters
|
|
|
|
|
if hours is None:
|
2024-12-19 14:50:19 +01:00
|
|
|
|
self.hours = self.total_hours # TODO where does that come from?
|
2024-12-15 14:40:03 +01:00
|
|
|
|
else:
|
|
|
|
|
self.hours = hours
|
|
|
|
|
|
|
|
|
|
self.initialised = False
|
2024-12-19 14:50:19 +01:00
|
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
|
# 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:
|
2024-12-19 14:50:19 +01:00
|
|
|
|
"""Sets up the battery parameters based on configuration or provided parameters."""
|
2024-12-15 14:40:03 +01:00
|
|
|
|
if self.initialised:
|
|
|
|
|
return
|
2024-12-19 14:50:19 +01:00
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
|
if self.provider_id == "GenericBattery":
|
2024-12-19 14:50:19 +01:00
|
|
|
|
self.min_soc_percentage = getattr(
|
|
|
|
|
self.config,
|
|
|
|
|
f"{self.prefix}_soc_min",
|
|
|
|
|
)
|
2024-12-15 14:40:03 +01:00
|
|
|
|
else:
|
2024-12-19 14:50:19 +01:00
|
|
|
|
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
|
2024-12-15 14:40:03 +01:00
|
|
|
|
# Only assign for storage battery
|
2024-12-19 14:50:19 +01:00
|
|
|
|
self.min_soc_percentage = (
|
|
|
|
|
self.parameters.min_soc_percentage
|
|
|
|
|
if isinstance(self.parameters, SolarPanelBatteryParameters)
|
2024-12-15 14:40:03 +01:00
|
|
|
|
else 0
|
|
|
|
|
)
|
2024-12-19 14:50:19 +01:00
|
|
|
|
self.max_soc_percentage = self.parameters.max_soc_percentage
|
2024-12-15 14:40:03 +01:00
|
|
|
|
else:
|
2024-12-19 14:50:19 +01:00
|
|
|
|
error_msg = "Parameters and provider ID are missing. Cannot instantiate."
|
2024-12-15 14:40:03 +01:00
|
|
|
|
logger.error(error_msg)
|
|
|
|
|
raise ValueError(error_msg)
|
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
# 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
|
2024-02-25 16:47:28 +01:00
|
|
|
|
self.discharge_array = np.full(self.hours, 1)
|
2024-10-16 15:40:04 +02:00
|
|
|
|
self.charge_array = np.full(self.hours, 1)
|
2024-12-19 14:50:19 +01:00
|
|
|
|
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
|
2024-10-03 09:20:15 +02:00
|
|
|
|
|
2024-12-15 14:40:03 +01:00
|
|
|
|
self.initialised = True
|
|
|
|
|
|
2024-11-26 22:28:05 +01:00
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
2024-12-19 14:50:19 +01:00
|
|
|
|
"""Converts the object to a dictionary representation."""
|
2024-03-28 08:16:57 +01:00
|
|
|
|
return {
|
2024-12-19 14:50:19 +01:00
|
|
|
|
"capacity_wh": self.capacity_wh,
|
|
|
|
|
"initial_soc_percentage": self.initial_soc_percentage,
|
2024-03-28 08:16:57 +01:00
|
|
|
|
"soc_wh": self.soc_wh,
|
|
|
|
|
"hours": self.hours,
|
2024-11-26 22:28:05 +01:00
|
|
|
|
"discharge_array": self.discharge_array,
|
|
|
|
|
"charge_array": self.charge_array,
|
2024-12-19 14:50:19 +01:00
|
|
|
|
"charging_efficiency": self.charging_efficiency,
|
|
|
|
|
"discharging_efficiency": self.discharging_efficiency,
|
|
|
|
|
"max_charge_power_w": self.max_charge_power_w,
|
2024-03-28 08:16:57 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-11-26 22:28:05 +01:00
|
|
|
|
def reset(self) -> None:
|
2024-12-19 14:50:19 +01:00
|
|
|
|
"""Resets the battery state to its initial values."""
|
|
|
|
|
self.soc_wh = (self.initial_soc_percentage / 100) * self.capacity_wh
|
2024-10-03 09:20:15 +02:00
|
|
|
|
self.soc_wh = min(max(self.soc_wh, self.min_soc_wh), self.max_soc_wh)
|
2024-02-25 16:47:28 +01:00
|
|
|
|
self.discharge_array = np.full(self.hours, 1)
|
2024-10-16 15:40:04 +02:00
|
|
|
|
self.charge_array = np.full(self.hours, 1)
|
2024-09-20 12:02:31 +02:00
|
|
|
|
|
2024-11-26 22:28:05 +01:00
|
|
|
|
def set_discharge_per_hour(self, discharge_array: np.ndarray) -> None:
|
2024-12-19 14:50:19 +01:00
|
|
|
|
"""Sets the discharge values for each hour."""
|
|
|
|
|
if len(discharge_array) != self.hours:
|
|
|
|
|
raise ValueError(f"Discharge array must have exactly {self.hours} elements.")
|
2024-03-28 08:16:57 +01:00
|
|
|
|
self.discharge_array = np.array(discharge_array)
|
2024-02-18 14:32:27 +01:00
|
|
|
|
|
2024-11-26 22:28:05 +01:00
|
|
|
|
def set_charge_per_hour(self, charge_array: np.ndarray) -> None:
|
2024-12-19 14:50:19 +01:00
|
|
|
|
"""Sets the charge values for each hour."""
|
|
|
|
|
if len(charge_array) != self.hours:
|
|
|
|
|
raise ValueError(f"Charge array must have exactly {self.hours} elements.")
|
2024-03-28 08:16:57 +01:00
|
|
|
|
self.charge_array = np.array(charge_array)
|
2024-03-25 14:40:48 +01:00
|
|
|
|
|
2024-11-26 22:28:05 +01:00
|
|
|
|
def set_charge_allowed_for_hour(self, charge: float, hour: int) -> None:
|
2024-12-19 14:50:19 +01:00
|
|
|
|
"""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}.")
|
2024-10-16 15:40:04 +02:00
|
|
|
|
self.charge_array[hour] = charge
|
2024-10-14 10:10:12 +02:00
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
def current_soc_percentage(self) -> float:
|
|
|
|
|
"""Calculates the current state of charge in percentage."""
|
|
|
|
|
return (self.soc_wh / self.capacity_wh) * 100
|
2024-02-18 14:32:27 +01:00
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
def discharge_energy(self, wh: float, hour: int) -> tuple[float, float]:
|
|
|
|
|
"""Discharges energy from the battery."""
|
2024-10-22 10:29:57 +02:00
|
|
|
|
if self.discharge_array[hour] == 0:
|
2024-12-19 14:50:19 +01:00
|
|
|
|
return 0.0, 0.0
|
2024-10-03 09:20:15 +02:00
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
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)
|
2024-09-20 12:02:31 +02:00
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
max_possible_discharge_wh = min(
|
|
|
|
|
max_possible_discharge_wh, self.max_charge_power_w
|
|
|
|
|
) # TODO make a new cfg variable max_discharge_power_w
|
2024-10-03 09:20:15 +02:00
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
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
|
|
|
|
|
)
|
2024-10-03 09:20:15 +02:00
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
self.soc_wh -= actual_withdrawal_wh
|
2024-10-03 09:20:15 +02:00
|
|
|
|
self.soc_wh = max(self.soc_wh, self.min_soc_wh)
|
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
losses_wh = actual_withdrawal_wh - actual_discharge_wh
|
|
|
|
|
return actual_discharge_wh, losses_wh
|
2024-10-03 09:20:15 +02:00
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
def charge_energy(
|
2024-11-26 22:28:05 +01:00
|
|
|
|
self, wh: Optional[float], hour: int, relative_power: float = 0.0
|
|
|
|
|
) -> tuple[float, float]:
|
2024-12-19 14:50:19 +01:00
|
|
|
|
"""Charges energy into the battery."""
|
2024-03-25 14:40:48 +01:00
|
|
|
|
if hour is not None and self.charge_array[hour] == 0:
|
2024-11-26 22:28:05 +01:00
|
|
|
|
return 0.0, 0.0 # Charging not allowed in this hour
|
2024-12-19 14:50:19 +01:00
|
|
|
|
|
2024-10-16 15:40:04 +02:00
|
|
|
|
if relative_power > 0.0:
|
2024-12-19 14:50:19 +01:00
|
|
|
|
wh = self.max_charge_power_w * relative_power
|
2024-10-03 09:20:15 +02:00
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
wh = wh if wh is not None else self.max_charge_power_w
|
2024-03-25 14:40:48 +01:00
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
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)
|
2024-10-22 10:29:57 +02:00
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
effective_charge_wh = min(wh, max_possible_charge_wh)
|
|
|
|
|
charged_wh = effective_charge_wh * self.charging_efficiency
|
2024-05-02 10:27:33 +02:00
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
self.soc_wh += charged_wh
|
2024-10-03 09:20:15 +02:00
|
|
|
|
self.soc_wh = min(self.soc_wh, self.max_soc_wh)
|
2024-10-22 10:29:57 +02:00
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
losses_wh = effective_charge_wh - charged_wh
|
|
|
|
|
return charged_wh, losses_wh
|
2024-10-03 09:20:15 +02:00
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
|
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)
|