|
17 | 17 | from frequenz.api.common.v1.metrics import bounds_pb2, metric_sample_pb2 |
18 | 18 | from frequenz.api.common.v1.microgrid.components import components_pb2 |
19 | 19 | from frequenz.api.microgrid.v1 import microgrid_pb2, microgrid_pb2_grpc |
| 20 | +from frequenz.channels import Receiver |
20 | 21 | from frequenz.client.base import channel, client, conversion, retry, streaming |
21 | 22 | from frequenz.client.common.microgrid.components import ComponentId |
22 | 23 | from google.protobuf.empty_pb2 import Empty |
|
30 | 31 | from .component._component_proto import component_from_proto |
31 | 32 | from .component._connection import ComponentConnection |
32 | 33 | from .component._connection_proto import component_connection_from_proto |
| 34 | +from .component._data_samples import ComponentDataSamples |
| 35 | +from .component._data_samples_proto import component_data_samples_from_proto |
33 | 36 | from .component._types import ComponentTypes |
34 | 37 | from .metrics._bounds import Bounds |
35 | 38 | from .metrics._metric import Metric |
@@ -84,8 +87,11 @@ def __init__( |
84 | 87 | connect=connect, |
85 | 88 | channel_defaults=channel_defaults, |
86 | 89 | ) |
87 | | - self._broadcasters: dict[ |
88 | | - ComponentId, streaming.GrpcStreamBroadcaster[Any, Any] |
| 90 | + self._component_data_broadcasters: dict[ |
| 91 | + str, |
| 92 | + streaming.GrpcStreamBroadcaster[ |
| 93 | + microgrid_pb2.ReceiveComponentDataStreamResponse, ComponentDataSamples |
| 94 | + ], |
89 | 95 | ] = {} |
90 | 96 | self._sensor_data_broadcasters: dict[ |
91 | 97 | str, |
@@ -118,15 +124,15 @@ async def __aexit__( |
118 | 124 | *( |
119 | 125 | broadcaster.stop() |
120 | 126 | for broadcaster in itertools.chain( |
121 | | - self._broadcasters.values(), |
| 127 | + self._component_data_broadcasters.values(), |
122 | 128 | self._sensor_data_broadcasters.values(), |
123 | 129 | ) |
124 | 130 | ), |
125 | 131 | return_exceptions=True, |
126 | 132 | ) |
127 | 133 | if isinstance(exc, BaseException) |
128 | 134 | ) |
129 | | - self._broadcasters.clear() |
| 135 | + self._component_data_broadcasters.clear() |
130 | 136 | self._sensor_data_broadcasters.clear() |
131 | 137 |
|
132 | 138 | result = None |
@@ -530,6 +536,70 @@ async def add_component_bounds( # noqa: DOC502 (Raises ApiClientError indirectl |
530 | 536 |
|
531 | 537 | return None |
532 | 538 |
|
| 539 | + # noqa: DOC502 (Raises ApiClientError indirectly) |
| 540 | + def receive_component_data_samples_stream( |
| 541 | + self, |
| 542 | + component: ComponentId | Component, |
| 543 | + metrics: Iterable[Metric | int], |
| 544 | + *, |
| 545 | + buffer_size: int = 50, |
| 546 | + ) -> Receiver[ComponentDataSamples]: |
| 547 | + """Stream data samples from a component. |
| 548 | +
|
| 549 | + At least one metric must be specified. If no metric is specified, then the |
| 550 | + stream will raise an error. |
| 551 | +
|
| 552 | + Warning: |
| 553 | + Components may not support all metrics. If a component does not support |
| 554 | + a given metric, then the returned data stream will not contain that metric. |
| 555 | +
|
| 556 | + There is no way to tell if a metric is not being received because the |
| 557 | + component does not support it or because there is a transient issue when |
| 558 | + retrieving the metric from the component. |
| 559 | +
|
| 560 | + The supported metrics by a component can even change with time, for example, |
| 561 | + if a component is updated with new firmware. |
| 562 | +
|
| 563 | + Args: |
| 564 | + component: The component to stream data from. |
| 565 | + metrics: List of metrics to return. Only the specified metrics will be |
| 566 | + returned. |
| 567 | + buffer_size: The maximum number of messages to buffer in the returned |
| 568 | + receiver. After this limit is reached, the oldest messages will be |
| 569 | + dropped. |
| 570 | +
|
| 571 | + Returns: |
| 572 | + The data stream from the component. |
| 573 | + """ |
| 574 | + component_id = _get_component_id(component) |
| 575 | + metrics_set = frozenset([_get_metric_value(m) for m in metrics]) |
| 576 | + key = f"{component_id}-{hash(metrics_set)}" |
| 577 | + broadcaster = self._component_data_broadcasters.get(key) |
| 578 | + if broadcaster is None: |
| 579 | + client_id = hex(id(self))[2:] |
| 580 | + stream_name = f"microgrid-client-{client_id}-component-data-{key}" |
| 581 | + # Alias to avoid too long lines linter errors |
| 582 | + # pylint: disable-next=invalid-name |
| 583 | + Request = microgrid_pb2.ReceiveComponentDataStreamRequest |
| 584 | + broadcaster = streaming.GrpcStreamBroadcaster( |
| 585 | + stream_name, |
| 586 | + lambda: aiter( |
| 587 | + self.stub.ReceiveComponentDataStream( |
| 588 | + Request( |
| 589 | + component_id=_get_component_id(component), |
| 590 | + filter=Request.ComponentDataStreamFilter( |
| 591 | + metrics=metrics_set |
| 592 | + ), |
| 593 | + ), |
| 594 | + timeout=DEFAULT_GRPC_CALL_TIMEOUT, |
| 595 | + ) |
| 596 | + ), |
| 597 | + lambda msg: component_data_samples_from_proto(msg.data), |
| 598 | + retry_strategy=self._retry_strategy, |
| 599 | + ) |
| 600 | + self._component_data_broadcasters[key] = broadcaster |
| 601 | + return broadcaster.new_receiver(maxsize=buffer_size) |
| 602 | + |
533 | 603 |
|
534 | 604 | class Validity(enum.Enum): |
535 | 605 | """The duration for which a given list of bounds will stay in effect.""" |
|
0 commit comments