Skip to content

Commit dda35f9

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

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
@@ -224,6 +226,85 @@ async def list_connections( # noqa: DOC502 (raises ApiClientError indirectly)
224226

225227
return map(component_connection_from_proto, connection_list.connections)
226228

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

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

0 commit comments

Comments
 (0)