From 2b01a9a70463f9be9d919116cec6b3bf02638ea1 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Wed, 10 Jun 2026 12:06:01 +0000 Subject: [PATCH 1/8] Import microgrid info module from the microgrid client The code is copied as is from the microgrid client v0.19.0 release. It will be adapted to this repo in follow-up commits. Signed-off-by: Leandro Lucarella --- .../common/microgrid/_microgrid_info.py | 87 +++++ .../common/microgrid/_microgrid_info_proto.py | 78 +++++ tests/microgrid/test_microgrid_info.py | 317 ++++++++++++++++++ 3 files changed, 482 insertions(+) create mode 100644 src/frequenz/client/common/microgrid/_microgrid_info.py create mode 100644 src/frequenz/client/common/microgrid/_microgrid_info_proto.py create mode 100644 tests/microgrid/test_microgrid_info.py diff --git a/src/frequenz/client/common/microgrid/_microgrid_info.py b/src/frequenz/client/common/microgrid/_microgrid_info.py new file mode 100644 index 00000000..8fbe328d --- /dev/null +++ b/src/frequenz/client/common/microgrid/_microgrid_info.py @@ -0,0 +1,87 @@ +# License: MIT +# Copyright © 2025 Frequenz Energy-as-a-Service GmbH + +"""Definition of a microgrid.""" + +import datetime +import enum +import logging +from dataclasses import dataclass +from functools import cached_property + +from frequenz.api.common.v1alpha8.microgrid import microgrid_pb2 +from frequenz.client.common.microgrid import EnterpriseId, MicrogridId + +from ._delivery_area import DeliveryArea +from ._location import Location + +_logger = logging.getLogger(__name__) + + +@enum.unique +class MicrogridStatus(enum.Enum): + """The possible statuses for a microgrid.""" + + UNSPECIFIED = microgrid_pb2.MICROGRID_STATUS_UNSPECIFIED + """The status is unspecified. This should not be used.""" + + ACTIVE = microgrid_pb2.MICROGRID_STATUS_ACTIVE + """The microgrid is active.""" + + INACTIVE = microgrid_pb2.MICROGRID_STATUS_INACTIVE + """The microgrid is inactive.""" + + +@dataclass(frozen=True, kw_only=True) +class MicrogridInfo: + """A localized grouping of electricity generation, energy storage, and loads. + + A microgrid is a localized grouping of electricity generation, energy storage, and + loads that normally operates connected to a traditional centralized grid. + + Each microgrid has a unique identifier and is associated with an enterprise account. + + A key feature is that it has a physical location and is situated in a delivery area. + + Note: Key Concepts + - Physical Location: Geographical coordinates specify the exact physical + location of the microgrid. + - Delivery Area: Each microgrid is part of a broader delivery area, which is + crucial for energy trading and compliance. + """ + + id: MicrogridId + """The unique identifier of the microgrid.""" + + enterprise_id: EnterpriseId + """The unique identifier linking this microgrid to its parent enterprise account.""" + + name: str | None + """Name of the microgrid.""" + + delivery_area: DeliveryArea | None + """The delivery area where the microgrid is located, as identified by a specific code.""" + + location: Location | None + """Physical location of the microgrid, in geographical co-ordinates.""" + + status: MicrogridStatus | int + """The current status of the microgrid.""" + + create_timestamp: datetime.datetime + """The UTC timestamp indicating when the microgrid was initially created.""" + + @cached_property + def is_active(self) -> bool: + """Whether the microgrid is active.""" + if self.status is MicrogridStatus.UNSPECIFIED: + # Because this is a cached property, the warning will only be logged once. + _logger.warning( + "Microgrid %s has an unspecified status. Assuming it is active.", self + ) + return self.status in (MicrogridStatus.ACTIVE, MicrogridStatus.UNSPECIFIED) + + def __str__(self) -> str: + """Return the ID of this microgrid as a string.""" + name = f":{self.name}" if self.name else "" + return f"{self.id}{name}" diff --git a/src/frequenz/client/common/microgrid/_microgrid_info_proto.py b/src/frequenz/client/common/microgrid/_microgrid_info_proto.py new file mode 100644 index 00000000..2404719d --- /dev/null +++ b/src/frequenz/client/common/microgrid/_microgrid_info_proto.py @@ -0,0 +1,78 @@ +# License: MIT +# Copyright © 2025 Frequenz Energy-as-a-Service GmbH + +"""Loading of MicrogridInfo objects from protobuf messages.""" + +import logging + +from frequenz.api.common.v1alpha8.microgrid import microgrid_pb2 +from frequenz.client.base import conversion +from frequenz.client.common.microgrid import EnterpriseId, MicrogridId + +from ._delivery_area import DeliveryArea +from ._delivery_area_proto import delivery_area_from_proto +from ._location import Location +from ._location_proto import location_from_proto +from ._microgrid_info import MicrogridInfo, MicrogridStatus +from ._util import enum_from_proto + +_logger = logging.getLogger(__name__) + + +def microgrid_info_from_proto(message: microgrid_pb2.Microgrid) -> MicrogridInfo: + """Convert a protobuf microgrid message to a microgrid object. + + Args: + message: The protobuf message to convert. + + Returns: + The resulting microgrid object. + """ + major_issues: list[str] = [] + minor_issues: list[str] = [] + + delivery_area: DeliveryArea | None = None + if message.HasField("delivery_area"): + delivery_area = delivery_area_from_proto(message.delivery_area) + else: + major_issues.append("delivery_area is missing") + + location: Location | None = None + if message.HasField("location"): + location = location_from_proto(message.location) + else: + major_issues.append("location is missing") + + name = message.name or None + if name is None: + minor_issues.append("name is empty") + + status = enum_from_proto(message.status, MicrogridStatus) + if status is MicrogridStatus.UNSPECIFIED: + major_issues.append("status is unspecified") + elif isinstance(status, int): + major_issues.append("status is unrecognized") + + if major_issues: + _logger.warning( + "Found issues in microgrid: %s | Protobuf message:\n%s", + ", ".join(major_issues), + message, + ) + + if minor_issues: + _logger.debug( + "Found minor issues in microgrid: %s | Protobuf message:\n%s", + ", ".join(minor_issues), + message, + ) + + return MicrogridInfo( + id=MicrogridId(message.id), + enterprise_id=EnterpriseId(message.enterprise_id), + name=message.name or None, + delivery_area=delivery_area, + location=location, + status=status, + create_timestamp=conversion.to_datetime(message.create_timestamp), + ) diff --git a/tests/microgrid/test_microgrid_info.py b/tests/microgrid/test_microgrid_info.py new file mode 100644 index 00000000..b1f27f32 --- /dev/null +++ b/tests/microgrid/test_microgrid_info.py @@ -0,0 +1,317 @@ +# License: MIT +# Copyright © 2025 Frequenz Energy-as-a-Service GmbH + +"""Tests for MicrogridInfo class.""" + +from dataclasses import dataclass +from datetime import datetime, timezone +from unittest.mock import Mock, patch + +import pytest +from frequenz.api.common.v1alpha8.grid import delivery_area_pb2 +from frequenz.api.common.v1alpha8.microgrid import microgrid_pb2 +from frequenz.client.common.microgrid import EnterpriseId, MicrogridId + +from frequenz.client.microgrid import ( + DeliveryArea, + EnergyMarketCodeType, + Location, + MicrogridInfo, + MicrogridStatus, +) +from frequenz.client.microgrid._microgrid_info_proto import microgrid_info_from_proto + + +@dataclass(frozen=True, kw_only=True) +class _ProtoConversionTestCase: + """Test case for protobuf conversion.""" + + name: str + """Description of the test case.""" + + has_delivery_area: bool + """Whether to include delivery area in the protobuf message.""" + + has_location: bool + """Whether to include location in the protobuf message.""" + + has_name: bool + """Whether to include name in the protobuf message.""" + + status: MicrogridStatus | int + """The status to set in the protobuf message.""" + + expected_log: tuple[str, str] | None = None + """Whether to expect a log during conversion (level, message).""" + + +def test_creation() -> None: + """Test MicrogridInfo creation with all fields.""" + now = datetime.now(timezone.utc) + info = MicrogridInfo( + id=MicrogridId(1234), + enterprise_id=EnterpriseId(5678), + name="Test Microgrid", + delivery_area=DeliveryArea( + code="DE123", code_type=EnergyMarketCodeType.EUROPE_EIC + ), + location=Location(latitude=52.52, longitude=13.405, country_code="DE"), + status=MicrogridStatus.ACTIVE, + create_timestamp=now, + ) + + assert info.id == MicrogridId(1234) + assert info.enterprise_id == EnterpriseId(5678) + assert info.name == "Test Microgrid" + assert info.delivery_area is not None + assert info.delivery_area.code == "DE123" + assert info.delivery_area.code_type == EnergyMarketCodeType.EUROPE_EIC + assert info.location is not None + assert info.location.latitude is not None + assert info.location.latitude == pytest.approx(52.52) + assert info.location.longitude is not None + assert info.location.longitude == pytest.approx(13.405) + assert info.location.country_code == "DE" + assert info.status == MicrogridStatus.ACTIVE + assert info.create_timestamp == now + assert info.is_active is True + + +def test_creation_without_optionals() -> None: + """Test MicrogridInfo creation with only required fields.""" + now = datetime.now(timezone.utc) + info = MicrogridInfo( + id=MicrogridId(1234), + enterprise_id=EnterpriseId(5678), + name=None, + delivery_area=None, + location=None, + status=MicrogridStatus.ACTIVE, + create_timestamp=now, + ) + + assert info.id == MicrogridId(1234) + assert info.enterprise_id == EnterpriseId(5678) + assert info.name is None + assert info.delivery_area is None + assert info.location is None + assert info.status == MicrogridStatus.ACTIVE + assert info.create_timestamp == now + assert info.is_active is True + + +@pytest.mark.parametrize( + "status,expected_active", + [ + pytest.param(MicrogridStatus.ACTIVE, True, id="ACTIVE"), + pytest.param(MicrogridStatus.INACTIVE, False, id="INACTIVE"), + pytest.param(MicrogridStatus.UNSPECIFIED, True, id="UNSPECIFIED"), + ], +) +def test_is_active_property(status: MicrogridStatus, expected_active: bool) -> None: + """Test the is_active property for different status values.""" + now = datetime.now(timezone.utc) + info = MicrogridInfo( + id=MicrogridId(1234), + enterprise_id=EnterpriseId(5678), + name=None, + delivery_area=None, + location=None, + status=status, + create_timestamp=now, + ) + assert info.is_active is expected_active + + +@pytest.mark.parametrize( + "name,expected_str", + [ + pytest.param("Test Grid", "MID1234:Test Grid", id="with-name"), + pytest.param(None, "MID1234", id="none-name"), + pytest.param("", "MID1234", id="empty-name"), + ], +) +def test_str(name: str | None, expected_str: str) -> None: + """Test string representation of MicrogridInfo.""" + now = datetime.now(timezone.utc) + info = MicrogridInfo( + id=MicrogridId(1234), + enterprise_id=EnterpriseId(5678), + name=name, + delivery_area=None, + location=None, + status=MicrogridStatus.ACTIVE, + create_timestamp=now, + ) + assert str(info) == expected_str + + +@pytest.mark.parametrize( + "case", + [ + _ProtoConversionTestCase( + name="full", + has_delivery_area=True, + has_location=True, + has_name=True, + status=MicrogridStatus.ACTIVE, + ), + _ProtoConversionTestCase( + name="no_delivery_area", + has_delivery_area=False, + has_location=True, + has_name=True, + status=MicrogridStatus.ACTIVE, + expected_log=( + "WARNING", + "Found issues in microgrid: delivery_area is missing", + ), + ), + _ProtoConversionTestCase( + name="no_location", + has_delivery_area=True, + has_location=False, + has_name=True, + status=MicrogridStatus.ACTIVE, + expected_log=("WARNING", "Found issues in microgrid: location is missing"), + ), + _ProtoConversionTestCase( + name="empty_name", + has_delivery_area=True, + has_location=True, + has_name=False, + status=MicrogridStatus.ACTIVE, + expected_log=("DEBUG", "Found minor issues in microgrid: name is empty"), + ), + _ProtoConversionTestCase( + name="unspecified_status", + has_delivery_area=True, + has_location=True, + has_name=True, + status=MicrogridStatus.UNSPECIFIED, + expected_log=( + "WARNING", + "Found issues in microgrid: status is unspecified", + ), + ), + _ProtoConversionTestCase( + name="unrecognized_status", + has_delivery_area=True, + has_location=True, + has_name=True, + status=999, # Unknown status value + expected_log=( + "WARNING", + "Found issues in microgrid: status is unrecognized", + ), + ), + ], + ids=lambda case: case.name, +) +@patch("frequenz.client.microgrid._microgrid_info_proto.delivery_area_from_proto") +@patch("frequenz.client.microgrid._microgrid_info_proto.location_from_proto") +@patch("frequenz.client.microgrid._microgrid_info_proto.enum_from_proto") +@patch("frequenz.client.microgrid._microgrid_info_proto.conversion.to_datetime") +# pylint: disable-next=too-many-arguments,too-many-positional-arguments,too-many-branches +def test_from_proto( + mock_to_datetime: Mock, + mock_enum_from_proto: Mock, + mock_location_from_proto: Mock, + mock_delivery_area_from_proto: Mock, + caplog: pytest.LogCaptureFixture, + case: _ProtoConversionTestCase, +) -> None: + """Test conversion from protobuf message to MicrogridInfo.""" + now = datetime.now(timezone.utc) + mock_to_datetime.return_value = now + + if isinstance(case.status, MicrogridStatus): + mock_enum_from_proto.return_value = case.status + else: + mock_enum_from_proto.return_value = case.status + + mock_location = ( + Location( + latitude=52.52, + longitude=13.405, + country_code="DE", + ) + if case.has_location + else None + ) + mock_location_from_proto.return_value = mock_location + + mock_delivery_area = ( + DeliveryArea( + code="DE123", + code_type=EnergyMarketCodeType.EUROPE_EIC, + ) + if case.has_delivery_area + else None + ) + mock_delivery_area_from_proto.return_value = mock_delivery_area + + proto = microgrid_pb2.Microgrid( + id=1234, + enterprise_id=5678, + name="Test Grid" if case.has_name else "", + # We use a ignore because we want to pass an arbitrary int here + status=( + case.status.value # type: ignore[arg-type] + if isinstance(case.status, MicrogridStatus) + else case.status + ), + ) + + # Add optional fields if needed + if case.has_delivery_area: + proto.delivery_area.code = "DE123" + proto.delivery_area.code_type = ( + delivery_area_pb2.EnergyMarketCodeType.ENERGY_MARKET_CODE_TYPE_EUROPE_EIC + ) + + if case.has_location: + proto.location.latitude = 52.52 + proto.location.longitude = 13.405 + proto.location.country_code = "DE" + + # Run the conversion + with caplog.at_level("DEBUG"): + info = microgrid_info_from_proto(proto) + + # Verify the result + assert info.id == MicrogridId(1234) + assert info.enterprise_id == EnterpriseId(5678) + assert info.create_timestamp == now + + if case.has_name: + assert info.name == "Test Grid" + else: + assert info.name is None + + # Verify mock calls + mock_to_datetime.assert_called_once_with(proto.create_timestamp) + mock_enum_from_proto.assert_called_once_with(proto.status, MicrogridStatus) + + if case.has_delivery_area: + mock_delivery_area_from_proto.assert_called_once_with(proto.delivery_area) + assert info.delivery_area == mock_delivery_area + else: + mock_delivery_area_from_proto.assert_not_called() + assert info.delivery_area is None + + if case.has_location: + mock_location_from_proto.assert_called_once_with(proto.location) + assert info.location == mock_location + else: + mock_location_from_proto.assert_not_called() + assert info.location is None + + # Verify logging behavior + if case.expected_log: + expected_level, expected_message = case.expected_log + assert len(caplog.records) == 1 + assert caplog.records[0].levelname == expected_level + assert expected_message in caplog.records[0].message + else: + assert len(caplog.records) == 0 From 696bfa644deebe03aa3e4e0eaea3b993793ffebe Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Wed, 10 Jun 2026 12:07:07 +0000 Subject: [PATCH 2/8] Rename to match the protobuf files In the microgrid client this was named `MicrogridInfo` because at some point we planned to rename the protobuf message like that. That never happen, so we rename everything to match the `microgrid_pb2` protobuf module: the modules become `_microgrid`, the `MicrogridInfo` class becomes `Microgrid` (the protobuf message name) and `microgrid_info_from_proto` becomes `microgrid_from_proto`. This follows the same convention as the other domains (e.g. `DeliveryArea` <-> `delivery_area_pb2`, `Location` <-> `location_pb2`). Signed-off-by: Leandro Lucarella --- .../{_microgrid_info.py => _microgrid.py} | 2 +- ...grid_info_proto.py => _microgrid_proto.py} | 8 ++--- ...st_microgrid_info.py => test_microgrid.py} | 32 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) rename src/frequenz/client/common/microgrid/{_microgrid_info.py => _microgrid.py} (99%) rename src/frequenz/client/common/microgrid/{_microgrid_info_proto.py => _microgrid_proto.py} (90%) rename tests/microgrid/{test_microgrid_info.py => test_microgrid.py} (91%) diff --git a/src/frequenz/client/common/microgrid/_microgrid_info.py b/src/frequenz/client/common/microgrid/_microgrid.py similarity index 99% rename from src/frequenz/client/common/microgrid/_microgrid_info.py rename to src/frequenz/client/common/microgrid/_microgrid.py index 8fbe328d..2acf3c2c 100644 --- a/src/frequenz/client/common/microgrid/_microgrid_info.py +++ b/src/frequenz/client/common/microgrid/_microgrid.py @@ -33,7 +33,7 @@ class MicrogridStatus(enum.Enum): @dataclass(frozen=True, kw_only=True) -class MicrogridInfo: +class Microgrid: """A localized grouping of electricity generation, energy storage, and loads. A microgrid is a localized grouping of electricity generation, energy storage, and diff --git a/src/frequenz/client/common/microgrid/_microgrid_info_proto.py b/src/frequenz/client/common/microgrid/_microgrid_proto.py similarity index 90% rename from src/frequenz/client/common/microgrid/_microgrid_info_proto.py rename to src/frequenz/client/common/microgrid/_microgrid_proto.py index 2404719d..5ee62978 100644 --- a/src/frequenz/client/common/microgrid/_microgrid_info_proto.py +++ b/src/frequenz/client/common/microgrid/_microgrid_proto.py @@ -1,7 +1,7 @@ # License: MIT # Copyright © 2025 Frequenz Energy-as-a-Service GmbH -"""Loading of MicrogridInfo objects from protobuf messages.""" +"""Loading of Microgrid objects from protobuf messages.""" import logging @@ -13,13 +13,13 @@ from ._delivery_area_proto import delivery_area_from_proto from ._location import Location from ._location_proto import location_from_proto -from ._microgrid_info import MicrogridInfo, MicrogridStatus +from ._microgrid import Microgrid, MicrogridStatus from ._util import enum_from_proto _logger = logging.getLogger(__name__) -def microgrid_info_from_proto(message: microgrid_pb2.Microgrid) -> MicrogridInfo: +def microgrid_from_proto(message: microgrid_pb2.Microgrid) -> Microgrid: """Convert a protobuf microgrid message to a microgrid object. Args: @@ -67,7 +67,7 @@ def microgrid_info_from_proto(message: microgrid_pb2.Microgrid) -> MicrogridInfo message, ) - return MicrogridInfo( + return Microgrid( id=MicrogridId(message.id), enterprise_id=EnterpriseId(message.enterprise_id), name=message.name or None, diff --git a/tests/microgrid/test_microgrid_info.py b/tests/microgrid/test_microgrid.py similarity index 91% rename from tests/microgrid/test_microgrid_info.py rename to tests/microgrid/test_microgrid.py index b1f27f32..fea09c55 100644 --- a/tests/microgrid/test_microgrid_info.py +++ b/tests/microgrid/test_microgrid.py @@ -1,7 +1,7 @@ # License: MIT # Copyright © 2025 Frequenz Energy-as-a-Service GmbH -"""Tests for MicrogridInfo class.""" +"""Tests for Microgrid class.""" from dataclasses import dataclass from datetime import datetime, timezone @@ -16,10 +16,10 @@ DeliveryArea, EnergyMarketCodeType, Location, - MicrogridInfo, + Microgrid, MicrogridStatus, ) -from frequenz.client.microgrid._microgrid_info_proto import microgrid_info_from_proto +from frequenz.client.microgrid._microgrid_proto import microgrid_from_proto @dataclass(frozen=True, kw_only=True) @@ -46,9 +46,9 @@ class _ProtoConversionTestCase: def test_creation() -> None: - """Test MicrogridInfo creation with all fields.""" + """Test Microgrid creation with all fields.""" now = datetime.now(timezone.utc) - info = MicrogridInfo( + info = Microgrid( id=MicrogridId(1234), enterprise_id=EnterpriseId(5678), name="Test Microgrid", @@ -78,9 +78,9 @@ def test_creation() -> None: def test_creation_without_optionals() -> None: - """Test MicrogridInfo creation with only required fields.""" + """Test Microgrid creation with only required fields.""" now = datetime.now(timezone.utc) - info = MicrogridInfo( + info = Microgrid( id=MicrogridId(1234), enterprise_id=EnterpriseId(5678), name=None, @@ -111,7 +111,7 @@ def test_creation_without_optionals() -> None: def test_is_active_property(status: MicrogridStatus, expected_active: bool) -> None: """Test the is_active property for different status values.""" now = datetime.now(timezone.utc) - info = MicrogridInfo( + info = Microgrid( id=MicrogridId(1234), enterprise_id=EnterpriseId(5678), name=None, @@ -132,9 +132,9 @@ def test_is_active_property(status: MicrogridStatus, expected_active: bool) -> N ], ) def test_str(name: str | None, expected_str: str) -> None: - """Test string representation of MicrogridInfo.""" + """Test string representation of Microgrid.""" now = datetime.now(timezone.utc) - info = MicrogridInfo( + info = Microgrid( id=MicrogridId(1234), enterprise_id=EnterpriseId(5678), name=name, @@ -208,10 +208,10 @@ def test_str(name: str | None, expected_str: str) -> None: ], ids=lambda case: case.name, ) -@patch("frequenz.client.microgrid._microgrid_info_proto.delivery_area_from_proto") -@patch("frequenz.client.microgrid._microgrid_info_proto.location_from_proto") -@patch("frequenz.client.microgrid._microgrid_info_proto.enum_from_proto") -@patch("frequenz.client.microgrid._microgrid_info_proto.conversion.to_datetime") +@patch("frequenz.client.microgrid._microgrid_proto.delivery_area_from_proto") +@patch("frequenz.client.microgrid._microgrid_proto.location_from_proto") +@patch("frequenz.client.microgrid._microgrid_proto.enum_from_proto") +@patch("frequenz.client.microgrid._microgrid_proto.conversion.to_datetime") # pylint: disable-next=too-many-arguments,too-many-positional-arguments,too-many-branches def test_from_proto( mock_to_datetime: Mock, @@ -221,7 +221,7 @@ def test_from_proto( caplog: pytest.LogCaptureFixture, case: _ProtoConversionTestCase, ) -> None: - """Test conversion from protobuf message to MicrogridInfo.""" + """Test conversion from protobuf message to Microgrid.""" now = datetime.now(timezone.utc) mock_to_datetime.return_value = now @@ -277,7 +277,7 @@ def test_from_proto( # Run the conversion with caplog.at_level("DEBUG"): - info = microgrid_info_from_proto(proto) + info = microgrid_from_proto(proto) # Verify the result assert info.id == MicrogridId(1234) From d04549f63c6d586dfba95ab8ff71571af10a330b Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Wed, 10 Jun 2026 12:10:12 +0000 Subject: [PATCH 3/8] Adapt microgrid info module to the common client layout Move the converter from the flat `_microgrid_proto.py` into `microgrid/proto/v1alpha8/_microgrid.py` and add the package `__init__.py` files re-exporting the public symbols. Signed-off-by: Leandro Lucarella --- .../client/common/microgrid/__init__.py | 3 +++ .../client/common/microgrid/_microgrid.py | 7 +++---- .../client/common/microgrid/proto/__init__.py | 4 ++++ .../common/microgrid/proto/v1alpha8/__init__.py | 10 ++++++++++ .../v1alpha8/_microgrid.py} | 17 ++++++++--------- 5 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 src/frequenz/client/common/microgrid/proto/__init__.py create mode 100644 src/frequenz/client/common/microgrid/proto/v1alpha8/__init__.py rename src/frequenz/client/common/microgrid/{_microgrid_proto.py => proto/v1alpha8/_microgrid.py} (81%) diff --git a/src/frequenz/client/common/microgrid/__init__.py b/src/frequenz/client/common/microgrid/__init__.py index ff7cd25b..b010f6a4 100644 --- a/src/frequenz/client/common/microgrid/__init__.py +++ b/src/frequenz/client/common/microgrid/__init__.py @@ -4,8 +4,11 @@ """Frequenz microgrid definition.""" from ._ids import EnterpriseId, MicrogridId +from ._microgrid import Microgrid, MicrogridStatus __all__ = [ "EnterpriseId", + "Microgrid", "MicrogridId", + "MicrogridStatus", ] diff --git a/src/frequenz/client/common/microgrid/_microgrid.py b/src/frequenz/client/common/microgrid/_microgrid.py index 2acf3c2c..7ce21dd1 100644 --- a/src/frequenz/client/common/microgrid/_microgrid.py +++ b/src/frequenz/client/common/microgrid/_microgrid.py @@ -10,10 +10,9 @@ from functools import cached_property from frequenz.api.common.v1alpha8.microgrid import microgrid_pb2 -from frequenz.client.common.microgrid import EnterpriseId, MicrogridId - -from ._delivery_area import DeliveryArea -from ._location import Location +from ..grid._delivery_area import DeliveryArea +from ..types._location import Location +from ._ids import EnterpriseId, MicrogridId _logger = logging.getLogger(__name__) diff --git a/src/frequenz/client/common/microgrid/proto/__init__.py b/src/frequenz/client/common/microgrid/proto/__init__.py new file mode 100644 index 00000000..e1eef33e --- /dev/null +++ b/src/frequenz/client/common/microgrid/proto/__init__.py @@ -0,0 +1,4 @@ +# License: MIT +# Copyright © 2026 Frequenz Energy-as-a-Service GmbH + +"""Microgrid objects from/to proto conversion functions.""" diff --git a/src/frequenz/client/common/microgrid/proto/v1alpha8/__init__.py b/src/frequenz/client/common/microgrid/proto/v1alpha8/__init__.py new file mode 100644 index 00000000..9c55d6c8 --- /dev/null +++ b/src/frequenz/client/common/microgrid/proto/v1alpha8/__init__.py @@ -0,0 +1,10 @@ +# License: MIT +# Copyright © 2026 Frequenz Energy-as-a-Service GmbH + +"""Conversion of microgrid objects from/to protobuf v1alpha8.""" + +from ._microgrid import microgrid_from_proto + +__all__ = [ + "microgrid_from_proto", +] diff --git a/src/frequenz/client/common/microgrid/_microgrid_proto.py b/src/frequenz/client/common/microgrid/proto/v1alpha8/_microgrid.py similarity index 81% rename from src/frequenz/client/common/microgrid/_microgrid_proto.py rename to src/frequenz/client/common/microgrid/proto/v1alpha8/_microgrid.py index 5ee62978..664664f8 100644 --- a/src/frequenz/client/common/microgrid/_microgrid_proto.py +++ b/src/frequenz/client/common/microgrid/proto/v1alpha8/_microgrid.py @@ -6,15 +6,14 @@ import logging from frequenz.api.common.v1alpha8.microgrid import microgrid_pb2 -from frequenz.client.base import conversion -from frequenz.client.common.microgrid import EnterpriseId, MicrogridId -from ._delivery_area import DeliveryArea -from ._delivery_area_proto import delivery_area_from_proto -from ._location import Location -from ._location_proto import location_from_proto -from ._microgrid import Microgrid, MicrogridStatus -from ._util import enum_from_proto +from ....grid import DeliveryArea +from ....grid.proto.v1alpha8 import delivery_area_from_proto +from ....proto import datetime_from_proto, enum_from_proto +from ....types import Location +from ....types.proto.v1alpha8 import location_from_proto +from ..._ids import EnterpriseId, MicrogridId +from ..._microgrid import Microgrid, MicrogridStatus _logger = logging.getLogger(__name__) @@ -74,5 +73,5 @@ def microgrid_from_proto(message: microgrid_pb2.Microgrid) -> Microgrid: delivery_area=delivery_area, location=location, status=status, - create_timestamp=conversion.to_datetime(message.create_timestamp), + create_timestamp=datetime_from_proto(message.create_timestamp), ) From e9d072310e20cd8f691ba370bad59a40d8a582f4 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Wed, 10 Jun 2026 12:18:46 +0000 Subject: [PATCH 4/8] Adapt microgrid info tests to the common client layout Split the imported test into a pure-type test (`tests/microgrid/`) and a protobuf conversion test (`tests/microgrid/proto/v1alpha8/`), mirroring the source tree, and import from `frequenz.client.common`. The conversion test patches the converters in their new location and mocks the local `datetime_from_proto` helper instead of `conversion.to_datetime`. Signed-off-by: Leandro Lucarella --- tests/microgrid/proto/__init__.py | 4 + tests/microgrid/proto/v1alpha8/__init__.py | 4 + .../proto/v1alpha8/test_microgrid.py | 209 +++++++++++++++++ tests/microgrid/test_microgrid.py | 211 +----------------- 4 files changed, 223 insertions(+), 205 deletions(-) create mode 100644 tests/microgrid/proto/__init__.py create mode 100644 tests/microgrid/proto/v1alpha8/__init__.py create mode 100644 tests/microgrid/proto/v1alpha8/test_microgrid.py diff --git a/tests/microgrid/proto/__init__.py b/tests/microgrid/proto/__init__.py new file mode 100644 index 00000000..50e6184d --- /dev/null +++ b/tests/microgrid/proto/__init__.py @@ -0,0 +1,4 @@ +# License: MIT +# Copyright © 2026 Frequenz Energy-as-a-Service GmbH + +"""Tests for the microgrid.proto package.""" diff --git a/tests/microgrid/proto/v1alpha8/__init__.py b/tests/microgrid/proto/v1alpha8/__init__.py new file mode 100644 index 00000000..4c368eb0 --- /dev/null +++ b/tests/microgrid/proto/v1alpha8/__init__.py @@ -0,0 +1,4 @@ +# License: MIT +# Copyright © 2026 Frequenz Energy-as-a-Service GmbH + +"""Tests for the microgrid.proto.v1alpha8 package.""" diff --git a/tests/microgrid/proto/v1alpha8/test_microgrid.py b/tests/microgrid/proto/v1alpha8/test_microgrid.py new file mode 100644 index 00000000..d34b589a --- /dev/null +++ b/tests/microgrid/proto/v1alpha8/test_microgrid.py @@ -0,0 +1,209 @@ +# License: MIT +# Copyright © 2025 Frequenz Energy-as-a-Service GmbH + +"""Tests for the Microgrid protobuf conversion.""" + +from dataclasses import dataclass +from datetime import datetime, timezone +from unittest.mock import Mock, patch + +import pytest +from frequenz.api.common.v1alpha8.grid import delivery_area_pb2 +from frequenz.api.common.v1alpha8.microgrid import microgrid_pb2 + +from frequenz.client.common.grid import DeliveryArea, EnergyMarketCodeType +from frequenz.client.common.microgrid import EnterpriseId, MicrogridId, MicrogridStatus +from frequenz.client.common.microgrid.proto.v1alpha8 import microgrid_from_proto +from frequenz.client.common.types import Location + + +@dataclass(frozen=True, kw_only=True) +class _ProtoConversionTestCase: + """Test case for protobuf conversion.""" + + name: str + """Description of the test case.""" + + has_delivery_area: bool + """Whether to include delivery area in the protobuf message.""" + + has_location: bool + """Whether to include location in the protobuf message.""" + + has_name: bool + """Whether to include name in the protobuf message.""" + + status: MicrogridStatus | int + """The status to set in the protobuf message.""" + + expected_log: tuple[str, str] | None = None + """Whether to expect a log during conversion (level, message).""" + + +@pytest.mark.parametrize( + "case", + [ + _ProtoConversionTestCase( + name="full", + has_delivery_area=True, + has_location=True, + has_name=True, + status=MicrogridStatus.ACTIVE, + ), + _ProtoConversionTestCase( + name="no_delivery_area", + has_delivery_area=False, + has_location=True, + has_name=True, + status=MicrogridStatus.ACTIVE, + expected_log=( + "WARNING", + "Found issues in microgrid: delivery_area is missing", + ), + ), + _ProtoConversionTestCase( + name="no_location", + has_delivery_area=True, + has_location=False, + has_name=True, + status=MicrogridStatus.ACTIVE, + expected_log=("WARNING", "Found issues in microgrid: location is missing"), + ), + _ProtoConversionTestCase( + name="empty_name", + has_delivery_area=True, + has_location=True, + has_name=False, + status=MicrogridStatus.ACTIVE, + expected_log=("DEBUG", "Found minor issues in microgrid: name is empty"), + ), + _ProtoConversionTestCase( + name="unspecified_status", + has_delivery_area=True, + has_location=True, + has_name=True, + status=MicrogridStatus.UNSPECIFIED, + expected_log=( + "WARNING", + "Found issues in microgrid: status is unspecified", + ), + ), + _ProtoConversionTestCase( + name="unrecognized_status", + has_delivery_area=True, + has_location=True, + has_name=True, + status=999, # Unknown status value + expected_log=( + "WARNING", + "Found issues in microgrid: status is unrecognized", + ), + ), + ], + ids=lambda case: case.name, +) +@patch( + "frequenz.client.common.microgrid.proto.v1alpha8._microgrid.delivery_area_from_proto" +) +@patch("frequenz.client.common.microgrid.proto.v1alpha8._microgrid.location_from_proto") +@patch("frequenz.client.common.microgrid.proto.v1alpha8._microgrid.enum_from_proto") +@patch("frequenz.client.common.microgrid.proto.v1alpha8._microgrid.datetime_from_proto") +# pylint: disable-next=too-many-arguments,too-many-positional-arguments,too-many-branches +def test_from_proto( + mock_datetime_from_proto: Mock, + mock_enum_from_proto: Mock, + mock_location_from_proto: Mock, + mock_delivery_area_from_proto: Mock, + caplog: pytest.LogCaptureFixture, + case: _ProtoConversionTestCase, +) -> None: + """Test conversion from protobuf message to Microgrid.""" + now = datetime.now(timezone.utc) + mock_datetime_from_proto.return_value = now + + mock_enum_from_proto.return_value = case.status + + mock_location = ( + Location( + latitude=52.52, + longitude=13.405, + country_code="DE", + ) + if case.has_location + else None + ) + mock_location_from_proto.return_value = mock_location + + mock_delivery_area = ( + DeliveryArea( + code="DE123", + code_type=EnergyMarketCodeType.EUROPE_EIC, + ) + if case.has_delivery_area + else None + ) + mock_delivery_area_from_proto.return_value = mock_delivery_area + + proto_status = microgrid_pb2.MicrogridStatus.ValueType( + case.status.value if isinstance(case.status, MicrogridStatus) else case.status + ) + + proto = microgrid_pb2.Microgrid( + id=1234, + enterprise_id=5678, + name="Test Grid" if case.has_name else "", + status=proto_status, + ) + + # Add optional fields if needed + if case.has_delivery_area: + proto.delivery_area.code = "DE123" + proto.delivery_area.code_type = ( + delivery_area_pb2.EnergyMarketCodeType.ENERGY_MARKET_CODE_TYPE_EUROPE_EIC + ) + + if case.has_location: + proto.location.latitude = 52.52 + proto.location.longitude = 13.405 + proto.location.country_code = "DE" + + # Run the conversion + with caplog.at_level("DEBUG"): + info = microgrid_from_proto(proto) + + # Verify the result + assert info.id == MicrogridId(1234) + assert info.enterprise_id == EnterpriseId(5678) + assert info.create_timestamp == now + + if case.has_name: + assert info.name == "Test Grid" + else: + assert info.name is None + + # Verify mock calls + mock_datetime_from_proto.assert_called_once_with(proto.create_timestamp) + mock_enum_from_proto.assert_called_once_with(proto.status, MicrogridStatus) + + if case.has_delivery_area: + mock_delivery_area_from_proto.assert_called_once_with(proto.delivery_area) + assert info.delivery_area == mock_delivery_area + else: + mock_delivery_area_from_proto.assert_not_called() + assert info.delivery_area is None + + if case.has_location: + mock_location_from_proto.assert_called_once_with(proto.location) + assert info.location == mock_location + else: + mock_location_from_proto.assert_not_called() + assert info.location is None + + # Verify logging behavior + if case.expected_log: + expected_level, expected_message = case.expected_log + assert len(caplog.records) == 1 + assert caplog.records[0].levelname == expected_level + assert expected_message in caplog.records[0].message + else: + assert len(caplog.records) == 0 diff --git a/tests/microgrid/test_microgrid.py b/tests/microgrid/test_microgrid.py index fea09c55..012c04c4 100644 --- a/tests/microgrid/test_microgrid.py +++ b/tests/microgrid/test_microgrid.py @@ -1,48 +1,20 @@ # License: MIT # Copyright © 2025 Frequenz Energy-as-a-Service GmbH -"""Tests for Microgrid class.""" +"""Tests for the Microgrid type.""" -from dataclasses import dataclass from datetime import datetime, timezone -from unittest.mock import Mock, patch import pytest -from frequenz.api.common.v1alpha8.grid import delivery_area_pb2 -from frequenz.api.common.v1alpha8.microgrid import microgrid_pb2 -from frequenz.client.common.microgrid import EnterpriseId, MicrogridId -from frequenz.client.microgrid import ( - DeliveryArea, - EnergyMarketCodeType, - Location, +from frequenz.client.common.grid import DeliveryArea, EnergyMarketCodeType +from frequenz.client.common.microgrid import ( + EnterpriseId, Microgrid, + MicrogridId, MicrogridStatus, ) -from frequenz.client.microgrid._microgrid_proto import microgrid_from_proto - - -@dataclass(frozen=True, kw_only=True) -class _ProtoConversionTestCase: - """Test case for protobuf conversion.""" - - name: str - """Description of the test case.""" - - has_delivery_area: bool - """Whether to include delivery area in the protobuf message.""" - - has_location: bool - """Whether to include location in the protobuf message.""" - - has_name: bool - """Whether to include name in the protobuf message.""" - - status: MicrogridStatus | int - """The status to set in the protobuf message.""" - - expected_log: tuple[str, str] | None = None - """Whether to expect a log during conversion (level, message).""" +from frequenz.client.common.types import Location def test_creation() -> None: @@ -144,174 +116,3 @@ def test_str(name: str | None, expected_str: str) -> None: create_timestamp=now, ) assert str(info) == expected_str - - -@pytest.mark.parametrize( - "case", - [ - _ProtoConversionTestCase( - name="full", - has_delivery_area=True, - has_location=True, - has_name=True, - status=MicrogridStatus.ACTIVE, - ), - _ProtoConversionTestCase( - name="no_delivery_area", - has_delivery_area=False, - has_location=True, - has_name=True, - status=MicrogridStatus.ACTIVE, - expected_log=( - "WARNING", - "Found issues in microgrid: delivery_area is missing", - ), - ), - _ProtoConversionTestCase( - name="no_location", - has_delivery_area=True, - has_location=False, - has_name=True, - status=MicrogridStatus.ACTIVE, - expected_log=("WARNING", "Found issues in microgrid: location is missing"), - ), - _ProtoConversionTestCase( - name="empty_name", - has_delivery_area=True, - has_location=True, - has_name=False, - status=MicrogridStatus.ACTIVE, - expected_log=("DEBUG", "Found minor issues in microgrid: name is empty"), - ), - _ProtoConversionTestCase( - name="unspecified_status", - has_delivery_area=True, - has_location=True, - has_name=True, - status=MicrogridStatus.UNSPECIFIED, - expected_log=( - "WARNING", - "Found issues in microgrid: status is unspecified", - ), - ), - _ProtoConversionTestCase( - name="unrecognized_status", - has_delivery_area=True, - has_location=True, - has_name=True, - status=999, # Unknown status value - expected_log=( - "WARNING", - "Found issues in microgrid: status is unrecognized", - ), - ), - ], - ids=lambda case: case.name, -) -@patch("frequenz.client.microgrid._microgrid_proto.delivery_area_from_proto") -@patch("frequenz.client.microgrid._microgrid_proto.location_from_proto") -@patch("frequenz.client.microgrid._microgrid_proto.enum_from_proto") -@patch("frequenz.client.microgrid._microgrid_proto.conversion.to_datetime") -# pylint: disable-next=too-many-arguments,too-many-positional-arguments,too-many-branches -def test_from_proto( - mock_to_datetime: Mock, - mock_enum_from_proto: Mock, - mock_location_from_proto: Mock, - mock_delivery_area_from_proto: Mock, - caplog: pytest.LogCaptureFixture, - case: _ProtoConversionTestCase, -) -> None: - """Test conversion from protobuf message to Microgrid.""" - now = datetime.now(timezone.utc) - mock_to_datetime.return_value = now - - if isinstance(case.status, MicrogridStatus): - mock_enum_from_proto.return_value = case.status - else: - mock_enum_from_proto.return_value = case.status - - mock_location = ( - Location( - latitude=52.52, - longitude=13.405, - country_code="DE", - ) - if case.has_location - else None - ) - mock_location_from_proto.return_value = mock_location - - mock_delivery_area = ( - DeliveryArea( - code="DE123", - code_type=EnergyMarketCodeType.EUROPE_EIC, - ) - if case.has_delivery_area - else None - ) - mock_delivery_area_from_proto.return_value = mock_delivery_area - - proto = microgrid_pb2.Microgrid( - id=1234, - enterprise_id=5678, - name="Test Grid" if case.has_name else "", - # We use a ignore because we want to pass an arbitrary int here - status=( - case.status.value # type: ignore[arg-type] - if isinstance(case.status, MicrogridStatus) - else case.status - ), - ) - - # Add optional fields if needed - if case.has_delivery_area: - proto.delivery_area.code = "DE123" - proto.delivery_area.code_type = ( - delivery_area_pb2.EnergyMarketCodeType.ENERGY_MARKET_CODE_TYPE_EUROPE_EIC - ) - - if case.has_location: - proto.location.latitude = 52.52 - proto.location.longitude = 13.405 - proto.location.country_code = "DE" - - # Run the conversion - with caplog.at_level("DEBUG"): - info = microgrid_from_proto(proto) - - # Verify the result - assert info.id == MicrogridId(1234) - assert info.enterprise_id == EnterpriseId(5678) - assert info.create_timestamp == now - - if case.has_name: - assert info.name == "Test Grid" - else: - assert info.name is None - - # Verify mock calls - mock_to_datetime.assert_called_once_with(proto.create_timestamp) - mock_enum_from_proto.assert_called_once_with(proto.status, MicrogridStatus) - - if case.has_delivery_area: - mock_delivery_area_from_proto.assert_called_once_with(proto.delivery_area) - assert info.delivery_area == mock_delivery_area - else: - mock_delivery_area_from_proto.assert_not_called() - assert info.delivery_area is None - - if case.has_location: - mock_location_from_proto.assert_called_once_with(proto.location) - assert info.location == mock_location - else: - mock_location_from_proto.assert_not_called() - assert info.location is None - - # Verify logging behavior - if case.expected_log: - expected_level, expected_message = case.expected_log - assert len(caplog.records) == 1 - assert caplog.records[0].levelname == expected_level - assert expected_message in caplog.records[0].message - else: - assert len(caplog.records) == 0 From 0a3c10b9fd0e96859c664704129ccc79c5b63a9b Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Wed, 10 Jun 2026 15:08:31 +0200 Subject: [PATCH 5/8] Add MicrogridStatus proto converters Add versioned converters for `MicrogridStatus`, matching the enum converter pattern used by the other common-client domains. Use `microgrid_status_from_proto()` inside `microgrid_from_proto()` so all enum conversion goes through the public versioned converter. Signed-off-by: Leandro Lucarella --- .../microgrid/proto/v1alpha8/__init__.py | 8 ++++- .../microgrid/proto/v1alpha8/_microgrid.py | 31 ++++++++++++++++++- .../proto/v1alpha8/test_microgrid.py | 10 +++--- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/frequenz/client/common/microgrid/proto/v1alpha8/__init__.py b/src/frequenz/client/common/microgrid/proto/v1alpha8/__init__.py index 9c55d6c8..0d563dda 100644 --- a/src/frequenz/client/common/microgrid/proto/v1alpha8/__init__.py +++ b/src/frequenz/client/common/microgrid/proto/v1alpha8/__init__.py @@ -3,8 +3,14 @@ """Conversion of microgrid objects from/to protobuf v1alpha8.""" -from ._microgrid import microgrid_from_proto +from ._microgrid import ( + microgrid_from_proto, + microgrid_status_from_proto, + microgrid_status_to_proto, +) __all__ = [ "microgrid_from_proto", + "microgrid_status_from_proto", + "microgrid_status_to_proto", ] diff --git a/src/frequenz/client/common/microgrid/proto/v1alpha8/_microgrid.py b/src/frequenz/client/common/microgrid/proto/v1alpha8/_microgrid.py index 664664f8..37d7c905 100644 --- a/src/frequenz/client/common/microgrid/proto/v1alpha8/_microgrid.py +++ b/src/frequenz/client/common/microgrid/proto/v1alpha8/_microgrid.py @@ -18,6 +18,35 @@ _logger = logging.getLogger(__name__) +def microgrid_status_from_proto( + message: microgrid_pb2.MicrogridStatus.ValueType, +) -> MicrogridStatus | int: + """Convert a protobuf MicrogridStatus enum value to a MicrogridStatus enum member. + + Args: + message: A protobuf MicrogridStatus enum value. + + Returns: + The corresponding MicrogridStatus enum member, or the raw `int` if the protobuf + value is not recognized. + """ + return enum_from_proto(message, MicrogridStatus) + + +def microgrid_status_to_proto( + status: MicrogridStatus, +) -> microgrid_pb2.MicrogridStatus.ValueType: + """Convert a MicrogridStatus enum member to a protobuf MicrogridStatus enum value. + + Args: + status: A MicrogridStatus enum member. + + Returns: + The corresponding protobuf MicrogridStatus enum value. + """ + return microgrid_pb2.MicrogridStatus.ValueType(status.value) + + def microgrid_from_proto(message: microgrid_pb2.Microgrid) -> Microgrid: """Convert a protobuf microgrid message to a microgrid object. @@ -46,7 +75,7 @@ def microgrid_from_proto(message: microgrid_pb2.Microgrid) -> Microgrid: if name is None: minor_issues.append("name is empty") - status = enum_from_proto(message.status, MicrogridStatus) + status = microgrid_status_from_proto(message.status) if status is MicrogridStatus.UNSPECIFIED: major_issues.append("status is unspecified") elif isinstance(status, int): diff --git a/tests/microgrid/proto/v1alpha8/test_microgrid.py b/tests/microgrid/proto/v1alpha8/test_microgrid.py index d34b589a..ce0378a7 100644 --- a/tests/microgrid/proto/v1alpha8/test_microgrid.py +++ b/tests/microgrid/proto/v1alpha8/test_microgrid.py @@ -106,12 +106,14 @@ class _ProtoConversionTestCase: "frequenz.client.common.microgrid.proto.v1alpha8._microgrid.delivery_area_from_proto" ) @patch("frequenz.client.common.microgrid.proto.v1alpha8._microgrid.location_from_proto") -@patch("frequenz.client.common.microgrid.proto.v1alpha8._microgrid.enum_from_proto") +@patch( + "frequenz.client.common.microgrid.proto.v1alpha8._microgrid.microgrid_status_from_proto" +) @patch("frequenz.client.common.microgrid.proto.v1alpha8._microgrid.datetime_from_proto") # pylint: disable-next=too-many-arguments,too-many-positional-arguments,too-many-branches def test_from_proto( mock_datetime_from_proto: Mock, - mock_enum_from_proto: Mock, + mock_microgrid_status_from_proto: Mock, mock_location_from_proto: Mock, mock_delivery_area_from_proto: Mock, caplog: pytest.LogCaptureFixture, @@ -121,7 +123,7 @@ def test_from_proto( now = datetime.now(timezone.utc) mock_datetime_from_proto.return_value = now - mock_enum_from_proto.return_value = case.status + mock_microgrid_status_from_proto.return_value = case.status mock_location = ( Location( @@ -183,7 +185,7 @@ def test_from_proto( # Verify mock calls mock_datetime_from_proto.assert_called_once_with(proto.create_timestamp) - mock_enum_from_proto.assert_called_once_with(proto.status, MicrogridStatus) + mock_microgrid_status_from_proto.assert_called_once_with(proto.status) if case.has_delivery_area: mock_delivery_area_from_proto.assert_called_once_with(proto.delivery_area) From 4951c5d7a270f91d445a24f7da33905cf9ee86de Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Wed, 10 Jun 2026 15:05:02 +0200 Subject: [PATCH 6/8] Convert enum members to plain `int`s This is to disallow implicit conversion, ensuring users use the appropriate conversion functions. Signed-off-by: Leandro Lucarella --- src/frequenz/client/common/microgrid/_microgrid.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/frequenz/client/common/microgrid/_microgrid.py b/src/frequenz/client/common/microgrid/_microgrid.py index 7ce21dd1..fc2874e5 100644 --- a/src/frequenz/client/common/microgrid/_microgrid.py +++ b/src/frequenz/client/common/microgrid/_microgrid.py @@ -9,7 +9,6 @@ from dataclasses import dataclass from functools import cached_property -from frequenz.api.common.v1alpha8.microgrid import microgrid_pb2 from ..grid._delivery_area import DeliveryArea from ..types._location import Location from ._ids import EnterpriseId, MicrogridId @@ -21,13 +20,13 @@ class MicrogridStatus(enum.Enum): """The possible statuses for a microgrid.""" - UNSPECIFIED = microgrid_pb2.MICROGRID_STATUS_UNSPECIFIED + UNSPECIFIED = 0 """The status is unspecified. This should not be used.""" - ACTIVE = microgrid_pb2.MICROGRID_STATUS_ACTIVE + ACTIVE = 1 """The microgrid is active.""" - INACTIVE = microgrid_pb2.MICROGRID_STATUS_INACTIVE + INACTIVE = 2 """The microgrid is inactive.""" From 90ea91fbf41763b48bd6633573d73fa96af977b7 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Wed, 10 Jun 2026 12:28:32 +0000 Subject: [PATCH 7/8] Add MicrogridStatus enum parity tests Use the shared `EnumParityTest` scaffold to keep `MicrogridStatus` in lock-step with the generated protobuf enum and its versioned converters. Signed-off-by: Leandro Lucarella --- .../microgrid/proto/v1alpha8/test_microgrid.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/microgrid/proto/v1alpha8/test_microgrid.py b/tests/microgrid/proto/v1alpha8/test_microgrid.py index ce0378a7..63b0761d 100644 --- a/tests/microgrid/proto/v1alpha8/test_microgrid.py +++ b/tests/microgrid/proto/v1alpha8/test_microgrid.py @@ -13,10 +13,25 @@ from frequenz.client.common.grid import DeliveryArea, EnergyMarketCodeType from frequenz.client.common.microgrid import EnterpriseId, MicrogridId, MicrogridStatus -from frequenz.client.common.microgrid.proto.v1alpha8 import microgrid_from_proto +from frequenz.client.common.microgrid.proto.v1alpha8 import ( + microgrid_from_proto, + microgrid_status_from_proto, + microgrid_status_to_proto, +) +from frequenz.client.common.test.enum_parity import EnumParityTest from frequenz.client.common.types import Location +class TestMicrogridStatusParity(EnumParityTest): + """Parity tests for the `MicrogridStatus` enum.""" + + python_enum = MicrogridStatus + proto_enum = microgrid_pb2.MicrogridStatus + name_prefix = "MICROGRID_STATUS_" + from_proto = staticmethod(microgrid_status_from_proto) + to_proto = staticmethod(microgrid_status_to_proto) + + @dataclass(frozen=True, kw_only=True) class _ProtoConversionTestCase: """Test case for protobuf conversion.""" From 244f59682ac988bcdf1abf96e5a9a713eba7fa38 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Wed, 10 Jun 2026 12:29:00 +0000 Subject: [PATCH 8/8] Update release notes Signed-off-by: Leandro Lucarella --- RELEASE_NOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 44a1df94..d384c60d 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -12,6 +12,7 @@ * Added a new `frequenz.client.common.types.Lifetime` type together with the `frequenz.client.common.types.proto.v1alpha8.lifetime_from_proto` conversion function. * Added a new `frequenz.client.common.types.Location` type together with the `frequenz.client.common.types.proto.v1alpha8.location_from_proto` conversion function. +* Added a new `frequenz.client.common.microgrid.Microgrid` type and `MicrogridStatus` enum together with the `frequenz.client.common.microgrid.proto.v1alpha8.microgrid_from_proto`, `microgrid_status_from_proto`, and `microgrid_status_to_proto` conversion functions. ## Bug Fixes