From f37d0b929081eacacfd78ebb954382c46f097583 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sun, 1 Mar 2026 11:39:10 +0100 Subject: [PATCH 1/6] Make GenericDataBatch generic over PB data type Signed-off-by: Sahas Subramanian --- src/frequenz/client/reporting/_types.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/frequenz/client/reporting/_types.py b/src/frequenz/client/reporting/_types.py index 7b090ba..aee4cd9 100644 --- a/src/frequenz/client/reporting/_types.py +++ b/src/frequenz/client/reporting/_types.py @@ -7,7 +7,7 @@ from collections.abc import Iterable, Iterator from dataclasses import dataclass from datetime import datetime -from typing import Any, NamedTuple +from typing import Generic, NamedTuple, Protocol, TypeVar # pylint: disable=no-name-in-module from frequenz.api.reporting.v1alpha10.reporting_pb2 import ( @@ -40,8 +40,20 @@ class MetricSample(NamedTuple): value: float +class _PbMgTelem(Protocol): + """Protocol for microgrid telemetry from the Reporting API client.""" + + @property + def microgrid_id(self) -> int: + """Return the microgrid ID of the telemetry batch.""" + + + +_MgTelemT = TypeVar("_MgTelemT", bound=_PbMgTelem) + + @dataclass(frozen=True) -class GenericDataBatch: +class GenericDataBatch(Generic[_MgTelemT]): """Base class for batches of microgrid data (components or sensors). This class serves as a base for handling batches of data related to microgrid @@ -50,7 +62,7 @@ class GenericDataBatch: functionality to work with bounds if applicable. """ - _data_pb: Any + _data_pb: _MgTelemT id_attr: str items_attr: str has_bounds: bool = False @@ -142,7 +154,9 @@ def __iter__(self) -> Iterator[MetricSample]: @dataclass(frozen=True) -class ComponentsDataBatch(GenericDataBatch): +class ComponentsDataBatch( + GenericDataBatch[PBReceiveMicrogridComponentsDataStreamResponse] +): """Batch of microgrid components data.""" def __init__(self, data_pb: PBReceiveMicrogridComponentsDataStreamResponse): @@ -160,7 +174,7 @@ def __init__(self, data_pb: PBReceiveMicrogridComponentsDataStreamResponse): @dataclass(frozen=True) -class SensorsDataBatch(GenericDataBatch): +class SensorsDataBatch(GenericDataBatch[PBReceiveMicrogridSensorsDataStreamResponse]): """Batch of microgrid sensors data.""" def __init__(self, data_pb: PBReceiveMicrogridSensorsDataStreamResponse): From e367ed983177ddeec30f825cc674e8a5422e9dbe Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sun, 1 Mar 2026 12:02:48 +0100 Subject: [PATCH 2/6] Make GenericDataBatch generic over PB telem type Signed-off-by: Sahas Subramanian --- src/frequenz/client/reporting/_types.py | 33 +++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/frequenz/client/reporting/_types.py b/src/frequenz/client/reporting/_types.py index aee4cd9..a3c542a 100644 --- a/src/frequenz/client/reporting/_types.py +++ b/src/frequenz/client/reporting/_types.py @@ -10,6 +10,15 @@ from typing import Generic, NamedTuple, Protocol, TypeVar # pylint: disable=no-name-in-module +from frequenz.api.common.v1alpha8.metrics.metrics_pb2 import ( + MetricSample as PbMetricSample, +) +from frequenz.api.common.v1alpha8.microgrid.electrical_components.electrical_components_pb2 import ( + ElectricalComponentTelemetry as PbElectricalComponentTelemetry, +) +from frequenz.api.common.v1alpha8.microgrid.sensors.sensors_pb2 import ( + SensorTelemetry as PbSensorTelemetry, +) from frequenz.api.reporting.v1alpha10.reporting_pb2 import ( ReceiveAggregatedMicrogridComponentsDataStreamResponse as PBAggregatedStreamResponse, ) @@ -48,12 +57,20 @@ def microgrid_id(self) -> int: """Return the microgrid ID of the telemetry batch.""" +class _PbTelem(Protocol): + """Protocol for telemetry items in the Reporting API client.""" + + @property + def metric_samples(self) -> MutableSequence[PbMetricSample]: + """Return the metric samples of the telemetry item.""" + _MgTelemT = TypeVar("_MgTelemT", bound=_PbMgTelem) +_TelemT = TypeVar("_TelemT", bound=_PbTelem) @dataclass(frozen=True) -class GenericDataBatch(Generic[_MgTelemT]): +class GenericDataBatch(Generic[_MgTelemT, _TelemT]): """Base class for batches of microgrid data (components or sensors). This class serves as a base for handling batches of data related to microgrid @@ -77,9 +94,7 @@ def is_empty(self) -> bool: if not items: return True for item in items: - if not getattr(item, "metric_samples", []) and not getattr( - item, "states", [] - ): + if not item.metric_samples and not getattr(item, "states", []): return True return False @@ -105,7 +120,7 @@ def __iter__(self) -> Iterator[MetricSample]: for item in items: cid = getattr(item, self.id_attr) - for sample in getattr(item, "metric_samples", []): + for sample in item.metric_samples: ts = datetime_from_proto(sample.sample_time) met = enum_from_proto(sample.metric, Metric, allow_invalid=False).name @@ -155,7 +170,9 @@ def __iter__(self) -> Iterator[MetricSample]: @dataclass(frozen=True) class ComponentsDataBatch( - GenericDataBatch[PBReceiveMicrogridComponentsDataStreamResponse] + GenericDataBatch[ + PBReceiveMicrogridComponentsDataStreamResponse, PbElectricalComponentTelemetry + ] ): """Batch of microgrid components data.""" @@ -174,7 +191,9 @@ def __init__(self, data_pb: PBReceiveMicrogridComponentsDataStreamResponse): @dataclass(frozen=True) -class SensorsDataBatch(GenericDataBatch[PBReceiveMicrogridSensorsDataStreamResponse]): +class SensorsDataBatch( + GenericDataBatch[PBReceiveMicrogridSensorsDataStreamResponse, PbSensorTelemetry] +): """Batch of microgrid sensors data.""" def __init__(self, data_pb: PBReceiveMicrogridSensorsDataStreamResponse): From cf2aa83338fd835dbabff5438854d94046bb2462 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sun, 1 Mar 2026 12:08:42 +0100 Subject: [PATCH 3/6] Replace getattr calls with typed callable for fetching items Signed-off-by: Sahas Subramanian --- src/frequenz/client/reporting/_types.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/frequenz/client/reporting/_types.py b/src/frequenz/client/reporting/_types.py index a3c542a..6e20f05 100644 --- a/src/frequenz/client/reporting/_types.py +++ b/src/frequenz/client/reporting/_types.py @@ -4,10 +4,10 @@ """Types for the Reporting API client.""" import math -from collections.abc import Iterable, Iterator +from collections.abc import Iterable, Iterator, MutableSequence from dataclasses import dataclass from datetime import datetime -from typing import Generic, NamedTuple, Protocol, TypeVar +from typing import Callable, Generic, NamedTuple, Protocol, TypeVar # pylint: disable=no-name-in-module from frequenz.api.common.v1alpha8.metrics.metrics_pb2 import ( @@ -81,7 +81,7 @@ class GenericDataBatch(Generic[_MgTelemT, _TelemT]): _data_pb: _MgTelemT id_attr: str - items_attr: str + items_fetcher: Callable[[_MgTelemT], MutableSequence[_TelemT]] has_bounds: bool = False def is_empty(self) -> bool: @@ -90,7 +90,7 @@ def is_empty(self) -> bool: Returns: True if the batch contains no valid data. """ - items = getattr(self._data_pb, self.items_attr, []) + items = self.items_fetcher(self._data_pb) if not items: return True for item in items: @@ -116,7 +116,7 @@ def __iter__(self) -> Iterator[MetricSample]: * value: The metric value. """ mid = self._data_pb.microgrid_id - items = getattr(self._data_pb, self.items_attr) + items = self.items_fetcher(self._data_pb) for item in items: cid = getattr(item, self.id_attr) @@ -185,7 +185,7 @@ def __init__(self, data_pb: PBReceiveMicrogridComponentsDataStreamResponse): super().__init__( data_pb, id_attr="electrical_component_id", - items_attr="components", + items_fetcher=lambda pb: pb.components, has_bounds=True, ) @@ -202,7 +202,11 @@ def __init__(self, data_pb: PBReceiveMicrogridSensorsDataStreamResponse): Args: data_pb: The underlying protobuf message. """ - super().__init__(data_pb, id_attr="sensor_id", items_attr="sensors") + super().__init__( + data_pb, + id_attr="sensor_id", + items_fetcher=lambda pb: pb.sensors, + ) @dataclass(frozen=True) From 3669d5224d1d587de533d51a25cafe3d3dacddce Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sun, 1 Mar 2026 12:39:14 +0100 Subject: [PATCH 4/6] Replace getattr calls with typed callable for fetching component IDs Signed-off-by: Sahas Subramanian --- src/frequenz/client/reporting/_types.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/frequenz/client/reporting/_types.py b/src/frequenz/client/reporting/_types.py index 6e20f05..0319914 100644 --- a/src/frequenz/client/reporting/_types.py +++ b/src/frequenz/client/reporting/_types.py @@ -44,7 +44,7 @@ class MetricSample(NamedTuple): timestamp: datetime microgrid_id: int - component_id: str + component_id: int | str metric: str value: float @@ -80,7 +80,7 @@ class GenericDataBatch(Generic[_MgTelemT, _TelemT]): """ _data_pb: _MgTelemT - id_attr: str + id_fetcher: Callable[[_TelemT], int] items_fetcher: Callable[[_MgTelemT], MutableSequence[_TelemT]] has_bounds: bool = False @@ -119,7 +119,7 @@ def __iter__(self) -> Iterator[MetricSample]: items = self.items_fetcher(self._data_pb) for item in items: - cid = getattr(item, self.id_attr) + cid = self.id_fetcher(item) for sample in item.metric_samples: ts = datetime_from_proto(sample.sample_time) met = enum_from_proto(sample.metric, Metric, allow_invalid=False).name @@ -184,7 +184,7 @@ def __init__(self, data_pb: PBReceiveMicrogridComponentsDataStreamResponse): """ super().__init__( data_pb, - id_attr="electrical_component_id", + id_fetcher=lambda item: item.electrical_component_id, items_fetcher=lambda pb: pb.components, has_bounds=True, ) @@ -204,7 +204,7 @@ def __init__(self, data_pb: PBReceiveMicrogridSensorsDataStreamResponse): """ super().__init__( data_pb, - id_attr="sensor_id", + id_fetcher=lambda item: item.sensor_id, items_fetcher=lambda pb: pb.sensors, ) From 719a09f4cb23056e59e45a5d49c6cf9642a8aee7 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sun, 1 Mar 2026 13:44:21 +0100 Subject: [PATCH 5/6] Use strong types for handling states Signed-off-by: Sahas Subramanian --- src/frequenz/client/reporting/_types.py | 66 +++++++++++++++++++------ 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/src/frequenz/client/reporting/_types.py b/src/frequenz/client/reporting/_types.py index 0319914..e6fa4a8 100644 --- a/src/frequenz/client/reporting/_types.py +++ b/src/frequenz/client/reporting/_types.py @@ -4,18 +4,36 @@ """Types for the Reporting API client.""" import math -from collections.abc import Iterable, Iterator, MutableSequence +from collections.abc import Iterator, MutableSequence from dataclasses import dataclass from datetime import datetime -from typing import Callable, Generic, NamedTuple, Protocol, TypeVar +from typing import Any, Callable, Generic, NamedTuple, Protocol, TypeVar, cast # pylint: disable=no-name-in-module from frequenz.api.common.v1alpha8.metrics.metrics_pb2 import ( MetricSample as PbMetricSample, ) +from frequenz.api.common.v1alpha8.microgrid.electrical_components.electrical_components_pb2 import ( + ElectricalComponentDiagnostic as PbElectricalComponentDiagnostic, +) +from frequenz.api.common.v1alpha8.microgrid.electrical_components.electrical_components_pb2 import ( + ElectricalComponentStateCode as PbElectricalComponentStateCode, +) +from frequenz.api.common.v1alpha8.microgrid.electrical_components.electrical_components_pb2 import ( + ElectricalComponentStateSnapshot as PbElectricalComponentStateSnapshot, +) from frequenz.api.common.v1alpha8.microgrid.electrical_components.electrical_components_pb2 import ( ElectricalComponentTelemetry as PbElectricalComponentTelemetry, ) +from frequenz.api.common.v1alpha8.microgrid.sensors.sensors_pb2 import ( + SensorDiagnostic as PbSensorDiagnostic, +) +from frequenz.api.common.v1alpha8.microgrid.sensors.sensors_pb2 import ( + SensorStateCode as PbSensorStateCode, +) +from frequenz.api.common.v1alpha8.microgrid.sensors.sensors_pb2 import ( + SensorStateSnapshot as PbSensorStateSnapshot, +) from frequenz.api.common.v1alpha8.microgrid.sensors.sensors_pb2 import ( SensorTelemetry as PbSensorTelemetry, ) @@ -46,7 +64,13 @@ class MetricSample(NamedTuple): microgrid_id: int component_id: int | str metric: str - value: float + value: ( + float + | PbElectricalComponentStateCode.ValueType + | PbSensorStateCode.ValueType + | PbElectricalComponentDiagnostic + | PbSensorDiagnostic + ) class _PbMgTelem(Protocol): @@ -64,13 +88,20 @@ class _PbTelem(Protocol): def metric_samples(self) -> MutableSequence[PbMetricSample]: """Return the metric samples of the telemetry item.""" + @property + def state_snapshots(self) -> MutableSequence[Any]: + """List of state snapshots associated with this telemetry item.""" + _MgTelemT = TypeVar("_MgTelemT", bound=_PbMgTelem) _TelemT = TypeVar("_TelemT", bound=_PbTelem) +_StateSnapshotT = TypeVar( + "_StateSnapshotT", bound=PbElectricalComponentStateSnapshot | PbSensorStateSnapshot +) @dataclass(frozen=True) -class GenericDataBatch(Generic[_MgTelemT, _TelemT]): +class GenericDataBatch(Generic[_MgTelemT, _TelemT, _StateSnapshotT]): """Base class for batches of microgrid data (components or sensors). This class serves as a base for handling batches of data related to microgrid @@ -94,9 +125,9 @@ def is_empty(self) -> bool: if not items: return True for item in items: - if not item.metric_samples and not getattr(item, "states", []): - return True - return False + if item.metric_samples or item.state_snapshots: + return False + return True # pylint: disable=too-many-locals # pylint: disable=too-many-branches @@ -155,15 +186,14 @@ def __iter__(self) -> Iterator[MetricSample]: ts, mid, cid, f"{met}_bound_{i}_upper", upper ) - for state in getattr(item, "state_snapshots", []): + for state in item.state_snapshots: + state = cast(_StateSnapshotT, state) ts = datetime_from_proto(state.origin_time) for category, category_items in { - "state": getattr(state, "states", []), - "warning": getattr(state, "warnings", []), - "error": getattr(state, "errors", []), + "state": state.states, + "warning": state.warnings, + "error": state.errors, }.items(): - if not isinstance(category_items, Iterable): - continue for s in category_items: yield MetricSample(ts, mid, cid, category, s) @@ -171,7 +201,9 @@ def __iter__(self) -> Iterator[MetricSample]: @dataclass(frozen=True) class ComponentsDataBatch( GenericDataBatch[ - PBReceiveMicrogridComponentsDataStreamResponse, PbElectricalComponentTelemetry + PBReceiveMicrogridComponentsDataStreamResponse, + PbElectricalComponentTelemetry, + PbElectricalComponentStateSnapshot, ] ): """Batch of microgrid components data.""" @@ -192,7 +224,11 @@ def __init__(self, data_pb: PBReceiveMicrogridComponentsDataStreamResponse): @dataclass(frozen=True) class SensorsDataBatch( - GenericDataBatch[PBReceiveMicrogridSensorsDataStreamResponse, PbSensorTelemetry] + GenericDataBatch[ + PBReceiveMicrogridSensorsDataStreamResponse, + PbSensorTelemetry, + PbSensorStateSnapshot, + ] ): """Batch of microgrid sensors data.""" From 2f1bd6895d6217a179dd4db87e5ffdd0820896f8 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Sun, 1 Mar 2026 14:00:44 +0100 Subject: [PATCH 6/6] Update release notes Signed-off-by: Sahas Subramanian --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index f80f12e..0d56f81 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -6,7 +6,7 @@ ## Upgrading - +- `MetricSample` values now have correct type-hints. ## New Features