Skip to content

Commit f43a32a

Browse files
committed
Forward zero power requests always to the microgrid API
Only non-zero requests should be rejected if they fall within the exclusion bounds. Sending zero requests to the api informs the api that we are not interested in these batteries, and it is free to start its pulse heating algorithm, etc. immediately, if necessary. Signed-off-by: Sahas Subramanian <[email protected]>
1 parent 62b1f59 commit f43a32a

File tree

3 files changed

+107
-0
lines changed

3 files changed

+107
-0
lines changed

src/frequenz/sdk/actor/power_distributing/_distribution_algorithm/_distribution_algorithm.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,8 @@ def _distribute_power( # pylint: disable=too-many-arguments
401401

402402
for inverter_id, deficit in deficits.items():
403403
while not is_close_to_zero(deficit) and deficit < 0.0:
404+
if not excess_reserved:
405+
break
404406
take_from = max(excess_reserved.items(), key=lambda item: item[1])
405407
if is_close_to_zero(take_from[1]) or take_from[1] < 0.0:
406408
break

src/frequenz/sdk/actor/power_distributing/power_distributing.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import grpc
2727
from frequenz.channels import Peekable, Receiver, Sender
2828

29+
from ..._internal._math import is_close_to_zero
2930
from ...actor import ChannelRegistry
3031
from ...actor._decorator import actor
3132
from ...microgrid import ComponentGraph, connection_manager
@@ -329,6 +330,7 @@ async def run(self) -> None:
329330
try:
330331
distribution = self._get_power_distribution(request, pairs_data)
331332
except ValueError as err:
333+
_logger.exception("Couldn't distribute power")
332334
error_msg = f"Couldn't distribute power, error: {str(err)}"
333335
await self._send_result(
334336
request.namespace, Error(request=request, msg=str(error_msg))
@@ -475,6 +477,12 @@ def _check_request(
475477
return Error(request=request, msg=msg)
476478

477479
bounds = self._get_bounds(pairs_data)
480+
481+
# Zero power requests are always forwarded to the microgrid API, even if they
482+
# are outside the exclusion bounds.
483+
if is_close_to_zero(request.power):
484+
return None
485+
478486
if request.adjust_power:
479487
# Automatic power adjustments can only bring down the requested power down
480488
# to the inclusion bounds.

tests/actor/power_distributing/test_power_distributing.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,103 @@ async def test_power_distributor_one_user(self, mocker: MockerFixture) -> None:
155155
assert result.excess_power == approx(200.0)
156156
assert result.request == request
157157

158+
async def test_power_distributor_exclusion_bounds(
159+
self, mocker: MockerFixture
160+
) -> None:
161+
"""Test if power distributing actor rejects non-zero requests in exclusion bounds."""
162+
mockgrid = MockMicrogrid(grid_meter=False)
163+
mockgrid.add_batteries(2)
164+
await mockgrid.start(mocker)
165+
await self.init_component_data(mockgrid)
166+
167+
await mockgrid.mock_client.send(
168+
battery_msg(
169+
9,
170+
soc=Metric(60, Bound(20, 80)),
171+
capacity=Metric(98000),
172+
power=PowerBounds(-1000, -300, 300, 1000),
173+
)
174+
)
175+
await mockgrid.mock_client.send(
176+
battery_msg(
177+
19,
178+
soc=Metric(60, Bound(20, 80)),
179+
capacity=Metric(98000),
180+
power=PowerBounds(-1000, -300, 300, 1000),
181+
)
182+
)
183+
184+
channel = Broadcast[Request]("power_distributor")
185+
channel_registry = ChannelRegistry(name="power_distributor")
186+
187+
attrs = {
188+
"get_working_batteries.return_value": microgrid.battery_pool().battery_ids
189+
}
190+
mocker.patch(
191+
"frequenz.sdk.actor.power_distributing.power_distributing.BatteryPoolStatus",
192+
return_value=MagicMock(spec=BatteryPoolStatus, **attrs),
193+
)
194+
195+
mocker.patch("asyncio.sleep", new_callable=AsyncMock)
196+
battery_status_channel = Broadcast[BatteryStatus]("battery_status")
197+
distributor = PowerDistributingActor(
198+
requests_receiver=channel.new_receiver(),
199+
channel_registry=channel_registry,
200+
battery_status_sender=battery_status_channel.new_sender(),
201+
)
202+
203+
## zero power requests should pass through despite the exclusion bounds.
204+
request = Request(
205+
namespace=self._namespace,
206+
power=0.0,
207+
batteries={9, 19},
208+
request_timeout_sec=SAFETY_TIMEOUT,
209+
)
210+
211+
await channel.new_sender().send(request)
212+
result_rx = channel_registry.new_receiver(self._namespace)
213+
214+
done, pending = await asyncio.wait(
215+
[asyncio.create_task(result_rx.receive())],
216+
timeout=SAFETY_TIMEOUT,
217+
)
218+
219+
assert len(pending) == 0
220+
assert len(done) == 1
221+
222+
result: Result = done.pop().result()
223+
assert isinstance(result, Success)
224+
assert result.succeeded_power == approx(0.0)
225+
assert result.excess_power == approx(0.0)
226+
assert result.request == request
227+
228+
## non-zero power requests that fall within the exclusion bounds should be
229+
## rejected.
230+
request = Request(
231+
namespace=self._namespace,
232+
power=300.0,
233+
batteries={9, 19},
234+
request_timeout_sec=SAFETY_TIMEOUT,
235+
)
236+
237+
await channel.new_sender().send(request)
238+
result_rx = channel_registry.new_receiver(self._namespace)
239+
240+
done, pending = await asyncio.wait(
241+
[asyncio.create_task(result_rx.receive())],
242+
timeout=SAFETY_TIMEOUT,
243+
)
244+
245+
assert len(pending) == 0
246+
assert len(done) == 1
247+
248+
result = done.pop().result()
249+
assert isinstance(result, OutOfBounds)
250+
assert result.bounds == PowerBounds(-1000, -600, 600, 1000)
251+
assert result.request == request
252+
253+
await distributor._stop_actor()
254+
158255
async def test_battery_soc_nan(self, mocker: MockerFixture) -> None:
159256
"""Test if battery with SoC==NaN is not used."""
160257
mockgrid = MockMicrogrid(grid_meter=False)

0 commit comments

Comments
 (0)