From 6b132d8374499e0fb8764298fe6089eb703b03f8 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Fri, 27 Jun 2025 10:36:00 +0200 Subject: [PATCH 1/5] Bump `frequenz-client-common` to v0.3.3 Signed-off-by: Leandro Lucarella --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d3015749..d10ca3d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,8 @@ dependencies = [ "frequenz-api-microgrid >= 0.15.5, < 0.16.0", "frequenz-channels >= 1.0.0-rc1, < 2.0.0", "frequenz-client-base >= 0.8.0, < 0.12.0", - "frequenz-client-common >= 0.3.2, < 0.4.0", + #"frequenz-client-common >= 0.3.2, < 0.4.0", + "frequenz-client-common @ git+https://github.com/frequenz-floss/frequenz-client-common-python.git@refs/pull/82/head", "grpcio >= 1.63.0, < 2", "protobuf >= 5.26.1, < 7", "timezonefinder >= 6.2.0, < 7", From c2f983f0420a786906517b10e126d7be318db8ca Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Fri, 27 Jun 2025 10:38:42 +0200 Subject: [PATCH 2/5] Use `ComponentCategory` from `frequenz-client-common` This allows for better interoperability with other API clients. Signed-off-by: Leandro Lucarella --- src/frequenz/client/microgrid/__init__.py | 2 -- src/frequenz/client/microgrid/_client.py | 3 +- src/frequenz/client/microgrid/_component.py | 37 +++++---------------- tests/test_client.py | 5 ++- tests/test_component.py | 7 ++-- 5 files changed, 15 insertions(+), 39 deletions(-) diff --git a/src/frequenz/client/microgrid/__init__.py b/src/frequenz/client/microgrid/__init__.py index 5b4c53de..0ae99d39 100644 --- a/src/frequenz/client/microgrid/__init__.py +++ b/src/frequenz/client/microgrid/__init__.py @@ -10,7 +10,6 @@ from ._client import MicrogridApiClient from ._component import ( Component, - ComponentCategory, ComponentMetadata, ComponentMetricId, ComponentType, @@ -74,7 +73,6 @@ "BatteryRelayState", "ClientNotConnected", "Component", - "ComponentCategory", "ComponentData", "ComponentMetadata", "ComponentMetricId", diff --git a/src/frequenz/client/microgrid/_client.py b/src/frequenz/client/microgrid/_client.py index 4f8cdcb7..2cac194f 100644 --- a/src/frequenz/client/microgrid/_client.py +++ b/src/frequenz/client/microgrid/_client.py @@ -18,14 +18,13 @@ from frequenz.channels import Receiver from frequenz.client.base import channel, client, retry, streaming from frequenz.client.common.microgrid import MicrogridId -from frequenz.client.common.microgrid.components import ComponentId +from frequenz.client.common.microgrid.components import ComponentCategory, ComponentId from frequenz.client.common.microgrid.sensors import SensorId from google.protobuf.empty_pb2 import Empty from typing_extensions import override from ._component import ( Component, - ComponentCategory, component_category_from_protobuf, component_metadata_from_protobuf, component_type_from_protobuf, diff --git a/src/frequenz/client/microgrid/_component.py b/src/frequenz/client/microgrid/_component.py index 6aa42a20..16745a3b 100644 --- a/src/frequenz/client/microgrid/_component.py +++ b/src/frequenz/client/microgrid/_component.py @@ -8,7 +8,7 @@ from frequenz.api.common import components_pb2 from frequenz.api.microgrid import grid_pb2, inverter_pb2 -from frequenz.client.common.microgrid.components import ComponentId +from frequenz.client.common.microgrid.components import ComponentCategory, ComponentId class ComponentType(Enum): @@ -61,31 +61,6 @@ def component_type_from_protobuf( return None -class ComponentCategory(Enum): - """Possible types of microgrid component.""" - - NONE = components_pb2.ComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED - """Unspecified component category.""" - - GRID = components_pb2.ComponentCategory.COMPONENT_CATEGORY_GRID - """Grid component.""" - - METER = components_pb2.ComponentCategory.COMPONENT_CATEGORY_METER - """Meter component.""" - - INVERTER = components_pb2.ComponentCategory.COMPONENT_CATEGORY_INVERTER - """Inverter component.""" - - BATTERY = components_pb2.ComponentCategory.COMPONENT_CATEGORY_BATTERY - """Battery component.""" - - EV_CHARGER = components_pb2.ComponentCategory.COMPONENT_CATEGORY_EV_CHARGER - """EV charger component.""" - - CHP = components_pb2.ComponentCategory.COMPONENT_CATEGORY_CHP - """CHP component.""" - - def component_category_from_protobuf( component_category: components_pb2.ComponentCategory.ValueType, ) -> ComponentCategory: @@ -107,8 +82,14 @@ def component_category_from_protobuf( if component_category == components_pb2.ComponentCategory.COMPONENT_CATEGORY_SENSOR: raise ValueError("Cannot create a component from a sensor!") - if not any(t.value == component_category for t in ComponentCategory): - return ComponentCategory.NONE + # We are converting to `int` because the microgrid API actually uses the common API + # v0.4, which import files without the `v1` prefix, and the `ComponentCategory` in + # `client-common` uses the API version v0.6, which imports files with the `v1` + # prefix, so effectively here we are comparing 2 different enums. + # With this conversion we are assuming the enum values didn't change between v0.4 + # and v0.6, which is the case as of v0.11.0. + if not any(int(t.value) == component_category for t in ComponentCategory): + return ComponentCategory.UNSPECIFIED return ComponentCategory(component_category) diff --git a/tests/test_client.py b/tests/test_client.py index d1029839..17c82627 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -18,7 +18,7 @@ from frequenz.api.microgrid import grid_pb2, inverter_pb2, microgrid_pb2, sensor_pb2 from frequenz.client.base import conversion, retry from frequenz.client.common.microgrid import MicrogridId -from frequenz.client.common.microgrid.components import ComponentId +from frequenz.client.common.microgrid.components import ComponentCategory, ComponentId from frequenz.client.common.microgrid.sensors import SensorId from google.protobuf.empty_pb2 import Empty @@ -26,7 +26,6 @@ ApiClientError, BatteryData, Component, - ComponentCategory, ComponentData, Connection, EVChargerData, @@ -191,7 +190,7 @@ async def test_components(client: _TestClient) -> None: grid_fuse = Fuse(123.0) assert set(await client.components()) == { - Component(ComponentId(100), ComponentCategory.NONE), + Component(ComponentId(100), ComponentCategory.UNSPECIFIED), Component( ComponentId(101), ComponentCategory.GRID, diff --git a/tests/test_component.py b/tests/test_component.py index 0fdc527d..41c338c0 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -5,11 +5,10 @@ import pytest from frequenz.api.common import components_pb2 -from frequenz.client.common.microgrid.components import ComponentId +from frequenz.client.common.microgrid.components import ComponentCategory, ComponentId from frequenz.client.microgrid import ( Component, - ComponentCategory, ) from frequenz.client.microgrid._component import component_category_from_protobuf @@ -20,7 +19,7 @@ def test_component_category_from_protobuf() -> None: component_category_from_protobuf( components_pb2.ComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED ) - == ComponentCategory.NONE + == ComponentCategory.UNSPECIFIED ) assert ( @@ -58,7 +57,7 @@ def test_component_category_from_protobuf() -> None: == ComponentCategory.EV_CHARGER ) - assert component_category_from_protobuf(666) == ComponentCategory.NONE # type: ignore + assert component_category_from_protobuf(666) == ComponentCategory.UNSPECIFIED # type: ignore with pytest.raises(ValueError): component_category_from_protobuf( From 07dd669091ee757a4b0a0420af5778cbd80b52c6 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Fri, 27 Jun 2025 11:06:20 +0200 Subject: [PATCH 3/5] Allow for unknown categories in `Component` Now an `unknown` category will be stored as a raw `int` instead of being converted to `UNSPECIFIED`. Signed-off-by: Leandro Lucarella --- src/frequenz/client/microgrid/_client.py | 8 ++++++-- src/frequenz/client/microgrid/_component.py | 2 +- tests/test_client.py | 3 ++- tests/test_component.py | 4 ++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/frequenz/client/microgrid/_client.py b/src/frequenz/client/microgrid/_client.py index 2cac194f..4ef24a2c 100644 --- a/src/frequenz/client/microgrid/_client.py +++ b/src/frequenz/client/microgrid/_client.py @@ -385,9 +385,13 @@ async def _expect_category( raise ValueError(f"Unable to find {component_id}") from exc if comp.category != expected_category: + cat = ( + comp.category.name + if isinstance(comp.category, ComponentCategory) + else comp.category + ) raise ValueError( - f"{component_id} is a {comp.category.name.lower()}" - f", not a {expected_category.name.lower()}." + f"{component_id} has category {cat}, but {expected_category.name} was expected" ) async def meter_data( # noqa: DOC502 (ValueError is raised indirectly by _expect_category) diff --git a/src/frequenz/client/microgrid/_component.py b/src/frequenz/client/microgrid/_component.py index 16745a3b..0740dd91 100644 --- a/src/frequenz/client/microgrid/_component.py +++ b/src/frequenz/client/microgrid/_component.py @@ -145,7 +145,7 @@ class Component: component_id: ComponentId """The ID of this component.""" - category: ComponentCategory + category: ComponentCategory | int """The category of this component.""" type: ComponentType | None = None diff --git a/tests/test_client.py b/tests/test_client.py index 17c82627..4b491779 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -656,7 +656,8 @@ async def test_data_bad_category( # It should raise a ValueError for a wrong component category with pytest.raises( - ValueError, match=f"{component_id} is a .*, not a {method[:-5]}" + ValueError, + match=rf"{component_id} has category .*, but {method[:-5].upper()} was expected", ): await getattr(client, method)(component_id) diff --git a/tests/test_component.py b/tests/test_component.py index 41c338c0..712825ff 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -90,8 +90,8 @@ def test_Component() -> None: # Should raise error with negative ID Component(ComponentId(-1), ComponentCategory.GRID) - invalid_type = Component(ComponentId(666), -1) # type: ignore + invalid_type = Component(ComponentId(666), -1) assert not invalid_type.is_valid() - another_invalid_type = Component(ComponentId(666), 666) # type: ignore + another_invalid_type = Component(ComponentId(666), 666) assert not another_invalid_type.is_valid() From 7d283f8742b64d3d7cabc2493e37d7d41c1023a7 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Fri, 27 Jun 2025 11:07:30 +0200 Subject: [PATCH 4/5] Use `enum_from_proto()` from `frequenz-client-common` This means we can remove `component_category_from_protobuf()` and now unknown categories will be preserved. Signed-off-by: Leandro Lucarella --- src/frequenz/client/microgrid/_client.py | 4 +- src/frequenz/client/microgrid/_component.py | 33 ------------ tests/test_component.py | 58 +-------------------- 3 files changed, 3 insertions(+), 92 deletions(-) diff --git a/src/frequenz/client/microgrid/_client.py b/src/frequenz/client/microgrid/_client.py index 4ef24a2c..3381e529 100644 --- a/src/frequenz/client/microgrid/_client.py +++ b/src/frequenz/client/microgrid/_client.py @@ -17,6 +17,7 @@ from frequenz.api.microgrid import microgrid_pb2, microgrid_pb2_grpc, sensor_pb2 from frequenz.channels import Receiver from frequenz.client.base import channel, client, retry, streaming +from frequenz.client.common.enum_proto import enum_from_proto from frequenz.client.common.microgrid import MicrogridId from frequenz.client.common.microgrid.components import ComponentCategory, ComponentId from frequenz.client.common.microgrid.sensors import SensorId @@ -25,7 +26,6 @@ from ._component import ( Component, - component_category_from_protobuf, component_metadata_from_protobuf, component_type_from_protobuf, ) @@ -186,7 +186,7 @@ async def components( # noqa: DOC502 (raises ApiClientError indirectly) result: Iterable[Component] = map( lambda c: Component( ComponentId(c.id), - component_category_from_protobuf(c.category), + enum_from_proto(c.category, ComponentCategory), component_type_from_protobuf(c.category, c.inverter), component_metadata_from_protobuf(c.category, c.grid), ), diff --git a/src/frequenz/client/microgrid/_component.py b/src/frequenz/client/microgrid/_component.py index 0740dd91..9b61dbed 100644 --- a/src/frequenz/client/microgrid/_component.py +++ b/src/frequenz/client/microgrid/_component.py @@ -61,39 +61,6 @@ def component_type_from_protobuf( return None -def component_category_from_protobuf( - component_category: components_pb2.ComponentCategory.ValueType, -) -> ComponentCategory: - """Convert a protobuf ComponentCategory message to ComponentCategory enum. - - For internal-only use by the `microgrid` package. - - Args: - component_category: protobuf enum to convert - - Returns: - Enum value corresponding to the protobuf message. - - Raises: - ValueError: if `component_category` is a sensor (this is not considered - a valid component category as it does not form part of the - microgrid itself) - """ - if component_category == components_pb2.ComponentCategory.COMPONENT_CATEGORY_SENSOR: - raise ValueError("Cannot create a component from a sensor!") - - # We are converting to `int` because the microgrid API actually uses the common API - # v0.4, which import files without the `v1` prefix, and the `ComponentCategory` in - # `client-common` uses the API version v0.6, which imports files with the `v1` - # prefix, so effectively here we are comparing 2 different enums. - # With this conversion we are assuming the enum values didn't change between v0.4 - # and v0.6, which is the case as of v0.11.0. - if not any(int(t.value) == component_category for t in ComponentCategory): - return ComponentCategory.UNSPECIFIED - - return ComponentCategory(component_category) - - @dataclass(frozen=True) class Fuse: """Fuse data class.""" diff --git a/tests/test_component.py b/tests/test_component.py index 712825ff..525b6aaf 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -4,65 +4,9 @@ """Tests for the microgrid component wrapper.""" import pytest -from frequenz.api.common import components_pb2 from frequenz.client.common.microgrid.components import ComponentCategory, ComponentId -from frequenz.client.microgrid import ( - Component, -) -from frequenz.client.microgrid._component import component_category_from_protobuf - - -def test_component_category_from_protobuf() -> None: - """Test the creating component category from protobuf.""" - assert ( - component_category_from_protobuf( - components_pb2.ComponentCategory.COMPONENT_CATEGORY_UNSPECIFIED - ) - == ComponentCategory.UNSPECIFIED - ) - - assert ( - component_category_from_protobuf( - components_pb2.ComponentCategory.COMPONENT_CATEGORY_GRID - ) - == ComponentCategory.GRID - ) - - assert ( - component_category_from_protobuf( - components_pb2.ComponentCategory.COMPONENT_CATEGORY_METER - ) - == ComponentCategory.METER - ) - - assert ( - component_category_from_protobuf( - components_pb2.ComponentCategory.COMPONENT_CATEGORY_INVERTER - ) - == ComponentCategory.INVERTER - ) - - assert ( - component_category_from_protobuf( - components_pb2.ComponentCategory.COMPONENT_CATEGORY_BATTERY - ) - == ComponentCategory.BATTERY - ) - - assert ( - component_category_from_protobuf( - components_pb2.ComponentCategory.COMPONENT_CATEGORY_EV_CHARGER - ) - == ComponentCategory.EV_CHARGER - ) - - assert component_category_from_protobuf(666) == ComponentCategory.UNSPECIFIED # type: ignore - - with pytest.raises(ValueError): - component_category_from_protobuf( - components_pb2.ComponentCategory.COMPONENT_CATEGORY_SENSOR - ) +from frequenz.client.microgrid import Component # pylint: disable=invalid-name From 7b8bf8fc6f2f416ef27b3d40fef707cd2d1577ce Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Fri, 27 Jun 2025 11:07:38 +0200 Subject: [PATCH 5/5] Update release notes Signed-off-by: Leandro Lucarella --- RELEASE_NOTES.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 80e5f0cb..0cd65e95 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -6,7 +6,12 @@ ## Upgrading - +- `ComponentCategory` is now being imported from `frequenz-client-common` v0.3.3. This means: + + * You need to add/update the minimum dependency to `frequenz-client-common` + * `ComponentCategory.NONE` is not named `ComponentCategory.UNSPECIFIED` + * `Component.category` has now the type `ComponentCategory | int`. + * Before if a category that had no corresponding value in `ComponentCategory` was received (the server is probably using a newer version with a new category), `ComponentCategory.NONE` was used. Now we keep the original `int` received from protobuf. This allows to use an old client version with a new server, as long as the user knows how to interpret the `int` value, so it provided more flexibility. ## New Features