Skip to content

Commit 157f2bc

Browse files
committed
Add a list_sensors() RPC to get sensors
This RPC is also based on version 0.17 of the microgrid API instead of the current 0.15 version, so users can already use the new interface and make migrating to 0.17 more straight forward. It uses the `ListComponents` RPC filtering to retrieve only sensors. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent a6d52cf commit 157f2bc

File tree

2 files changed

+127
-1
lines changed

2 files changed

+127
-1
lines changed

src/frequenz/client/microgrid/_client.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
from ._exception import ApiClientError, ClientNotConnected
3838
from ._id import ComponentId, MicrogridId
3939
from ._metadata import Location, Metadata
40+
from ._sensor_proto import sensor_from_proto
41+
from .sensor import Sensor
4042

4143
DEFAULT_GRPC_CALL_TIMEOUT = 60.0
4244
"""The default timeout for gRPC calls made by this client (in seconds)."""
@@ -177,6 +179,33 @@ async def components( # noqa: DOC502 (raises ApiClientError indirectly)
177179

178180
return result
179181

182+
async def list_sensors( # noqa: DOC502 (raises ApiClientError indirectly)
183+
self,
184+
) -> Iterable[Sensor]:
185+
"""Fetch all the sensors present in the microgrid.
186+
187+
Returns:
188+
Iterator whose elements are all the sensors in the microgrid.
189+
190+
Raises:
191+
ApiClientError: If the are any errors communicating with the Microgrid API,
192+
most likely a subclass of
193+
[GrpcError][frequenz.client.microgrid.GrpcError].
194+
"""
195+
component_list = await client.call_stub_method(
196+
self,
197+
lambda: self.stub.ListComponents(
198+
microgrid_pb2.ComponentFilter(
199+
categories=[
200+
components_pb2.ComponentCategory.COMPONENT_CATEGORY_SENSOR
201+
]
202+
),
203+
timeout=int(DEFAULT_GRPC_CALL_TIMEOUT),
204+
),
205+
method_name="ListComponents",
206+
)
207+
return map(sensor_from_proto, component_list.components)
208+
180209
async def metadata(self) -> Metadata:
181210
"""Fetch the microgrid metadata.
182211

tests/test_client.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import grpc.aio
1212
import pytest
1313
from frequenz.api.common import components_pb2, metrics_pb2
14-
from frequenz.api.microgrid import grid_pb2, inverter_pb2, microgrid_pb2
14+
from frequenz.api.microgrid import grid_pb2, inverter_pb2, microgrid_pb2, sensor_pb2
1515
from frequenz.client.base import retry
1616
from google.protobuf.empty_pb2 import Empty
1717

@@ -31,7 +31,9 @@
3131
MeterData,
3232
MicrogridApiClient,
3333
MicrogridId,
34+
SensorId,
3435
)
36+
from frequenz.client.microgrid.sensor import Sensor
3537

3638

3739
class _TestClient(MicrogridApiClient):
@@ -461,6 +463,101 @@ async def test_metadata_grpc_error(
461463
assert "fake grpc details for metadata" in caplog.records[0].exc_text
462464

463465

466+
async def test_list_sensors(client: _TestClient) -> None:
467+
"""Test the list_sensors() method."""
468+
server_response = microgrid_pb2.ComponentList()
469+
client.mock_stub.ListComponents.return_value = server_response
470+
assert set(await client.list_sensors()) == set()
471+
472+
# Add a sensor
473+
sensor_component = microgrid_pb2.Component(
474+
id=201,
475+
category=components_pb2.ComponentCategory.COMPONENT_CATEGORY_SENSOR,
476+
sensor=sensor_pb2.Metadata(
477+
type=components_pb2.SensorType.SENSOR_TYPE_ACCELEROMETER,
478+
),
479+
)
480+
server_response.components.append(sensor_component)
481+
assert set(await client.list_sensors()) == {
482+
Sensor(id=SensorId(201)),
483+
}
484+
485+
# Add another sensor
486+
sensor_component_2 = microgrid_pb2.Component(
487+
id=202,
488+
category=components_pb2.ComponentCategory.COMPONENT_CATEGORY_SENSOR,
489+
sensor=sensor_pb2.Metadata(
490+
type=components_pb2.SensorType.SENSOR_TYPE_HYGROMETER
491+
),
492+
)
493+
server_response.components.append(sensor_component_2)
494+
assert set(await client.list_sensors()) == {
495+
Sensor(id=SensorId(201)),
496+
Sensor(id=SensorId(202)),
497+
}
498+
499+
# Add a non-sensor component to the mock response from ListSensors
500+
# The client.list_sensors() method should filter this out if it's robust,
501+
# or the ListSensors RPC itself should only return sensor components.
502+
meter_component = microgrid_pb2.Component(
503+
id=203, category=components_pb2.ComponentCategory.COMPONENT_CATEGORY_METER
504+
)
505+
server_response.components.append(meter_component)
506+
# Assert that only SENSOR category components are returned by client.list_sensors()
507+
assert set(await client.list_sensors()) == {
508+
Sensor(id=SensorId(201)),
509+
Sensor(id=SensorId(202)),
510+
Sensor(id=SensorId(203)),
511+
}
512+
# Clean up: remove the meter component from the mock response
513+
server_response.components.pop()
514+
515+
_replace_components(
516+
server_response,
517+
[
518+
microgrid_pb2.Component(
519+
id=204,
520+
category=components_pb2.ComponentCategory.COMPONENT_CATEGORY_SENSOR,
521+
sensor=sensor_pb2.Metadata(
522+
type=components_pb2.SensorType.SENSOR_TYPE_ANEMOMETER
523+
),
524+
),
525+
microgrid_pb2.Component(
526+
id=205,
527+
category=components_pb2.ComponentCategory.COMPONENT_CATEGORY_SENSOR,
528+
sensor=sensor_pb2.Metadata(
529+
type=components_pb2.SensorType.SENSOR_TYPE_PYRANOMETER
530+
),
531+
),
532+
],
533+
)
534+
assert set(await client.list_sensors()) == {
535+
Sensor(id=SensorId(204)),
536+
Sensor(id=SensorId(205)),
537+
}
538+
539+
540+
async def test_list_sensors_grpc_error(client: _TestClient) -> None:
541+
"""Test the list_sensors() method when the gRPC call fails."""
542+
client.mock_stub.GetMicrogridMetadata.return_value = (
543+
microgrid_pb2.MicrogridMetadata(microgrid_id=101)
544+
)
545+
client.mock_stub.ListComponents.side_effect = grpc.aio.AioRpcError(
546+
mock.MagicMock(name="mock_status"),
547+
mock.MagicMock(name="mock_initial_metadata"),
548+
mock.MagicMock(name="mock_trailing_metadata"),
549+
"fake grpc details",
550+
"fake grpc debug_error_string",
551+
)
552+
with pytest.raises(
553+
ApiClientError,
554+
match=r"Failed calling 'ListComponents' on 'grpc://mock_host:1234': .* "
555+
r"<status=<MagicMock name='mock_status\.name' id='.*'>>: fake grpc details "
556+
r"\(fake grpc debug_error_string\)",
557+
):
558+
await client.list_sensors()
559+
560+
464561
@pytest.fixture
465562
def meter83() -> microgrid_pb2.Component:
466563
"""Return a test meter component."""

0 commit comments

Comments
 (0)