mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-11-09 15:24:01 +01:00
fix(battery): preserve energy below minimum state of charge (#1483)
This commit is contained in:
@@ -366,6 +366,9 @@ class Battery:
|
||||
|
||||
# Raw extractable energy above minimum SoC
|
||||
raw_available_wh = max(self.soc_wh - self.min_soc_wh, 0.0)
|
||||
if raw_available_wh == 0.0:
|
||||
# A measured SoC below the reserve must not be raised by discharging.
|
||||
return 0.0, 0.0
|
||||
|
||||
# Maximum raw discharge due to power limit, scaled to the slot duration.
|
||||
# max_charge_power_w is a power [W]; energy movable in one slot is
|
||||
|
||||
@@ -304,6 +304,9 @@ class Inverter:
|
||||
discharged_raw_wh = battery._discharged_raw_wh_per_slot
|
||||
discharging_efficiency = battery.discharging_efficiency
|
||||
raw_available_wh = max(soc_wh - min_soc_wh, 0.0)
|
||||
if raw_available_wh == 0.0:
|
||||
# Match Battery.discharge_energy(): preserve a SoC below the reserve.
|
||||
return 0.0, 0.0
|
||||
max_raw_wh = max(
|
||||
battery.max_charge_power_w * battery.slot_duration_h - discharged_raw_wh[hour], 0.0
|
||||
)
|
||||
|
||||
@@ -227,6 +227,9 @@ class Genetic0Battery:
|
||||
|
||||
# Raw extractable energy above minimum SoC
|
||||
raw_available_wh = max(self.soc_wh - self.min_soc_wh, 0.0)
|
||||
if raw_available_wh == 0.0:
|
||||
# A measured SoC below the reserve must not be raised by discharging.
|
||||
return 0.0, 0.0
|
||||
|
||||
# Maximum raw discharge due to power limit
|
||||
max_raw_wh = self.max_charge_power_w # TODO rename to max_discharge_power_w
|
||||
|
||||
@@ -119,6 +119,20 @@ def test_battery_discharge_exactly_min_soc(setup_pv_battery):
|
||||
assert battery.current_soc_percentage() == 20 # SOC should remain at min_SOC
|
||||
|
||||
|
||||
@pytest.mark.parametrize("initial_soc", [0, 5, 20])
|
||||
@pytest.mark.parametrize("requested_wh", [0.0, 280.0])
|
||||
def test_discharge_preserves_energy_at_or_below_min_soc(
|
||||
setup_pv_battery, initial_soc, requested_wh
|
||||
):
|
||||
battery = setup_pv_battery
|
||||
battery.soc_wh = initial_soc / 100 * battery.capacity_wh
|
||||
initial_wh = battery.soc_wh
|
||||
|
||||
for slot in (0, 0, 1):
|
||||
assert battery.discharge_energy(requested_wh, slot) == (0.0, 0.0)
|
||||
assert battery.soc_wh == initial_wh
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -114,6 +114,31 @@ def test_battery_discharge_when_empty(setup_pv_battery):
|
||||
assert battery.current_soc_percentage() == 20, "SoC should remain at min_soc"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("slot_duration_h", [0.25, 1.0])
|
||||
@pytest.mark.parametrize("initial_soc", [0, 5, 6])
|
||||
@pytest.mark.parametrize("requested_wh", [0.0, 280.0])
|
||||
def test_discharge_preserves_energy_at_or_below_min_soc(
|
||||
slot_duration_h, initial_soc, requested_wh
|
||||
):
|
||||
battery = Battery(
|
||||
SolarPanelBatteryParameters(
|
||||
device_id="battery1",
|
||||
capacity_wh=26_400,
|
||||
initial_soc_percentage=initial_soc,
|
||||
min_soc_percentage=6,
|
||||
),
|
||||
prediction_hours=2,
|
||||
slot_duration_h=slot_duration_h,
|
||||
)
|
||||
battery.set_discharge_per_hour(np.ones(2))
|
||||
initial_wh = battery.soc_wh
|
||||
|
||||
for slot in (0, 0, 1):
|
||||
assert battery.discharge_energy(requested_wh, slot) == (0.0, 0.0)
|
||||
assert battery.soc_wh == initial_wh
|
||||
assert battery.discharged_energy_wh(slot) == 0.0
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -25,6 +25,65 @@ from akkudoktoreos.utils.datetimeutil import to_duration, to_time
|
||||
start_hour = 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("efficiency_curve", [None, [(0.0, 0.8), (1.0, 0.97)]])
|
||||
def test_below_min_soc_requires_real_charging_energy(efficiency_curve):
|
||||
"""Discharge attempts before and after partial charging must not create energy."""
|
||||
battery = Battery(
|
||||
SolarPanelBatteryParameters(
|
||||
device_id="battery1",
|
||||
capacity_wh=26_400,
|
||||
initial_soc_percentage=0,
|
||||
min_soc_percentage=6,
|
||||
max_charge_power_w=7_000,
|
||||
charging_efficiency=0.95,
|
||||
discharging_efficiency=0.95,
|
||||
),
|
||||
prediction_hours=4,
|
||||
slot_duration_h=0.25,
|
||||
)
|
||||
inverter = Inverter(
|
||||
InverterParameters(
|
||||
device_id="inverter1",
|
||||
battery_id="battery1",
|
||||
max_power_wh=10_000,
|
||||
max_ac_charge_power_w=6_000,
|
||||
ac_to_dc_efficiency=0.97,
|
||||
dc_to_ac_efficiency=0.97,
|
||||
dc_to_ac_efficiency_curve=efficiency_curve,
|
||||
),
|
||||
battery=battery,
|
||||
slot_duration_h=0.25,
|
||||
)
|
||||
simulation = GeneticSimulation()
|
||||
simulation.prepare(
|
||||
GeneticEnergyManagementParameters(
|
||||
pv_forecast_wh=[0.0] * 4,
|
||||
total_load=[280.0] * 4,
|
||||
electricity_price_per_wh=[0.00037, 0.00037, 0.00050, 0.00063],
|
||||
feed_in_tariff_per_wh=0.0,
|
||||
price_per_wh_battery=0.0,
|
||||
),
|
||||
optimization_hours=4,
|
||||
prediction_hours=4,
|
||||
inverter=inverter,
|
||||
)
|
||||
assert simulation.ac_charge_hours is not None
|
||||
assert simulation.bat_discharge_hours is not None
|
||||
simulation.ac_charge_hours[:] = [0.0, 1.0, 0.0, 0.0]
|
||||
simulation.bat_discharge_hours[:] = [1.0, 0.0, 1.0, 1.0]
|
||||
|
||||
result = simulation.simulate(0)
|
||||
|
||||
stored_wh = 1_500 * 0.97 * 0.95
|
||||
assert result["akku_soc_pro_stunde"] == pytest.approx(
|
||||
[0.0, 0.0, stored_wh / 26_400 * 100, stored_wh / 26_400 * 100]
|
||||
)
|
||||
assert result["Netzbezug_Wh_pro_Stunde"] == pytest.approx([280, 1780, 280, 280])
|
||||
assert battery.soc_wh == pytest.approx(stored_wh)
|
||||
assert battery.soc_wh < battery.min_soc_wh
|
||||
assert result["Gesamt_Verluste"] == pytest.approx(1_500 - stored_wh)
|
||||
|
||||
|
||||
# Example initialization of necessary components
|
||||
@pytest.fixture
|
||||
def genetic_simulation(config_eos) -> GeneticSimulation:
|
||||
|
||||
Reference in New Issue
Block a user