Skip to content

Commit 52b11ad

Browse files
committed
Send the next command after a failed one, even if it is 0 W
A command that failed or timed out may still have been applied. When the last successful command was 0 W, a following 0 W command was skipped as a repeat, so the battery could keep running the failed command's power. Signed-off-by: Sahas Subramanian <sahas.subramanian@proton.me>
1 parent 61bb385 commit 52b11ad

3 files changed

Lines changed: 55 additions & 1 deletion

File tree

‎RELEASE_NOTES.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ This release updates the microgrid component graph library to v0.6.0, which stop
1414
## Bug Fixes
1515

1616
- The battery pool no longer drops the cached data of batteries that stop working. A battery that started working again was left out of the pool's metrics until its next data sample arrived. When every working battery had just started working again, `system_power_bounds` briefly reported no bounds, and if an actor had requested power, the power manager set the target power to zero.
17+
- After a power request to a battery's inverter fails or times out, the next request that includes that inverter is now always sent, even if it is 0 W. Before, a 0 W request was skipped when the last successful request to that inverter was also 0 W. The failed request may have been applied anyway, so the battery could keep running at that request's power. As before, a battery whose request failed is left out of new requests for a while: 1 second at first, and up to 30 seconds if its requests keep failing. This only happens while another battery in the new request is working. If none is, the failed battery is used again right away.

‎src/frequenz/sdk/microgrid/_power_distributing/_component_managers/_battery_manager.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,12 @@ def __init__(
184184
"""The distribution algorithm used to distribute power between batteries."""
185185

186186
self._last_set_powers: dict[ComponentId, Power] = {}
187-
"""The last power value set to each battery's inverter."""
187+
"""The last power value set to each battery's inverter.
188+
189+
An inverter is missing if it was never sent a request, or if its last
190+
request failed. A failed request may still have been applied, so the
191+
next request to that inverter is always sent.
192+
"""
188193

189194
@override
190195
def component_ids(self) -> collections.abc.Set[ComponentId]:
@@ -772,6 +777,9 @@ def _parse_result(
772777
if failed:
773778
failed_power += distribution[inverter_id]
774779
failed_batteries.update(battery_ids)
780+
# The command may still have been applied, so the next one must
781+
# be sent even if it is 0 W.
782+
self._last_set_powers.pop(inverter_id, None)
775783
else:
776784
self._last_set_powers[inverter_id] = distribution[inverter_id]
777785

‎tests/timeseries/_battery_pool/test_battery_pool_control_methods.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,3 +618,48 @@ async def test_no_resend_0w(self, mocks: Mocks, mocker: MockerFixture) -> None:
618618
assert sorted(set_power.call_args_list) == [
619619
mocker.call(inv_id, 25.0) for inv_id in mocks.microgrid.battery_inverter_ids
620620
]
621+
622+
async def test_resend_0w_after_failure(
623+
self, mocks: Mocks, mocker: MockerFixture
624+
) -> None:
625+
"""Test that 0W is sent again to an inverter whose last command failed.
626+
627+
A command that timed out may still have been applied. The battery status
628+
is mocked, so the failed battery is not blocked before the next request.
629+
"""
630+
set_power = cast(
631+
AsyncMock,
632+
microgrid.connection_manager.get().api_client.set_component_power_active,
633+
)
634+
await self._patch_battery_pool_status(mocks, mocker)
635+
await self._init_data_for_batteries(mocks)
636+
await self._init_data_for_inverters(mocks)
637+
638+
battery_pool = microgrid.new_battery_pool(priority=5)
639+
bounds_rx = battery_pool.power_status.new_receiver()
640+
self._assert_report(
641+
await bounds_rx.receive(), power=None, lower=-4000.0, upper=4000.0
642+
)
643+
644+
await battery_pool.propose_power(Power.from_watts(0.0))
645+
await asyncio.sleep(1.0) # Wait for the power to be distributed.
646+
assert set_power.call_count == 4
647+
648+
# The command to the first inverter times out.
649+
failing_inverter = mocks.microgrid.battery_inverter_ids[0]
650+
651+
async def side_effect(inv_id: int, _: float) -> None:
652+
if inv_id == failing_inverter:
653+
await asyncio.sleep(1000.0)
654+
655+
set_power.side_effect = side_effect
656+
await battery_pool.propose_power(Power.from_watts(100.0))
657+
await asyncio.sleep(20.0) # Wait for the requests to time out.
658+
659+
set_power.side_effect = None
660+
set_power.reset_mock()
661+
await battery_pool.propose_power(Power.from_watts(0.0))
662+
await asyncio.sleep(1.0)
663+
assert sorted(set_power.call_args_list) == [
664+
mocker.call(inv_id, 0.0) for inv_id in mocks.microgrid.battery_inverter_ids
665+
]

0 commit comments

Comments
 (0)