Skip to content

Commit 271c42c

Browse files
committed
Implement AddComponentBounds
Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 9d9ae7c commit 271c42c

File tree

2 files changed

+129
-2
lines changed

2 files changed

+129
-2
lines changed

src/frequenz/client/microgrid/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
"""
88

99

10-
from ._client import MicrogridApiClient
10+
from ._client import (
11+
MicrogridApiClient,
12+
Validity,
13+
)
1114
from ._delivery_area import DeliveryArea, EnergyMarketCodeType
1215
from ._exception import (
1316
ApiClientError,
@@ -67,4 +70,5 @@
6770
"ServiceUnavailable",
6871
"UnknownError",
6972
"UnrecognizedGrpcStatus",
73+
"Validity",
7074
]

src/frequenz/client/microgrid/_client.py

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55

66
from __future__ import annotations
77

8+
import enum
89
import math
910
from collections.abc import Iterable
1011
from dataclasses import replace
1112
from datetime import datetime, timedelta
1213
from typing import assert_never
1314

14-
from frequenz.api.common.v1.metrics import metric_sample_pb2
15+
from frequenz.api.common.v1.metrics import bounds_pb2, metric_sample_pb2
1516
from frequenz.api.common.v1.microgrid.components import components_pb2
1617
from frequenz.api.microgrid.v1 import microgrid_pb2, microgrid_pb2_grpc
1718
from frequenz.channels import Receiver
@@ -30,6 +31,7 @@
3031
from .component._connection_proto import component_connection_from_proto
3132
from .component._data_samples import ComponentDataSamples
3233
from .component._data_samples_proto import component_data_samples_from_proto
34+
from .metrics._bounds import Bounds
3335
from .metrics._metric import Metric
3436

3537
DEFAULT_GRPC_CALL_TIMEOUT = 60.0
@@ -398,6 +400,103 @@ async def set_component_power_reactive( # noqa: DOC502 (raises ApiClientError i
398400

399401
return None
400402

403+
async def add_component_bounds( # noqa: DOC502 (Raises ApiClientError indirectly)
404+
self,
405+
component: ComponentId | Component,
406+
target: Metric | int,
407+
bounds: Iterable[Bounds],
408+
*,
409+
validity: Validity | None = None,
410+
) -> datetime | None:
411+
"""Add inclusion bounds for a given metric of a given component.
412+
413+
The bounds are used to define the acceptable range of values for a metric
414+
of a component. The added bounds are kept only temporarily, and removed
415+
automatically after some expiry time.
416+
417+
Inclusion bounds give the range that the system will try to keep the
418+
metric within. If the metric goes outside of these bounds, the system will
419+
try to bring it back within the bounds.
420+
If the bounds for a metric are `[[lower_1, upper_1], [lower_2, upper_2]]`,
421+
then this metric's `value` needs to comply with the constraints `lower_1 <=
422+
value <= upper_1` OR `lower_2 <= value <= upper_2`.
423+
424+
If multiple inclusion bounds have been provided for a metric, then the
425+
overlapping bounds are merged into a single bound, and non-overlapping
426+
bounds are kept separate.
427+
428+
Example:
429+
If the bounds are [[0, 10], [5, 15], [20, 30]], then the resulting bounds
430+
will be [[0, 15], [20, 30]].
431+
432+
The following diagram illustrates how bounds are applied:
433+
434+
```
435+
lower_1 upper_1
436+
<----|========|--------|========|-------->
437+
lower_2 upper_2
438+
```
439+
440+
The bounds in this example are `[[lower_1, upper_1], [lower_2, upper_2]]`.
441+
442+
```
443+
---- values here are considered out of range.
444+
==== values here are considered within range.
445+
```
446+
447+
Note:
448+
For power metrics, regardless of the bounds, 0W is always allowed.
449+
450+
Args:
451+
component: The component to add bounds to.
452+
target: The target metric whose bounds have to be added.
453+
bounds: The bounds to add to the target metric. Overlapping pairs of bounds
454+
are merged into a single pair of bounds, and non-overlapping ones are
455+
kept separated.
456+
validity: The duration for which the given bounds will stay in effect.
457+
If `None`, then the bounds will be removed after some default time
458+
decided by the server, typically 5 seconds.
459+
460+
The duration for which the bounds are valid. If not provided, the
461+
bounds are considered to be valid indefinitely.
462+
463+
Returns:
464+
The timestamp until which the given bounds will stay in effect, or `None` if
465+
if it was not provided by the server.
466+
467+
Raises:
468+
ApiClientError: If the are any errors communicating with the Microgrid API,
469+
most likely a subclass of
470+
[GrpcError][frequenz.client.microgrid.GrpcError].
471+
"""
472+
extra_args = {}
473+
if validity is not None:
474+
extra_args["validity_duration"] = validity.value
475+
response = await client.call_stub_method(
476+
self,
477+
lambda: self.stub.AddComponentBounds(
478+
microgrid_pb2.AddComponentBoundsRequest(
479+
component_id=_get_component_id(component),
480+
target_metric=_get_metric_value(target),
481+
bounds=(
482+
bounds_pb2.Bounds(
483+
lower=bounds.lower,
484+
upper=bounds.upper,
485+
)
486+
for bounds in bounds
487+
),
488+
**extra_args,
489+
),
490+
timeout=int(DEFAULT_GRPC_CALL_TIMEOUT),
491+
),
492+
method_name="AddComponentBounds",
493+
)
494+
495+
if response.HasField("ts"):
496+
return conversion.to_datetime(response.ts)
497+
498+
return None
499+
401500
# noqa: DOC502 (Raises ApiClientError indirectly)
402501
async def receive_component_data_samples_stream(
403502
self,
@@ -461,6 +560,30 @@ async def receive_component_data_samples_stream(
461560
return broadcaster.new_receiver(maxsize=buffer_size)
462561

463562

563+
class Validity(enum.Enum):
564+
"""The duration for which a given list of bounds will stay in effect."""
565+
566+
FIVE_SECONDS = (
567+
microgrid_pb2.ComponentBoundsValidityDuration.COMPONENT_BOUNDS_VALIDITY_DURATION_5_SECONDS
568+
)
569+
"""The bounds will stay in effect for 5 seconds."""
570+
571+
ONE_MINUTE = (
572+
microgrid_pb2.ComponentBoundsValidityDuration.COMPONENT_BOUNDS_VALIDITY_DURATION_1_MINUTE
573+
)
574+
"""The bounds will stay in effect for 1 minute."""
575+
576+
FIVE_MINUTES = (
577+
microgrid_pb2.ComponentBoundsValidityDuration.COMPONENT_BOUNDS_VALIDITY_DURATION_5_MINUTES
578+
)
579+
"""The bounds will stay in effect for 5 minutes."""
580+
581+
FIFTEEN_MINUTES = (
582+
microgrid_pb2.ComponentBoundsValidityDuration.COMPONENT_BOUNDS_VALIDITY_DURATION_15_MINUTES
583+
)
584+
"""The bounds will stay in effect for 15 minutes."""
585+
586+
464587
def _get_component_id(component: ComponentId | Component) -> int:
465588
"""Get the component ID from a component or component ID."""
466589
match component:

0 commit comments

Comments
 (0)