Skip to content

Commit 9ea714b

Browse files
authored
Don't repeat zero commands to inverters (#1299)
Closes #1259
2 parents a8eb0a5 + 41636af commit 9ea714b

File tree

4 files changed

+82
-20
lines changed

4 files changed

+82
-20
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@
1414

1515
## Bug Fixes
1616

17+
- Doesn't repeat zero commands to battery inverters anymore, to not interfere with lower level logic that might want to do things only when there are no actors trying to use the batteries.
18+
1719
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->

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

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,9 @@ def __init__(
183183
)
184184
"""The distribution algorithm used to distribute power between batteries."""
185185

186+
self._last_set_powers: dict[ComponentId, Power] = {}
187+
"""The last power value set to each battery's inverter."""
188+
186189
@override
187190
def component_ids(self) -> collections.abc.Set[ComponentId]:
188191
"""Return the set of component ids."""
@@ -266,29 +269,13 @@ async def _distribute_power(
266269
Result from the microgrid API.
267270
"""
268271
distributed_power_value = request.power - distribution.remaining_power
269-
battery_distribution: dict[frozenset[ComponentId], Power] = {}
270272
battery_ids: set[ComponentId] = set()
271-
for inverter_id, dist in distribution.distribution.items():
273+
for inverter_id in distribution.distribution:
272274
for battery_id in self._inv_bats_map[inverter_id]:
273275
battery_ids.add(battery_id)
274-
battery_distribution[self._inv_bats_map[inverter_id]] = dist
275-
if _logger.isEnabledFor(logging.DEBUG):
276-
_logger.debug(
277-
"Distributing power %s between the batteries: %s",
278-
distributed_power_value,
279-
", ".join(
280-
(
281-
str(next(iter(cids)))
282-
if len(cids) == 1
283-
else f"({', '.join(str(cid) for cid in cids)})"
284-
)
285-
+ f": {power}"
286-
for cids, power in battery_distribution.items()
287-
),
288-
)
289276

290277
failed_power, failed_batteries = await self._set_distributed_power(
291-
distribution, self._api_power_request_timeout
278+
request, distribution, self._api_power_request_timeout
292279
)
293280

294281
response: Success | PartialFailure
@@ -632,12 +619,14 @@ def _get_power_distribution(
632619

633620
async def _set_distributed_power(
634621
self,
622+
request: Request,
635623
distribution: DistributionResult,
636624
timeout: timedelta,
637625
) -> tuple[Power, set[ComponentId]]:
638626
"""Send distributed power to the inverters.
639627
640628
Args:
629+
request: Request to set the power for.
641630
distribution: Distribution result
642631
timeout: How long wait for the response
643632
@@ -652,8 +641,34 @@ async def _set_distributed_power(
652641
api.set_power(inverter_id, power.as_watts())
653642
)
654643
for inverter_id, power in distribution.distribution.items()
644+
if power != Power.zero()
645+
or self._last_set_powers.get(inverter_id) != Power.zero()
655646
}
656647

648+
if not tasks:
649+
if _logger.isEnabledFor(logging.DEBUG):
650+
_logger.debug("Not repeating 0W commands to batteries.")
651+
return Power.zero(), set()
652+
653+
if _logger.isEnabledFor(logging.DEBUG):
654+
battery_distribution = {
655+
self._inv_bats_map[inverter_id]: distribution.distribution[inverter_id]
656+
for inverter_id in tasks
657+
}
658+
_logger.debug(
659+
"Distributing power %s between the batteries: %s",
660+
request.power - distribution.remaining_power,
661+
", ".join(
662+
(
663+
str(next(iter(cids)))
664+
if len(cids) == 1
665+
else f"({', '.join(str(cid) for cid in cids)})"
666+
)
667+
+ f": {power}"
668+
for cids, power in battery_distribution.items()
669+
),
670+
)
671+
657672
_, pending = await asyncio.wait(
658673
tasks.values(),
659674
timeout=timeout.total_seconds(),
@@ -722,6 +737,8 @@ def _parse_result(
722737
if failed:
723738
failed_power += distribution[inverter_id]
724739
failed_batteries.update(battery_ids)
740+
else:
741+
self._last_set_powers[inverter_id] = distribution[inverter_id]
725742

726743
return failed_power, failed_batteries
727744

src/frequenz/sdk/microgrid/_power_distributing/_distribution_algorithm/_battery_distribution_algorithm.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,8 +370,6 @@ def __init__(self, distributor_exponent: float = 1) -> None:
370370
ValueError: If distributor_exponent < 0
371371
372372
"""
373-
super().__init__()
374-
375373
if distributor_exponent < 0:
376374
raise ValueError("Distribution factor should be float >= 0.")
377375
self._distributor_exponent: float = distributor_exponent

tests/timeseries/_battery_pool/test_battery_pool_control_methods.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,3 +551,48 @@ async def test_case_4(self, mocks: Mocks, mocker: MockerFixture) -> None:
551551
result, _power_distributing.Success
552552
),
553553
)
554+
555+
async def test_no_resend_0w(self, mocks: Mocks, mocker: MockerFixture) -> None:
556+
"""Test that 0W command is not resent unnecessarily."""
557+
set_power = typing.cast(
558+
AsyncMock, microgrid.connection_manager.get().api_client.set_power
559+
)
560+
await self._patch_battery_pool_status(mocks, mocker)
561+
await self._init_data_for_batteries(mocks)
562+
await self._init_data_for_inverters(mocks)
563+
564+
battery_pool = microgrid.new_battery_pool(priority=5)
565+
bounds_rx = battery_pool.power_status.new_receiver()
566+
567+
self._assert_report(
568+
await bounds_rx.receive(), power=None, lower=-4000.0, upper=4000.0
569+
)
570+
571+
# First send 0W command. This should result in API calls.
572+
await battery_pool.propose_power(Power.from_watts(0.0))
573+
574+
self._assert_report(
575+
await bounds_rx.receive(), power=0.0, lower=-4000.0, upper=4000.0
576+
)
577+
await asyncio.sleep(1.0) # Wait for the power to be distributed.
578+
assert set_power.call_count == 4
579+
assert sorted(set_power.call_args_list) == [
580+
mocker.call(inv_id, 0.0) for inv_id in mocks.microgrid.battery_inverter_ids
581+
]
582+
583+
set_power.reset_mock()
584+
585+
# Sending 0W again should not result in any API calls.
586+
await battery_pool.propose_power(Power.from_watts(0.0))
587+
await asyncio.sleep(1.0) # Wait for the power to be distributed.
588+
assert set_power.call_count == 0
589+
590+
set_power.reset_mock()
591+
592+
# Sending a different power should result in API calls.
593+
await battery_pool.propose_power(Power.from_watts(100.0))
594+
await asyncio.sleep(1.0)
595+
assert set_power.call_count == 4
596+
assert sorted(set_power.call_args_list) == [
597+
mocker.call(inv_id, 25.0) for inv_id in mocks.microgrid.battery_inverter_ids
598+
]

0 commit comments

Comments
 (0)