Skip to content

Commit 5072029

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

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
@@ -5,13 +5,15 @@
55

66
from __future__ import annotations
77

8+
import math
89
from collections.abc import Iterable
910
from dataclasses import replace
11+
from datetime import datetime, timedelta
1012
from typing import Any, assert_never
1113

1214
from frequenz.api.common.v1.microgrid.components import components_pb2
1315
from frequenz.api.microgrid.v1 import microgrid_pb2, microgrid_pb2_grpc
14-
from frequenz.client.base import channel, client, retry, streaming
16+
from frequenz.client.base import channel, client, conversion, retry, streaming
1517
from google.protobuf.empty_pb2 import Empty
1618

1719
from ._exception import ClientNotConnected
@@ -226,6 +228,85 @@ async def list_connections( # noqa: DOC502 (raises ApiClientError indirectly)
226228

227229
return map(component_connection_from_proto, connection_list.connections)
228230

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

230311
def _get_component_id(component: ComponentId | Component) -> int:
231312
"""Get the component ID from a component or component ID."""
@@ -249,3 +330,21 @@ def _get_category_value(
249330
return components_pb2.ComponentCategory.ValueType(category)
250331
case unexpected:
251332
assert_never(unexpected)
333+
334+
335+
def _delta_to_seconds(delta: timedelta | None) -> int | None:
336+
"""Convert a `timedelta` to seconds (or `None` if `None`)."""
337+
return round(delta.total_seconds()) if delta is not None else None
338+
339+
340+
def _validate_set_power_args(*, power: float, request_lifetime: int | None) -> None:
341+
"""Validate the request lifetime."""
342+
if math.isnan(power):
343+
raise ValueError("power cannot be NaN")
344+
if request_lifetime is not None:
345+
minimum_lifetime = 10 # 10 seconds
346+
maximum_lifetime = 900 # 15 minutes
347+
if not minimum_lifetime <= request_lifetime <= maximum_lifetime:
348+
raise ValueError(
349+
"request_lifetime must be between 10 seconds and 15 minutes"
350+
)

0 commit comments

Comments
 (0)