Skip to content

Commit c217620

Browse files
committed
Implement SetComponentPowerActive
Signed-off-by: Leandro Lucarella <[email protected]>
1 parent d6b565c commit c217620

File tree

1 file changed

+100
-1
lines changed

1 file changed

+100
-1
lines changed

src/frequenz/client/microgrid/_client.py

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33

44
"""Client for requests to the Microgrid API."""
55

6+
import math
67
from collections.abc import Iterable
78
from dataclasses import replace
9+
from datetime import datetime, timedelta
810
from typing import Any, assert_never
911

1012
from frequenz.api.common.v1.microgrid.components import components_pb2
1113
from frequenz.api.microgrid.v1 import microgrid_pb2, microgrid_pb2_grpc
12-
from frequenz.client.base import channel, client, retry, streaming
14+
from frequenz.client.base import channel, client, conversion, retry, streaming
1315
from google.protobuf.empty_pb2 import Empty
1416

1517
from ._id import ComponentId
@@ -211,6 +213,85 @@ async def list_connections( # noqa: DOC502 (raises ApiClientError indirectly)
211213

212214
return map(component_connection_from_proto, connection_list.connections)
213215

216+
async def set_component_power_active( # noqa: DOC502 (raises ApiClientError indirectly)
217+
self,
218+
component: ComponentId | Component,
219+
power: float,
220+
*,
221+
request_lifetime: timedelta | None = None,
222+
validate_arguments: bool = True,
223+
) -> datetime | None:
224+
"""Set the active power output of a component.
225+
226+
The power output can be negative or positive, depending on whether the component
227+
is supposed to be discharging or charging, respectively.
228+
229+
The power output is specified in watts.
230+
231+
The return value is the timestamp until which the given power command will
232+
stay in effect. After this timestamp, the component's active power will be
233+
set to 0, if the API receives no further command to change it before then.
234+
By default, this timestamp will be set to the current time plus 60 seconds.
235+
236+
Note:
237+
The target component may have a resolution of more than 1 W. E.g., an
238+
inverter may have a resolution of 88 W. In such cases, the magnitude of
239+
power will be floored to the nearest multiple of the resolution.
240+
241+
Performs the following sequence actions for the following component
242+
categories:
243+
244+
* Inverter: Sends the discharge command to the inverter, making it deliver
245+
AC power.
246+
* TODO document missing.
247+
248+
Args:
249+
component: The component to set the output active power of.
250+
power: The output active power level, in watts. Negative values are for
251+
discharging, and positive values are for charging.
252+
request_lifetime: The duration, until which the request will stay in effect.
253+
This duration has to be between 10 seconds and 15 minutes (including
254+
both limits), otherwise the request will be rejected. It has
255+
a resolution of a second, so fractions of a second will be rounded for
256+
`timedelta` objects, and it is interpreted as seconds for `int` objects.
257+
If not provided, it usually defaults to 60 seconds.
258+
validate_arguments: Whether to validate the arguments before sending the
259+
request. If `True` a `ValueError` will be raised if an argument is
260+
invalid without even sending the request to the server, if `False`, the
261+
request will be sent without validation.
262+
263+
Returns:
264+
The timestamp until which the given power command will stay in effect, or
265+
`None` if it was not provided.
266+
267+
Raises:
268+
ApiClientError: If the are any errors communicating with the Microgrid API,
269+
most likely a subclass of
270+
[GrpcError][frequenz.client.microgrid.GrpcError].
271+
"""
272+
lifetime_seconds = _delta_to_seconds(request_lifetime)
273+
274+
if validate_arguments:
275+
_validate_set_power_args(power=power, request_lifetime=lifetime_seconds)
276+
277+
response = await client.call_stub_method(
278+
self,
279+
lambda: self._async_stub.SetComponentPowerActive(
280+
microgrid_pb2.SetComponentPowerActiveRequest(
281+
component_id=_get_component_id(component),
282+
power=power,
283+
request_lifetime=lifetime_seconds,
284+
),
285+
timeout=int(DEFAULT_GRPC_CALL_TIMEOUT),
286+
),
287+
method_name="SetComponentPowerActive",
288+
)
289+
290+
if response.HasField("valid_until"):
291+
return conversion.to_datetime(response.valid_until)
292+
293+
return None
294+
214295

215296
def _get_component_id(component: ComponentId | Component) -> int:
216297
"""Get the component ID from a component or component ID."""
@@ -234,3 +315,21 @@ def _get_category_value(
234315
return components_pb2.ComponentCategory.ValueType(category)
235316
case unexpected:
236317
assert_never(unexpected)
318+
319+
320+
def _delta_to_seconds(delta: timedelta | None) -> int | None:
321+
"""Convert a `timedelta` to seconds (or `None` if `None`)."""
322+
return round(delta.total_seconds()) if delta is not None else None
323+
324+
325+
def _validate_set_power_args(*, power: float, request_lifetime: int | None) -> None:
326+
"""Validate the request lifetime."""
327+
if math.isnan(power):
328+
raise ValueError("power cannot be NaN")
329+
if request_lifetime is not None:
330+
minimum_lifetime = 10 # 10 seconds
331+
maximum_lifetime = 900 # 15 minutes
332+
if not minimum_lifetime <= request_lifetime <= maximum_lifetime:
333+
raise ValueError(
334+
"request_lifetime must be between 10 seconds and 15 minutes"
335+
)

0 commit comments

Comments
 (0)