Skip to content

Commit 4306d72

Browse files
committed
Expose bounds from PowerManager through the BatteryPool
Also update the battery pool control method tests to use the new bounds streams. Signed-off-by: Sahas Subramanian <[email protected]>
1 parent db814e2 commit 4306d72

File tree

4 files changed

+100
-19
lines changed

4 files changed

+100
-19
lines changed

src/frequenz/sdk/actor/_power_managing/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33

44
"""A power manager implementation."""
55

6-
from ._base_classes import Algorithm, Proposal, Report, ReportRequest
6+
from ._base_classes import Algorithm, Bounds, Proposal, Report, ReportRequest
77
from ._power_managing_actor import PowerManagingActor
88

99
__all__ = [
1010
"Algorithm",
11+
"Bounds",
1112
"PowerManagingActor",
1213
"Proposal",
1314
"Report",

src/frequenz/sdk/microgrid/_data_pipeline.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ def battery_pool(
218218
power_manager_requests_sender=(
219219
self._power_management_proposals_channel.new_sender()
220220
),
221+
power_manager_bounds_subscription_sender=(
222+
self._power_manager_bounds_subscription_channel.new_sender()
223+
),
221224
power_manager_results_receiver=(
222225
self._power_distribution_results_channel.new_receiver()
223226
),

src/frequenz/sdk/timeseries/battery_pool/battery_pool.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414

1515
from ..._internal._asyncio import cancel_and_await
1616
from ..._internal._channels import ReceiverFetcher
17-
from ...actor import ChannelRegistry, ComponentMetricRequest
18-
from ...actor._power_managing import Proposal
17+
from ...actor import ComponentMetricRequest, _channel_registry, _power_managing
1918
from ...actor.power_distributing._battery_pool_status import BatteryStatus
2019
from ...actor.power_distributing.result import Result
2120
from ...microgrid import connection_manager
@@ -38,7 +37,7 @@
3837
from ._result_types import PowerMetrics
3938

4039

41-
class BatteryPool:
40+
class BatteryPool: # pylint: disable=too-many-instance-attributes
4241
"""Calculate high level metrics for a pool of the batteries.
4342
4443
BatterPool accepts subset of the battery ids and provides methods methods for
@@ -47,10 +46,11 @@ class BatteryPool:
4746

4847
def __init__( # pylint: disable=too-many-arguments
4948
self,
50-
channel_registry: ChannelRegistry,
49+
channel_registry: _channel_registry.ChannelRegistry,
5150
resampler_subscription_sender: Sender[ComponentMetricRequest],
5251
batteries_status_receiver: Receiver[BatteryStatus],
53-
power_manager_requests_sender: Sender[Proposal],
52+
power_manager_requests_sender: Sender[_power_managing.Proposal],
53+
power_manager_bounds_subscription_sender: Sender[_power_managing.ReportRequest],
5454
power_manager_results_receiver: Receiver[Result],
5555
min_update_interval: timedelta,
5656
batteries_id: Set[int] | None = None,
@@ -70,6 +70,8 @@ def __init__( # pylint: disable=too-many-arguments
7070
battery.
7171
power_manager_requests_sender: A Channel sender for sending power
7272
requests to the power managing actor.
73+
power_manager_bounds_subscription_sender: A Channel sender for sending
74+
power bounds requests to the power managing actor.
7375
power_manager_results_receiver: A Channel receiver for receiving results
7476
from the power managing actor.
7577
min_update_interval: Some metrics in BatteryPool are send only when they
@@ -101,14 +103,19 @@ def __init__( # pylint: disable=too-many-arguments
101103
self._min_update_interval = min_update_interval
102104

103105
self._power_manager_requests_sender = power_manager_requests_sender
106+
self._power_manager_bounds_subscription_sender = (
107+
power_manager_bounds_subscription_sender
108+
)
104109
self._power_manager_results_receiver = power_manager_results_receiver
105110

106111
self._active_methods: dict[str, MetricAggregator[Any]] = {}
112+
self._power_bounds_subs: dict[str, asyncio.Task[None]] = {}
107113
self._namespace: str = f"battery-pool-{self._batteries}-{uuid.uuid4()}"
108114
self._power_distributing_namespace: str = f"power-distributor-{self._namespace}"
115+
self._channel_registry = channel_registry
109116
self._formula_pool: FormulaEnginePool = FormulaEnginePool(
110117
self._namespace,
111-
channel_registry,
118+
self._channel_registry,
112119
resampler_subscription_sender,
113120
)
114121

@@ -148,7 +155,7 @@ async def set_power(
148155
_priority: The priority of the actor making the request.
149156
"""
150157
await self._power_manager_requests_sender.send(
151-
Proposal(
158+
_power_managing.Proposal(
152159
source_id=self._namespace,
153160
preferred_power=preferred_power,
154161
bounds=_bounds,
@@ -191,7 +198,7 @@ async def charge(
191198
if power < Power.zero():
192199
raise ValueError("Charge power must be positive.")
193200
await self._power_manager_requests_sender.send(
194-
Proposal(
201+
_power_managing.Proposal(
195202
source_id=self._namespace,
196203
preferred_power=power,
197204
bounds=None,
@@ -234,7 +241,7 @@ async def discharge(
234241
if power < Power.zero():
235242
raise ValueError("Discharge power must be positive.")
236243
await self._power_manager_requests_sender.send(
237-
Proposal(
244+
_power_managing.Proposal(
238245
source_id=self._namespace,
239246
preferred_power=power,
240247
bounds=None,
@@ -447,6 +454,28 @@ def capacity(self) -> ReceiverFetcher[Sample[Energy]]:
447454

448455
return self._active_methods[method_name]
449456

457+
def power_bounds(
458+
self, priority: int = 0
459+
) -> ReceiverFetcher[_power_managing.Report]:
460+
"""Get a receiver to receive new power bounds when they change.
461+
462+
These bounds are the bounds specified the power manager for actors with the
463+
given priority.
464+
465+
Args:
466+
priority: The priority of the actor to get the power bounds for.
467+
468+
Returns:
469+
A receiver that will receive the power bounds for the given priority.
470+
"""
471+
sub = _power_managing.ReportRequest(
472+
source_id=self._namespace, priority=priority, battery_ids=self._batteries
473+
)
474+
self._power_bounds_subs[sub.get_channel_name()] = asyncio.create_task(
475+
self._power_manager_bounds_subscription_sender.send(sub)
476+
)
477+
return self._channel_registry.new_receiver_fetcher(sub.get_channel_name())
478+
450479
@property
451480
def _system_power_bounds(self) -> ReceiverFetcher[PowerMetrics]:
452481
"""Get receiver to receive new power bounds when they change.

tests/timeseries/_battery_pool/test_battery_pool_control_methods.py

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from pytest_mock import MockerFixture
1515

1616
from frequenz.sdk import microgrid
17-
from frequenz.sdk.actor import ResamplerConfig
17+
from frequenz.sdk.actor import ResamplerConfig, _power_managing
1818
from frequenz.sdk.actor.power_distributing import BatteryStatus
1919
from frequenz.sdk.actor.power_distributing._battery_pool_status import BatteryPoolStatus
2020
from frequenz.sdk.timeseries import Power
@@ -136,6 +136,17 @@ async def _init_data_for_inverters(self, mocks: Mocks) -> None:
136136
0.05,
137137
)
138138

139+
def _make_report(
140+
self, *, power: float, lower: float, upper: float
141+
) -> _power_managing.Report:
142+
return _power_managing.Report(
143+
target_power=Power.from_watts(power),
144+
available_bounds=_power_managing.Bounds(
145+
lower=Power.from_watts(lower),
146+
upper=Power.from_watts(upper),
147+
),
148+
)
149+
139150
async def test_case_1(
140151
self,
141152
mocks: Mocks,
@@ -161,10 +172,17 @@ async def test_case_1(
161172
#
162173
# It will be replaced by a reporting streaming from the PowerManager in a
163174
# subsequent commit.
164-
results_rx = battery_pool.power_distribution_results()
175+
bounds_rx = battery_pool.power_bounds().new_receiver()
176+
177+
assert await bounds_rx.receive() == self._make_report(
178+
power=0.0, lower=-4000.0, upper=4000.0
179+
)
165180

166181
await battery_pool.set_power(Power.from_watts(1000.0))
167-
await results_rx.receive()
182+
183+
assert await bounds_rx.receive() == self._make_report(
184+
power=1000.0, lower=-4000.0, upper=4000.0
185+
)
168186

169187
assert set_power.call_count == 4
170188
assert set_power.call_args_list == [
@@ -187,11 +205,20 @@ async def test_case_2(self, mocks: Mocks, mocker: MockerFixture) -> None:
187205
await self._init_data_for_inverters(mocks)
188206

189207
battery_pool_1 = microgrid.battery_pool(set(mocks.microgrid.battery_ids[:2]))
208+
bounds_1_rx = battery_pool_1.power_bounds().new_receiver()
190209
battery_pool_2 = microgrid.battery_pool(set(mocks.microgrid.battery_ids[2:]))
210+
bounds_2_rx = battery_pool_2.power_bounds().new_receiver()
191211

192-
results_rx = battery_pool_1.power_distribution_results()
212+
assert await bounds_1_rx.receive() == self._make_report(
213+
power=0.0, lower=-2000.0, upper=2000.0
214+
)
215+
assert await bounds_2_rx.receive() == self._make_report(
216+
power=0.0, lower=-2000.0, upper=2000.0
217+
)
193218
await battery_pool_1.set_power(Power.from_watts(1000.0))
194-
await results_rx.receive()
219+
assert await bounds_1_rx.receive() == self._make_report(
220+
power=1000.0, lower=-2000.0, upper=2000.0
221+
)
195222
assert set_power.call_count == 2
196223
assert set_power.call_args_list == [
197224
mocker.call(inv_id, 500.0)
@@ -200,7 +227,9 @@ async def test_case_2(self, mocks: Mocks, mocker: MockerFixture) -> None:
200227
set_power.reset_mock()
201228

202229
await battery_pool_2.set_power(Power.from_watts(1000.0))
203-
await results_rx.receive()
230+
assert await bounds_2_rx.receive() == self._make_report(
231+
power=1000.0, lower=-2000.0, upper=2000.0
232+
)
204233
assert set_power.call_count == 2
205234
assert set_power.call_args_list == [
206235
mocker.call(inv_id, 500.0)
@@ -222,15 +251,28 @@ async def test_case_3(self, mocks: Mocks, mocker: MockerFixture) -> None:
222251
await self._init_data_for_inverters(mocks)
223252

224253
battery_pool_1 = microgrid.battery_pool()
254+
bounds_1_rx = battery_pool_1.power_bounds(2).new_receiver()
225255
battery_pool_2 = microgrid.battery_pool()
256+
bounds_2_rx = battery_pool_2.power_bounds(1).new_receiver()
226257

227-
results_rx = battery_pool_1.power_distribution_results()
258+
assert await bounds_1_rx.receive() == self._make_report(
259+
power=0.0, lower=-4000.0, upper=4000.0
260+
)
261+
assert await bounds_2_rx.receive() == self._make_report(
262+
power=0.0, lower=-4000.0, upper=4000.0
263+
)
228264
await battery_pool_1.set_power(
229265
Power.from_watts(-1000.0),
230266
_priority=2,
231267
_bounds=(Power.from_watts(-1000.0), Power.from_watts(0.0)),
232268
)
233-
await results_rx.receive()
269+
assert await bounds_1_rx.receive() == self._make_report(
270+
power=-1000.0, lower=-4000.0, upper=4000.0
271+
)
272+
assert await bounds_2_rx.receive() == self._make_report(
273+
power=-1000.0, lower=-1000.0, upper=0.0
274+
)
275+
234276
assert set_power.call_count == 4
235277
assert set_power.call_args_list == [
236278
mocker.call(inv_id, -250.0)
@@ -243,7 +285,13 @@ async def test_case_3(self, mocks: Mocks, mocker: MockerFixture) -> None:
243285
_priority=1,
244286
_bounds=(Power.from_watts(0.0), Power.from_watts(1000.0)),
245287
)
246-
await results_rx.receive()
288+
assert await bounds_1_rx.receive() == self._make_report(
289+
power=0.0, lower=-4000.0, upper=4000.0
290+
)
291+
assert await bounds_2_rx.receive() == self._make_report(
292+
power=0.0, lower=-1000.0, upper=0.0
293+
)
294+
247295
assert set_power.call_count == 4
248296
assert set_power.call_args_list == [
249297
mocker.call(inv_id, 0.0) for inv_id in mocks.microgrid.battery_inverter_ids

0 commit comments

Comments
 (0)