2024-12-19 14:50:19 +01:00
|
|
|
import numpy as np
|
|
|
|
|
import pytest
|
|
|
|
|
|
2025-10-28 02:50:31 +01:00
|
|
|
from akkudoktoreos.devices.genetic.battery import Battery, SolarPanelBatteryParameters
|
2024-12-19 14:50:19 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def setup_pv_battery():
|
2025-11-08 15:42:18 +01:00
|
|
|
device_id="battery1"
|
|
|
|
|
capacity_wh=10000
|
|
|
|
|
initial_soc_percentage=50
|
|
|
|
|
charging_efficiency=0.88
|
|
|
|
|
discharging_efficiency=0.88
|
|
|
|
|
min_soc_percentage=20
|
|
|
|
|
max_soc_percentage=80
|
|
|
|
|
max_charge_power_w=8000
|
|
|
|
|
hours=24
|
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
params = SolarPanelBatteryParameters(
|
2025-11-08 15:42:18 +01:00
|
|
|
device_id=device_id,
|
|
|
|
|
capacity_wh=capacity_wh,
|
|
|
|
|
initial_soc_percentage=initial_soc_percentage,
|
|
|
|
|
charging_efficiency=charging_efficiency,
|
|
|
|
|
discharging_efficiency=discharging_efficiency,
|
|
|
|
|
min_soc_percentage=min_soc_percentage,
|
|
|
|
|
max_soc_percentage=max_soc_percentage,
|
|
|
|
|
max_charge_power_w=max_charge_power_w,
|
|
|
|
|
hours=hours,
|
2024-12-19 14:50:19 +01:00
|
|
|
)
|
2025-10-28 02:50:31 +01:00
|
|
|
battery = Battery(
|
|
|
|
|
params,
|
|
|
|
|
prediction_hours=48,
|
|
|
|
|
)
|
2024-12-19 14:50:19 +01:00
|
|
|
battery.reset()
|
2025-11-08 15:42:18 +01:00
|
|
|
|
|
|
|
|
assert battery.parameters.device_id==device_id
|
|
|
|
|
assert battery.capacity_wh==capacity_wh
|
|
|
|
|
assert battery.initial_soc_percentage==initial_soc_percentage
|
|
|
|
|
assert battery.charging_efficiency==charging_efficiency
|
|
|
|
|
assert battery.initial_soc_percentage==initial_soc_percentage
|
|
|
|
|
assert battery.discharging_efficiency==discharging_efficiency
|
|
|
|
|
assert battery.max_soc_percentage==max_soc_percentage
|
|
|
|
|
assert battery.max_charge_power_w==max_charge_power_w
|
|
|
|
|
assert battery.soc_wh==float((initial_soc_percentage / 100) * capacity_wh)
|
|
|
|
|
assert battery.min_soc_wh==float((min_soc_percentage / 100) * capacity_wh)
|
|
|
|
|
assert battery.max_soc_wh==float((max_soc_percentage / 100) * capacity_wh)
|
|
|
|
|
assert np.all(battery.charge_array == 0)
|
|
|
|
|
assert np.all(battery.discharge_array == 0)
|
|
|
|
|
|
|
|
|
|
# Init for test
|
|
|
|
|
battery.charge_array = np.full(battery.prediction_hours, 1)
|
|
|
|
|
battery.discharge_array = np.full(battery.prediction_hours, 1)
|
|
|
|
|
assert np.all(battery.charge_array == 1)
|
|
|
|
|
assert np.all(battery.discharge_array == 1)
|
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
return battery
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_initial_state_of_charge(setup_pv_battery):
|
|
|
|
|
battery = setup_pv_battery
|
|
|
|
|
assert battery.current_soc_percentage() == 50.0, "Initial SoC should be 50%"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_battery_discharge_below_min_soc(setup_pv_battery):
|
|
|
|
|
battery = setup_pv_battery
|
|
|
|
|
discharged_wh, loss_wh = battery.discharge_energy(5000, 0)
|
|
|
|
|
|
|
|
|
|
# Ensure it discharges energy and stops at the min SOC
|
|
|
|
|
assert discharged_wh > 0
|
|
|
|
|
print(discharged_wh, loss_wh, battery.current_soc_percentage(), battery.min_soc_percentage)
|
|
|
|
|
assert battery.current_soc_percentage() >= 20 # Ensure it's above min_soc_percentage
|
|
|
|
|
assert loss_wh >= 0 # Losses should not be negative
|
|
|
|
|
assert discharged_wh == 2640.0, "The energy discharged should be limited by min_soc"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_battery_charge_above_max_soc(setup_pv_battery):
|
|
|
|
|
battery = setup_pv_battery
|
|
|
|
|
charged_wh, loss_wh = battery.charge_energy(5000, 0)
|
|
|
|
|
|
|
|
|
|
# Ensure it charges energy and stops at the max SOC
|
|
|
|
|
assert charged_wh > 0
|
|
|
|
|
assert battery.current_soc_percentage() <= 80 # Ensure it's below max_soc_percentage
|
|
|
|
|
assert loss_wh >= 0 # Losses should not be negative
|
|
|
|
|
assert charged_wh == 3000.0, "The energy charged should be limited by max_soc"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_battery_charge_when_full(setup_pv_battery):
|
|
|
|
|
battery = setup_pv_battery
|
|
|
|
|
battery.soc_wh = battery.max_soc_wh # Set battery to full
|
|
|
|
|
charged_wh, loss_wh = battery.charge_energy(5000, 0)
|
|
|
|
|
|
|
|
|
|
# No charging should happen if battery is full
|
|
|
|
|
assert charged_wh == 0
|
|
|
|
|
assert loss_wh == 0
|
|
|
|
|
assert battery.current_soc_percentage() == 80, "SoC should remain at max_soc"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_battery_discharge_when_empty(setup_pv_battery):
|
|
|
|
|
battery = setup_pv_battery
|
|
|
|
|
battery.soc_wh = battery.min_soc_wh # Set battery to minimum SOC
|
|
|
|
|
discharged_wh, loss_wh = battery.discharge_energy(5000, 0)
|
|
|
|
|
|
|
|
|
|
# No discharge should happen if battery is at min SOC
|
|
|
|
|
assert discharged_wh == 0
|
|
|
|
|
assert loss_wh == 0
|
|
|
|
|
assert battery.current_soc_percentage() == 20, "SoC should remain at min_soc"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_battery_discharge_exactly_min_soc(setup_pv_battery):
|
|
|
|
|
battery = setup_pv_battery
|
|
|
|
|
battery.soc_wh = battery.min_soc_wh # Set battery to exactly min SOC
|
|
|
|
|
discharged_wh, loss_wh = battery.discharge_energy(1000, 0)
|
|
|
|
|
|
|
|
|
|
# Battery should not go below the min SOC
|
|
|
|
|
assert discharged_wh == 0
|
|
|
|
|
assert battery.current_soc_percentage() == 20 # SOC should remain at min_SOC
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_battery_charge_exactly_max_soc(setup_pv_battery):
|
|
|
|
|
battery = setup_pv_battery
|
|
|
|
|
battery.soc_wh = battery.max_soc_wh # Set battery to exactly max SOC
|
|
|
|
|
charged_wh, loss_wh = battery.charge_energy(1000, 0)
|
|
|
|
|
|
|
|
|
|
# Battery should not exceed the max SOC
|
|
|
|
|
assert charged_wh == 0
|
|
|
|
|
assert battery.current_soc_percentage() == 80 # SOC should remain at max_SOC
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_battery_reset_function(setup_pv_battery):
|
|
|
|
|
battery = setup_pv_battery
|
|
|
|
|
battery.soc_wh = 8000 # Change the SOC to some value
|
|
|
|
|
battery.reset()
|
|
|
|
|
|
|
|
|
|
# After reset, SOC should be equal to the initial value
|
|
|
|
|
assert battery.current_soc_percentage() == battery.initial_soc_percentage
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_soc_limits(setup_pv_battery):
|
|
|
|
|
battery = setup_pv_battery
|
|
|
|
|
|
|
|
|
|
# Manually set SoC above max limit
|
|
|
|
|
battery.soc_wh = battery.max_soc_wh + 1000
|
|
|
|
|
battery.soc_wh = min(battery.soc_wh, battery.max_soc_wh)
|
|
|
|
|
assert battery.current_soc_percentage() <= 80, "SoC should not exceed max_soc"
|
|
|
|
|
|
|
|
|
|
# Manually set SoC below min limit
|
|
|
|
|
battery.soc_wh = battery.min_soc_wh - 1000
|
|
|
|
|
battery.soc_wh = max(battery.soc_wh, battery.min_soc_wh)
|
|
|
|
|
assert battery.current_soc_percentage() >= 20, "SoC should not drop below min_soc"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_max_charge_power_w(setup_pv_battery):
|
|
|
|
|
battery = setup_pv_battery
|
2025-02-24 10:00:09 +01:00
|
|
|
assert battery.parameters.max_charge_power_w == 8000, (
|
|
|
|
|
"Default max charge power should be 5000W, We ask for 8000W here"
|
|
|
|
|
)
|
2024-12-19 14:50:19 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_charge_energy_within_limits(setup_pv_battery):
|
|
|
|
|
battery = setup_pv_battery
|
|
|
|
|
initial_soc_wh = battery.soc_wh
|
|
|
|
|
|
|
|
|
|
charged_wh, losses_wh = battery.charge_energy(wh=4000, hour=1)
|
|
|
|
|
|
|
|
|
|
assert charged_wh > 0, "Charging should add energy"
|
|
|
|
|
assert losses_wh >= 0, "Losses should not be negative"
|
|
|
|
|
assert battery.soc_wh > initial_soc_wh, "State of charge should increase after charging"
|
|
|
|
|
assert battery.soc_wh <= battery.max_soc_wh, "SOC should not exceed max SOC"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_charge_energy_exceeds_capacity(setup_pv_battery):
|
|
|
|
|
battery = setup_pv_battery
|
|
|
|
|
initial_soc_wh = battery.soc_wh
|
|
|
|
|
|
|
|
|
|
# Try to overcharge beyond max capacity
|
|
|
|
|
charged_wh, losses_wh = battery.charge_energy(wh=20000, hour=2)
|
|
|
|
|
|
2025-02-24 10:00:09 +01:00
|
|
|
assert charged_wh + initial_soc_wh <= battery.max_soc_wh, (
|
|
|
|
|
"Charging should not exceed max capacity"
|
|
|
|
|
)
|
2024-12-19 14:50:19 +01:00
|
|
|
assert losses_wh >= 0, "Losses should not be negative"
|
|
|
|
|
assert battery.soc_wh == battery.max_soc_wh, "SOC should be at max after overcharge attempt"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_charge_energy_not_allowed_hour(setup_pv_battery):
|
|
|
|
|
battery = setup_pv_battery
|
|
|
|
|
|
|
|
|
|
# Disable charging for all hours
|
2025-10-28 02:50:31 +01:00
|
|
|
battery.set_charge_per_hour(np.zeros(battery.prediction_hours))
|
2024-12-19 14:50:19 +01:00
|
|
|
|
|
|
|
|
charged_wh, losses_wh = battery.charge_energy(wh=4000, hour=3)
|
|
|
|
|
|
|
|
|
|
assert charged_wh == 0, "No energy should be charged in disallowed hours"
|
|
|
|
|
assert losses_wh == 0, "No losses should occur if charging is not allowed"
|
|
|
|
|
assert (
|
|
|
|
|
battery.soc_wh == (battery.parameters.initial_soc_percentage / 100) * battery.capacity_wh
|
|
|
|
|
), "SOC should remain unchanged"
|
|
|
|
|
|
|
|
|
|
|
2025-11-08 15:42:18 +01:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"wh, charge_factor, expected_raises",
|
|
|
|
|
[
|
|
|
|
|
(None, 0.5, False), # Expected to work normally (if capacity allows)
|
|
|
|
|
(None, 1.0, False), # Often still OK, depending on fixture capacity
|
|
|
|
|
(None, 2.0, False), # Exceeds max charge → always ValueError
|
|
|
|
|
(1000, 0, False),
|
|
|
|
|
(1000, 1.0, True),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_charge_energy_with_charge_factor(setup_pv_battery, wh, charge_factor, expected_raises):
|
2024-12-19 14:50:19 +01:00
|
|
|
battery = setup_pv_battery
|
2025-11-08 15:42:18 +01:00
|
|
|
hour = 4
|
2024-12-19 14:50:19 +01:00
|
|
|
|
2025-11-08 15:42:18 +01:00
|
|
|
if wh is not None and charge_factor == 0.0: # mode 1
|
|
|
|
|
raw_request_wh = wh
|
|
|
|
|
else:
|
|
|
|
|
raw_request_wh = battery.max_charge_power_w * charge_factor
|
|
|
|
|
raw_capacity_wh = max(battery.max_soc_wh - battery.soc_wh, 0.0)
|
2024-12-19 14:50:19 +01:00
|
|
|
|
2025-11-08 15:42:18 +01:00
|
|
|
if expected_raises:
|
|
|
|
|
# Should raise
|
|
|
|
|
with pytest.raises(ValueError):
|
|
|
|
|
battery.charge_energy(
|
|
|
|
|
wh=wh,
|
|
|
|
|
hour=hour,
|
|
|
|
|
charge_factor=charge_factor,
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Should NOT raise
|
|
|
|
|
charged_wh, losses_wh = battery.charge_energy(
|
|
|
|
|
wh=wh,
|
|
|
|
|
hour=hour,
|
|
|
|
|
charge_factor=charge_factor,
|
2025-02-24 10:00:09 +01:00
|
|
|
)
|
2025-11-08 15:42:18 +01:00
|
|
|
|
|
|
|
|
# Expectations
|
|
|
|
|
assert charged_wh > 0, "Charging should occur with charge factor"
|
|
|
|
|
assert losses_wh >= 0, "Losses must not be negative"
|
|
|
|
|
assert charged_wh <= raw_request_wh, "Charging must not exceed request"
|
2024-12-19 14:50:19 +01:00
|
|
|
assert battery.soc_wh > 0, "SOC should increase after charging"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def setup_car_battery():
|
2025-10-28 02:50:31 +01:00
|
|
|
from akkudoktoreos.optimization.genetic.geneticparams import (
|
|
|
|
|
ElectricVehicleParameters,
|
|
|
|
|
)
|
2024-12-19 14:50:19 +01:00
|
|
|
|
|
|
|
|
params = ElectricVehicleParameters(
|
2025-01-12 05:19:37 +01:00
|
|
|
device_id="ev1",
|
2024-12-19 14:50:19 +01:00
|
|
|
capacity_wh=40000,
|
|
|
|
|
initial_soc_percentage=60,
|
|
|
|
|
min_soc_percentage=10,
|
|
|
|
|
max_soc_percentage=90,
|
|
|
|
|
max_charge_power_w=7000,
|
2025-01-12 05:19:37 +01:00
|
|
|
hours=24,
|
2024-12-19 14:50:19 +01:00
|
|
|
)
|
2025-10-28 02:50:31 +01:00
|
|
|
battery = Battery(
|
|
|
|
|
params,
|
|
|
|
|
prediction_hours=48,
|
|
|
|
|
)
|
2024-12-19 14:50:19 +01:00
|
|
|
battery.reset()
|
2025-11-08 15:42:18 +01:00
|
|
|
|
|
|
|
|
# Init for test
|
|
|
|
|
battery.charge_array = np.full(battery.prediction_hours, 1)
|
|
|
|
|
battery.discharge_array = np.full(battery.prediction_hours, 1)
|
|
|
|
|
assert np.all(battery.charge_array == 1)
|
|
|
|
|
assert np.all(battery.discharge_array == 1)
|
|
|
|
|
|
2024-12-19 14:50:19 +01:00
|
|
|
return battery
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_car_and_pv_battery_discharge_and_max_charge_power(setup_pv_battery, setup_car_battery):
|
|
|
|
|
pv_battery = setup_pv_battery
|
|
|
|
|
car_battery = setup_car_battery
|
|
|
|
|
|
|
|
|
|
# Test discharge for PV battery
|
|
|
|
|
pv_discharged_wh, pv_loss_wh = pv_battery.discharge_energy(3000, 5)
|
|
|
|
|
assert pv_discharged_wh > 0, "PV battery should discharge energy"
|
2025-02-24 10:00:09 +01:00
|
|
|
assert pv_battery.current_soc_percentage() >= pv_battery.parameters.min_soc_percentage, (
|
|
|
|
|
"PV battery SOC should stay above min SOC"
|
|
|
|
|
)
|
|
|
|
|
assert pv_battery.parameters.max_charge_power_w == 8000, (
|
|
|
|
|
"PV battery max charge power should remain as defined"
|
|
|
|
|
)
|
2024-12-19 14:50:19 +01:00
|
|
|
|
|
|
|
|
# Test discharge for car battery
|
|
|
|
|
car_discharged_wh, car_loss_wh = car_battery.discharge_energy(5000, 10)
|
|
|
|
|
assert car_discharged_wh > 0, "Car battery should discharge energy"
|
2025-02-24 10:00:09 +01:00
|
|
|
assert car_battery.current_soc_percentage() >= car_battery.parameters.min_soc_percentage, (
|
|
|
|
|
"Car battery SOC should stay above min SOC"
|
|
|
|
|
)
|
|
|
|
|
assert car_battery.parameters.max_charge_power_w == 7000, (
|
|
|
|
|
"Car battery max charge power should remain as defined"
|
|
|
|
|
)
|