fix(devices): let EVs finish charging within the final slot (#1423)

This commit is contained in:
Andreas
2026-10-20 12:05:44 +02:00
committed by GitHub
parent 08178b9f68
commit 46a89a6f39
2 changed files with 90 additions and 4 deletions
+12 -4
View File
@@ -418,9 +418,10 @@ class Battery:
- `wh is None` and `charge_factor > 0`
- The raw requested energy is `max_charge_power_w * charge_factor`.
- If the request exceeds remaining capacity, the algorithm tries to find a lower
`charge_factor` that is compatible. If such a charge factor exists, this hour’s
`charge_factor` is replaced.
- An electric vehicle may finish within a slot at the requested current;
its accepted energy is capped at the remaining capacity.
- For stationary batteries, requests above the remaining capacity try a lower
compatible `charge_factor`, replacing this slot's factor.
- If no charge factor can accommodate charging, the request is ignored (``(0.0, 0.0)`` is
returned) and a penalty is applied elsewhere.
@@ -485,7 +486,14 @@ class Battery:
elif wh is None and charge_factor > 0.0: # mode 2
raw_request_wh = max_charge_per_slot_wh_fast * charge_factor
raw_charge_wh = max(self.max_soc_wh - soc_wh_fast, 0.0) / charging_efficiency_fast
if raw_request_wh > raw_charge_wh:
if isinstance(self.parameters, ElectricVehicleParameters):
# The vehicle BMS ends charging when full, even within a slot.
# Keep the feasible pilot/current setting; cap only the energy
# below instead of inventing a current below the EV minimum.
if raw_charge_wh <= 0.0:
self.charge_array[hour] = 0
return 0.0, 0.0
elif raw_request_wh > raw_charge_wh:
# Use a lower charge factor: the rates below the requested
# one, highest first (as _lower_charge_rates_desc yields them).
requested_charge_factor = charge_factor
+78
View File
@@ -0,0 +1,78 @@
import unittest
import numpy as np
from akkudoktoreos.devices.genetic.battery import (
Battery,
ElectricVehicleParameters,
SolarPanelBatteryParameters,
)
RATES = [0.0] + [amps / 16 for amps in range(6, 17)]
def battery(kind=ElectricVehicleParameters, soc=99, maximum=100):
parameters = kind(
device_id="ev-test", capacity_wh=60000,
initial_soc_percentage=int(soc), charging_efficiency=0.95,
discharging_efficiency=1.0, max_charge_power_w=11040,
max_soc_percentage=maximum, charge_rates=RATES,
)
value = Battery(parameters, prediction_hours=4, slot_duration_h=0.25)
value.reset()
value.soc_wh = soc / 100 * value.capacity_wh
value.charge_array = np.full(4, 0.375)
return value
class TestEVMinimumCurrent(unittest.TestCase):
def test_ev_finishes_within_final_slot_at_unchanged_minimum_current(self):
for soc in [98.5, 99.0, 99.9]:
with self.subTest(soc=soc):
value = battery(soc=soc)
missing = 60000 - value.soc_wh
stored, losses = value.charge_energy(None, 0, charge_factor=0.375)
self.assertAlmostEqual(stored, missing)
self.assertAlmostEqual(value.soc_wh, 60000)
self.assertAlmostEqual(losses, missing / 0.95 - missing)
self.assertEqual(value.charge_array[0], 0.375)
self.assertLessEqual(stored + losses, 4140 * 0.25)
def test_full_ev_has_no_charge_command_or_energy(self):
value = battery(soc=100)
self.assertEqual(value.charge_energy(None, 0, charge_factor=0.375), (0.0, 0.0))
self.assertEqual(value.charge_array[0], 0)
def test_complete_minimum_current_slot_preserves_watt_hours(self):
value = battery(soc=80)
stored, losses = value.charge_energy(None, 0, charge_factor=0.375)
self.assertAlmostEqual(stored + losses, 4140 * 0.25)
self.assertAlmostEqual(stored, 4140 * 0.25 * 0.95)
def test_ev_respects_a_lower_configured_maximum(self):
value = battery(soc=95.9, maximum=96)
value.charge_energy(None, 0, charge_factor=0.375)
self.assertAlmostEqual(value.soc_wh, 60000 * 0.96)
def test_stationary_battery_keeps_discrete_rate_behavior(self):
value = battery(SolarPanelBatteryParameters, soc=99)
self.assertEqual(value.charge_energy(None, 0, charge_factor=0.375), (0.0, 0.0))
self.assertEqual(value.soc_wh, 59400)
self.assertEqual(value.charge_array[0], 0)
def test_final_slot_preserves_each_configured_current(self):
for amps in range(6, 17):
with self.subTest(amps=amps):
value = battery(soc=99.9)
factor = amps / 16
value.charge_array[0] = factor
missing = value.max_soc_wh - value.soc_wh
stored, losses = value.charge_energy(None, 0, charge_factor=factor)
self.assertAlmostEqual(stored, missing)
self.assertAlmostEqual(value.soc_wh, value.max_soc_wh)
self.assertAlmostEqual(losses, missing / 0.95 - missing)
self.assertEqual(value.charge_array[0], factor)
if __name__ == "__main__":
unittest.main()