Skip to content

Commit 1fe5cb8

Browse files
Update high-level public interfaces to concrete types (#607)
Fixes #188
2 parents d033941 + 34b030c commit 1fe5cb8

File tree

8 files changed

+127
-113
lines changed

8 files changed

+127
-113
lines changed

RELEASE_NOTES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ This release replaces the `@actor` decorator with a new `Actor` class.
8282

8383
- The base actors (`ConfigManagingActor`, `ComponentMetricsResamplingActor`, `DataSourcingActor`, `PowerDistributingActor`) now inherit from the new `Actor` class, if you are using them directly, you need to start them manually with `await actor.start()` and you might need to do some other adjustments.
8484

85+
- The `BatteryPool.power_distribution_results` method has been enhanced to provide power distribution results in the form of `Power` objects, replacing the previous use of `float` values.
86+
87+
- In the `Request` class:
88+
* The attribute `request_timeout_sec` has been updated and is now named `request_timeout` and it is represented by a `timedelta` object rather than a `float`.
89+
* The attribute `power` is now presented as a `Power` object, as opposed to a `float`.
90+
91+
- Within the `EVChargerPool.set_bounds` method, the parameter `max_amps` has been redefined as `max_current`, and it is now represented using a `Current` object instead of a `float`.
92+
8593
## New Features
8694

8795
- Added `DFS` to the component graph

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

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

29+
from frequenz.sdk.timeseries._quantities import Power
30+
2931
from ..._internal._math import is_close_to_zero
3032
from ...actor import ChannelRegistry
3133
from ...actor._actor import Actor
@@ -96,7 +98,7 @@ class PowerDistributingActor(Actor):
9698
9799
It is recommended to wait for PowerDistributingActor output with timeout. Otherwise if
98100
the processing function fails then the response will never come.
99-
The timeout should be Result:request_timeout_sec + time for processing the request.
101+
The timeout should be Result:request_timeout + time for processing the request.
100102
101103
Edge cases:
102104
* If there are 2 requests to be processed for the same subset of batteries, then
@@ -271,7 +273,9 @@ async def _run(self) -> None:
271273
)
272274
continue
273275

274-
distributed_power_value = request.power - distribution.remaining_power
276+
distributed_power_value = (
277+
request.power.as_watts() - distribution.remaining_power
278+
)
275279
battery_distribution = {
276280
self._inv_bat_map[bat_id]: dist
277281
for bat_id, dist in distribution.distribution.items()
@@ -283,27 +287,27 @@ async def _run(self) -> None:
283287
)
284288

285289
failed_power, failed_batteries = await self._set_distributed_power(
286-
api, distribution, request.request_timeout_sec
290+
api, distribution, request.request_timeout
287291
)
288292

289293
response: Success | PartialFailure
290294
if len(failed_batteries) > 0:
291295
succeed_batteries = set(battery_distribution.keys()) - failed_batteries
292296
response = PartialFailure(
293297
request=request,
294-
succeeded_power=distributed_power_value,
298+
succeeded_power=Power.from_watts(distributed_power_value),
295299
succeeded_batteries=succeed_batteries,
296-
failed_power=failed_power,
300+
failed_power=Power.from_watts(failed_power),
297301
failed_batteries=failed_batteries,
298-
excess_power=distribution.remaining_power,
302+
excess_power=Power.from_watts(distribution.remaining_power),
299303
)
300304
else:
301305
succeed_batteries = set(battery_distribution.keys())
302306
response = Success(
303307
request=request,
304-
succeeded_power=distributed_power_value,
308+
succeeded_power=Power.from_watts(distributed_power_value),
305309
succeeded_batteries=succeed_batteries,
306-
excess_power=distribution.remaining_power,
310+
excess_power=Power.from_watts(distribution.remaining_power),
307311
)
308312

309313
asyncio.gather(
@@ -319,14 +323,14 @@ async def _set_distributed_power(
319323
self,
320324
api: MicrogridApiClient,
321325
distribution: DistributionResult,
322-
timeout_sec: float,
326+
timeout: timedelta,
323327
) -> Tuple[float, Set[int]]:
324328
"""Send distributed power to the inverters.
325329
326330
Args:
327331
api: Microgrid api client
328332
distribution: Distribution result
329-
timeout_sec: How long wait for the response
333+
timeout: How long wait for the response
330334
331335
Returns:
332336
Tuple where first element is total failed power, and the second element
@@ -339,13 +343,13 @@ async def _set_distributed_power(
339343

340344
_, pending = await asyncio.wait(
341345
tasks.values(),
342-
timeout=timeout_sec,
346+
timeout=timeout.total_seconds(),
343347
return_when=ALL_COMPLETED,
344348
)
345349

346350
await self._cancel_tasks(pending)
347351

348-
return self._parse_result(tasks, distribution.distribution, timeout_sec)
352+
return self._parse_result(tasks, distribution.distribution, timeout)
349353

350354
def _get_power_distribution(
351355
self, request: Request, inv_bat_pairs: List[InvBatPair]
@@ -367,11 +371,11 @@ def _get_power_distribution(
367371

368372
if request.include_broken_batteries and not available_bat_ids:
369373
return self.distribution_algorithm.distribute_power_equally(
370-
request.power, unavailable_inv_ids
374+
request.power.as_watts(), unavailable_inv_ids
371375
)
372376

373377
result = self.distribution_algorithm.distribute_power(
374-
request.power, inv_bat_pairs
378+
request.power.as_watts(), inv_bat_pairs
375379
)
376380

377381
if request.include_broken_batteries and unavailable_inv_ids:
@@ -412,9 +416,11 @@ def _check_request(
412416

413417
bounds = self._get_bounds(pairs_data)
414418

419+
power = request.power.as_watts()
420+
415421
# Zero power requests are always forwarded to the microgrid API, even if they
416422
# are outside the exclusion bounds.
417-
if is_close_to_zero(request.power):
423+
if is_close_to_zero(power):
418424
return None
419425

420426
if request.adjust_power:
@@ -423,15 +429,11 @@ def _check_request(
423429
#
424430
# If the requested power is in the exclusion bounds, it is NOT possible to
425431
# increase it so that it is outside the exclusion bounds.
426-
if bounds.exclusion_lower < request.power < bounds.exclusion_upper:
432+
if bounds.exclusion_lower < power < bounds.exclusion_upper:
427433
return OutOfBounds(request=request, bounds=bounds)
428434
else:
429-
in_lower_range = (
430-
bounds.inclusion_lower <= request.power <= bounds.exclusion_lower
431-
)
432-
in_upper_range = (
433-
bounds.exclusion_upper <= request.power <= bounds.inclusion_upper
434-
)
435+
in_lower_range = bounds.inclusion_lower <= power <= bounds.exclusion_lower
436+
in_upper_range = bounds.exclusion_upper <= power <= bounds.inclusion_upper
435437
if not (in_lower_range or in_upper_range):
436438
return OutOfBounds(request=request, bounds=bounds)
437439

@@ -630,7 +632,7 @@ def _parse_result(
630632
self,
631633
tasks: Dict[int, asyncio.Task[None]],
632634
distribution: Dict[int, float],
633-
request_timeout_sec: float,
635+
request_timeout: timedelta,
634636
) -> Tuple[float, Set[int]]:
635637
"""Parse the results of `set_power` requests.
636638
@@ -642,7 +644,7 @@ def _parse_result(
642644
set the power for this inverter. Each task should be finished or cancelled.
643645
distribution: A dictionary where the key is the inverter ID and the value is how much
644646
power was set to the corresponding inverter.
645-
request_timeout_sec: The timeout that was used for the request.
647+
request_timeout: The timeout that was used for the request.
646648
647649
Returns:
648650
A tuple where the first element is the total failed power, and the second element is
@@ -676,7 +678,7 @@ def _parse_result(
676678
_logger.warning(
677679
"Battery %d didn't respond in %f sec. Mark it as broken.",
678680
battery_id,
679-
request_timeout_sec,
681+
request_timeout.total_seconds(),
680682
)
681683

682684
return failed_power, failed_batteries

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
import dataclasses
88
from collections import abc
9+
from datetime import timedelta
10+
11+
from ...timeseries._quantities import Power
912

1013

1114
@dataclasses.dataclass
@@ -19,13 +22,13 @@ class Request:
1922
channel registry.
2023
"""
2124

22-
power: float
23-
"""The requested power in watts."""
25+
power: Power
26+
"""The requested power."""
2427

2528
batteries: abc.Set[int]
2629
"""The component ids of the batteries to be used for this request."""
2730

28-
request_timeout_sec: float = 5.0
31+
request_timeout: timedelta = timedelta(seconds=5.0)
2932
"""The maximum amount of time to wait for the request to be fulfilled."""
3033

3134
adjust_power: bool = True

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import dataclasses
99

10+
from frequenz.sdk.timeseries._quantities import Power
11+
1012
from .request import Request
1113

1214

@@ -34,13 +36,13 @@ class Result(_BaseResultMixin):
3436
class _BaseSuccessMixin:
3537
"""Result returned when setting the power succeed for all batteries."""
3638

37-
succeeded_power: float
39+
succeeded_power: Power
3840
"""The part of the requested power that was successfully set."""
3941

4042
succeeded_batteries: set[int]
4143
"""The subset of batteries for which power was set successfully."""
4244

43-
excess_power: float
45+
excess_power: Power
4446
"""The part of the requested power that could not be fulfilled.
4547
4648
This happens when the requested power is outside the available power bounds.
@@ -62,7 +64,7 @@ class Success(_BaseSuccessMixin, Result): # Order matters here. See above.
6264
class PartialFailure(_BaseSuccessMixin, Result):
6365
"""Result returned when any battery failed to perform the request."""
6466

65-
failed_power: float
67+
failed_power: Power
6668
"""The part of the requested power that failed to be set."""
6769

6870
failed_batteries: set[int]

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,10 @@ async def set_power(
143143
await self._power_distributing_sender.send(
144144
Request(
145145
namespace=self._power_distributing_namespace,
146-
power=power.as_watts(),
146+
power=power,
147147
batteries=self._batteries,
148148
adjust_power=adjust_power,
149-
request_timeout_sec=request_timeout.total_seconds(),
149+
request_timeout=request_timeout,
150150
include_broken_batteries=include_broken_batteries,
151151
)
152152
)
@@ -184,16 +184,15 @@ async def charge(
184184
Raises:
185185
ValueError: If the given power is negative.
186186
"""
187-
as_watts = power.as_watts()
188-
if as_watts < 0.0:
187+
if power < Power.zero():
189188
raise ValueError("Charge power must be positive.")
190189
await self._power_distributing_sender.send(
191190
Request(
192191
namespace=self._power_distributing_namespace,
193-
power=as_watts,
192+
power=power,
194193
batteries=self._batteries,
195194
adjust_power=adjust_power,
196-
request_timeout_sec=request_timeout.total_seconds(),
195+
request_timeout=request_timeout,
197196
include_broken_batteries=include_broken_batteries,
198197
)
199198
)
@@ -231,16 +230,15 @@ async def discharge(
231230
Raises:
232231
ValueError: If the given power is negative.
233232
"""
234-
as_watts = power.as_watts()
235-
if as_watts < 0.0:
233+
if power < Power.zero():
236234
raise ValueError("Discharge power must be positive.")
237235
await self._power_distributing_sender.send(
238236
Request(
239237
namespace=self._power_distributing_namespace,
240-
power=-as_watts,
238+
power=-power,
241239
batteries=self._batteries,
242240
adjust_power=adjust_power,
243-
request_timeout_sec=request_timeout.total_seconds(),
241+
request_timeout=request_timeout,
244242
include_broken_batteries=include_broken_batteries,
245243
)
246244
)

src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,19 +240,19 @@ def component_data(self, component_id: int) -> Receiver[EVChargerData]:
240240

241241
return output_chan.new_receiver()
242242

243-
async def set_bounds(self, component_id: int, max_amps: float) -> None:
243+
async def set_bounds(self, component_id: int, max_current: Current) -> None:
244244
"""Send given max current bound for the given EV Charger to the microgrid API.
245245
246246
Bounds are used to limit the max current drawn by an EV, although the exact
247247
value will be determined by the EV.
248248
249249
Args:
250250
component_id: ID of EV Charger to set the current bounds to.
251-
max_amps: maximum current in amps, that an EV can draw from this EV Charger.
251+
max_current: maximum current that an EV can draw from this EV Charger.
252252
"""
253253
if not self._bounds_setter:
254254
self._bounds_setter = BoundsSetter(self._repeat_interval)
255-
await self._bounds_setter.set(component_id, max_amps)
255+
await self._bounds_setter.set(component_id, max_current.as_amperes())
256256

257257
def new_bounds_sender(self) -> Sender[ComponentCurrentLimit]:
258258
"""Return a `Sender` for setting EV Charger current bounds with.

0 commit comments

Comments
 (0)