From e4082eac1f3ba736b02cef9156743ace587456db Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 11 Jun 2026 10:10:41 +0000 Subject: [PATCH 1/4] Import inverter component module from the microgrid client Signed-off-by: Leandro Lucarella --- .../electrical_components/_inverter.py | 155 ++++++++++++++++++ .../electrical_components/test_inverter.py | 120 ++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 src/frequenz/client/common/microgrid/electrical_components/_inverter.py create mode 100644 tests/microgrid/electrical_components/test_inverter.py diff --git a/src/frequenz/client/common/microgrid/electrical_components/_inverter.py b/src/frequenz/client/common/microgrid/electrical_components/_inverter.py new file mode 100644 index 00000000..a49395df --- /dev/null +++ b/src/frequenz/client/common/microgrid/electrical_components/_inverter.py @@ -0,0 +1,155 @@ +# License: MIT +# Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +"""Inverter component.""" + +import dataclasses +import enum +from typing import Any, Literal, Self, TypeAlias + +from frequenz.api.common.v1alpha8.microgrid.electrical_components import ( + electrical_components_pb2, +) + +from ._category import ComponentCategory +from ._component import Component + + +@enum.unique +class InverterType(enum.Enum): + """The known types of inverters.""" + + UNSPECIFIED = electrical_components_pb2.INVERTER_TYPE_UNSPECIFIED + """The type of the inverter is unspecified.""" + + BATTERY = electrical_components_pb2.INVERTER_TYPE_BATTERY + """The inverter is a battery inverter.""" + + SOLAR = electrical_components_pb2.INVERTER_TYPE_PV + """The inverter is a solar inverter.""" + + HYBRID = electrical_components_pb2.INVERTER_TYPE_HYBRID + """The inverter is a hybrid inverter.""" + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class Inverter(Component): + """An abstract inverter component.""" + + category: Literal[ComponentCategory.INVERTER] = ComponentCategory.INVERTER + """The category of this component. + + Note: + This should not be used normally, you should test if a component + [`isinstance`][] of a concrete component class instead. + + It is only provided for using with a newer version of the API where the client + doesn't know about a new category yet (i.e. for use with + [`UnrecognizedComponent`][frequenz.client.microgrid.component.UnrecognizedComponent]) + and in case some low level code needs to know the category of a component. + """ + + type: InverterType | int + """The type of this inverter. + + Note: + This should not be used normally, you should test if a inverter + [`isinstance`][] of a concrete inverter class instead. + + It is only provided for using with a newer version of the API where the client + doesn't know about the new inverter type yet (i.e. for use with + [`UnrecognizedInverter`][frequenz.client.microgrid.component.UnrecognizedInverter]). + """ + + # pylint: disable-next=unused-argument + def __new__(cls, *args: Any, **kwargs: Any) -> Self: + """Prevent instantiation of this class.""" + if cls is Inverter: + raise TypeError(f"Cannot instantiate {cls.__name__} directly") + return super().__new__(cls) + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class UnspecifiedInverter(Inverter): + """An inverter of an unspecified type.""" + + type: Literal[InverterType.UNSPECIFIED] = InverterType.UNSPECIFIED + """The type of this inverter. + + Note: + This should not be used normally, you should test if a inverter + [`isinstance`][] of a concrete inverter class instead. + + It is only provided for using with a newer version of the API where the client + doesn't know about the new inverter type yet (i.e. for use with + [`UnrecognizedInverter`][frequenz.client.microgrid.component.UnrecognizedInverter]). + """ + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class BatteryInverter(Inverter): + """A battery inverter.""" + + type: Literal[InverterType.BATTERY] = InverterType.BATTERY + """The type of this inverter. + + Note: + This should not be used normally, you should test if a inverter + [`isinstance`][] of a concrete inverter class instead. + + It is only provided for using with a newer version of the API where the client + doesn't know about the new inverter type yet (i.e. for use with + [`UnrecognizedInverter`][frequenz.client.microgrid.component.UnrecognizedInverter]). + """ + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class SolarInverter(Inverter): + """A solar inverter.""" + + type: Literal[InverterType.SOLAR] = InverterType.SOLAR + """The type of this inverter. + + Note: + This should not be used normally, you should test if a inverter + [`isinstance`][] of a concrete inverter class instead. + + It is only provided for using with a newer version of the API where the client + doesn't know about the new inverter type yet (i.e. for use with + [`UnrecognizedInverter`][frequenz.client.microgrid.component.UnrecognizedInverter]). + """ + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class HybridInverter(Inverter): + """A hybrid inverter.""" + + type: Literal[InverterType.HYBRID] = InverterType.HYBRID + """The type of this inverter. + + Note: + This should not be used normally, you should test if a inverter + [`isinstance`][] of a concrete inverter class instead. + + It is only provided for using with a newer version of the API where the client + doesn't know about the new inverter type yet (i.e. for use with + [`UnrecognizedInverter`][frequenz.client.microgrid.component.UnrecognizedInverter]). + """ + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class UnrecognizedInverter(Inverter): + """An inverter component.""" + + type: int + """The unrecognized type of this inverter.""" + + +InverterTypes: TypeAlias = ( + UnspecifiedInverter + | BatteryInverter + | SolarInverter + | HybridInverter + | UnrecognizedInverter +) +"""All possible inverter types.""" diff --git a/tests/microgrid/electrical_components/test_inverter.py b/tests/microgrid/electrical_components/test_inverter.py new file mode 100644 index 00000000..db1b356b --- /dev/null +++ b/tests/microgrid/electrical_components/test_inverter.py @@ -0,0 +1,120 @@ +# License: MIT +# Copyright © 2025 Frequenz Energy-as-a-Service GmbH + +"""Tests for Inverter components.""" + +import dataclasses + +import pytest +from frequenz.client.common.microgrid import MicrogridId +from frequenz.client.common.microgrid.components import ComponentId + +from frequenz.client.microgrid.component import ( + BatteryInverter, + ComponentCategory, + HybridInverter, + Inverter, + InverterType, + SolarInverter, + UnrecognizedInverter, + UnspecifiedInverter, +) + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class InverterTestCase: + """Test case for Inverter components.""" + + cls: type[UnspecifiedInverter | BatteryInverter | SolarInverter | HybridInverter] + expected_type: InverterType + name: str + + +@pytest.fixture +def component_id() -> ComponentId: + """Provide a test component ID.""" + return ComponentId(42) + + +@pytest.fixture +def microgrid_id() -> MicrogridId: + """Provide a test microgrid ID.""" + return MicrogridId(1) + + +def test_abstract_inverter_cannot_be_instantiated( + component_id: ComponentId, microgrid_id: MicrogridId +) -> None: + """Test that Inverter base class cannot be instantiated.""" + with pytest.raises(TypeError, match="Cannot instantiate Inverter directly"): + Inverter( + id=component_id, + microgrid_id=microgrid_id, + name="test_inverter", + manufacturer="test_manufacturer", + model_name="test_model", + type=InverterType.BATTERY, + ) + + +@pytest.mark.parametrize( + "case", + [ + InverterTestCase( + cls=UnspecifiedInverter, + expected_type=InverterType.UNSPECIFIED, + name="unspecified", + ), + InverterTestCase( + cls=BatteryInverter, expected_type=InverterType.BATTERY, name="battery" + ), + InverterTestCase( + cls=SolarInverter, expected_type=InverterType.SOLAR, name="solar" + ), + InverterTestCase( + cls=HybridInverter, expected_type=InverterType.HYBRID, name="hybrid" + ), + ], + ids=lambda case: case.name, +) +def test_recognized_inverter_types( + case: InverterTestCase, component_id: ComponentId, microgrid_id: MicrogridId +) -> None: + """Test initialization and properties of different recognized inverter types.""" + inverter = case.cls( + id=component_id, + microgrid_id=microgrid_id, + name=case.name, + manufacturer="test_manufacturer", + model_name="test_model", + ) + + assert inverter.id == component_id + assert inverter.microgrid_id == microgrid_id + assert inverter.name == case.name + assert inverter.manufacturer == "test_manufacturer" + assert inverter.model_name == "test_model" + assert inverter.category == ComponentCategory.INVERTER + assert inverter.type == case.expected_type + + +def test_unrecognized_inverter_type( + component_id: ComponentId, microgrid_id: MicrogridId +) -> None: + """Test initialization and properties of unrecognized inverter type.""" + inverter = UnrecognizedInverter( + id=component_id, + microgrid_id=microgrid_id, + name="unrecognized_inverter", + manufacturer="test_manufacturer", + model_name="test_model", + type=999, # type is passed here for UnrecognizedInverter + ) + + assert inverter.id == component_id + assert inverter.microgrid_id == microgrid_id + assert inverter.name == "unrecognized_inverter" + assert inverter.manufacturer == "test_manufacturer" + assert inverter.model_name == "test_model" + assert inverter.category == ComponentCategory.INVERTER + assert inverter.type == 999 From 24ff11593b14f1456bd9e2a2ab32bd4b6f054687 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 11 Jun 2026 10:13:33 +0000 Subject: [PATCH 2/4] Generalize inverter component docstrings Signed-off-by: Leandro Lucarella --- .../electrical_components/_inverter.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/frequenz/client/common/microgrid/electrical_components/_inverter.py b/src/frequenz/client/common/microgrid/electrical_components/_inverter.py index a49395df..f12ae474 100644 --- a/src/frequenz/client/common/microgrid/electrical_components/_inverter.py +++ b/src/frequenz/client/common/microgrid/electrical_components/_inverter.py @@ -1,7 +1,7 @@ # License: MIT # Copyright © 2024 Frequenz Energy-as-a-Service GmbH -"""Inverter component.""" +"""Inverter electrical component.""" import dataclasses import enum @@ -12,7 +12,7 @@ ) from ._category import ComponentCategory -from ._component import Component +from ._electrical_component import ElectricalComponent @enum.unique @@ -33,20 +33,21 @@ class InverterType(enum.Enum): @dataclasses.dataclass(frozen=True, kw_only=True) -class Inverter(Component): - """An abstract inverter component.""" +class Inverter(ElectricalComponent): + """An abstract inverter electrical component.""" category: Literal[ComponentCategory.INVERTER] = ComponentCategory.INVERTER - """The category of this component. + """The category of this electrical component. Note: - This should not be used normally, you should test if a component - [`isinstance`][] of a concrete component class instead. + This should not be used normally, you should test if an electrical component + [`isinstance`][] of a concrete electrical component class instead. It is only provided for using with a newer version of the API where the client doesn't know about a new category yet (i.e. for use with [`UnrecognizedComponent`][frequenz.client.microgrid.component.UnrecognizedComponent]) - and in case some low level code needs to know the category of a component. + and in case some low level code needs to know the category of an electrical + component. """ type: InverterType | int @@ -139,7 +140,7 @@ class HybridInverter(Inverter): @dataclasses.dataclass(frozen=True, kw_only=True) class UnrecognizedInverter(Inverter): - """An inverter component.""" + """An inverter of an unrecognized type.""" type: int """The unrecognized type of this inverter.""" From 734f7a6057fd20e5a93fded7b844b04c9da1e282 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 11 Jun 2026 10:16:42 +0000 Subject: [PATCH 3/4] Adapt inverter component module to the common client layout Signed-off-by: Leandro Lucarella --- .../electrical_components/__init__.py | 18 ++++++++++++++++ .../electrical_components/_inverter.py | 21 ++++++++++--------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/frequenz/client/common/microgrid/electrical_components/__init__.py b/src/frequenz/client/common/microgrid/electrical_components/__init__.py index ce462b36..724ee2ec 100644 --- a/src/frequenz/client/common/microgrid/electrical_components/__init__.py +++ b/src/frequenz/client/common/microgrid/electrical_components/__init__.py @@ -16,6 +16,16 @@ from ._diagnostic_code import ElectricalComponentDiagnosticCode from ._electrical_component import ElectricalComponent from ._ids import ElectricalComponentId +from ._inverter import ( + BatteryInverter, + HybridInverter, + Inverter, + InverterType, + InverterTypes, + SolarInverter, + UnrecognizedInverter, + UnspecifiedInverter, +) from ._problematic import ( MismatchedCategoryComponent, ProblematicComponent, @@ -26,6 +36,7 @@ __all__ = [ "Battery", + "BatteryInverter", "BatteryType", "BatteryTypes", "ElectricalComponent", @@ -33,12 +44,19 @@ "ElectricalComponentDiagnosticCode", "ElectricalComponentId", "ElectricalComponentStateCode", + "HybridInverter", + "Inverter", + "InverterType", + "InverterTypes", "LiIonBattery", "MismatchedCategoryComponent", "NaIonBattery", "ProblematicComponent", + "SolarInverter", "UnrecognizedBattery", "UnrecognizedComponent", + "UnrecognizedInverter", "UnspecifiedBattery", "UnspecifiedComponent", + "UnspecifiedInverter", ] diff --git a/src/frequenz/client/common/microgrid/electrical_components/_inverter.py b/src/frequenz/client/common/microgrid/electrical_components/_inverter.py index f12ae474..3bcc1782 100644 --- a/src/frequenz/client/common/microgrid/electrical_components/_inverter.py +++ b/src/frequenz/client/common/microgrid/electrical_components/_inverter.py @@ -11,7 +11,7 @@ electrical_components_pb2, ) -from ._category import ComponentCategory +from ._category import ElectricalComponentCategory from ._electrical_component import ElectricalComponent @@ -36,7 +36,9 @@ class InverterType(enum.Enum): class Inverter(ElectricalComponent): """An abstract inverter electrical component.""" - category: Literal[ComponentCategory.INVERTER] = ComponentCategory.INVERTER + category: Literal[ElectricalComponentCategory.INVERTER] = ( + ElectricalComponentCategory.INVERTER + ) """The category of this electrical component. Note: @@ -45,9 +47,8 @@ class Inverter(ElectricalComponent): It is only provided for using with a newer version of the API where the client doesn't know about a new category yet (i.e. for use with - [`UnrecognizedComponent`][frequenz.client.microgrid.component.UnrecognizedComponent]) - and in case some low level code needs to know the category of an electrical - component. + [`UnrecognizedComponent`][...UnrecognizedComponent]) and in case some low level + code needs to know the category of an electrical component. """ type: InverterType | int @@ -59,7 +60,7 @@ class Inverter(ElectricalComponent): It is only provided for using with a newer version of the API where the client doesn't know about the new inverter type yet (i.e. for use with - [`UnrecognizedInverter`][frequenz.client.microgrid.component.UnrecognizedInverter]). + [`UnrecognizedInverter`][...UnrecognizedInverter]). """ # pylint: disable-next=unused-argument @@ -83,7 +84,7 @@ class UnspecifiedInverter(Inverter): It is only provided for using with a newer version of the API where the client doesn't know about the new inverter type yet (i.e. for use with - [`UnrecognizedInverter`][frequenz.client.microgrid.component.UnrecognizedInverter]). + [`UnrecognizedInverter`][...UnrecognizedInverter]). """ @@ -100,7 +101,7 @@ class BatteryInverter(Inverter): It is only provided for using with a newer version of the API where the client doesn't know about the new inverter type yet (i.e. for use with - [`UnrecognizedInverter`][frequenz.client.microgrid.component.UnrecognizedInverter]). + [`UnrecognizedInverter`][...UnrecognizedInverter]). """ @@ -117,7 +118,7 @@ class SolarInverter(Inverter): It is only provided for using with a newer version of the API where the client doesn't know about the new inverter type yet (i.e. for use with - [`UnrecognizedInverter`][frequenz.client.microgrid.component.UnrecognizedInverter]). + [`UnrecognizedInverter`][...UnrecognizedInverter]). """ @@ -134,7 +135,7 @@ class HybridInverter(Inverter): It is only provided for using with a newer version of the API where the client doesn't know about the new inverter type yet (i.e. for use with - [`UnrecognizedInverter`][frequenz.client.microgrid.component.UnrecognizedInverter]). + [`UnrecognizedInverter`][...UnrecognizedInverter]). """ From 28839c5a7b76fc8c5df6115b3f58a6fa7d217e0c Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 11 Jun 2026 10:19:23 +0000 Subject: [PATCH 4/4] Adapt inverter component tests to the common client layout Signed-off-by: Leandro Lucarella --- .../electrical_components/test_inverter.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/microgrid/electrical_components/test_inverter.py b/tests/microgrid/electrical_components/test_inverter.py index db1b356b..ccddaa13 100644 --- a/tests/microgrid/electrical_components/test_inverter.py +++ b/tests/microgrid/electrical_components/test_inverter.py @@ -6,12 +6,12 @@ import dataclasses import pytest -from frequenz.client.common.microgrid import MicrogridId -from frequenz.client.common.microgrid.components import ComponentId -from frequenz.client.microgrid.component import ( +from frequenz.client.common.microgrid import MicrogridId +from frequenz.client.common.microgrid.electrical_components import ( BatteryInverter, - ComponentCategory, + ElectricalComponentCategory, + ElectricalComponentId, HybridInverter, Inverter, InverterType, @@ -31,9 +31,9 @@ class InverterTestCase: @pytest.fixture -def component_id() -> ComponentId: +def component_id() -> ElectricalComponentId: """Provide a test component ID.""" - return ComponentId(42) + return ElectricalComponentId(42) @pytest.fixture @@ -43,7 +43,7 @@ def microgrid_id() -> MicrogridId: def test_abstract_inverter_cannot_be_instantiated( - component_id: ComponentId, microgrid_id: MicrogridId + component_id: ElectricalComponentId, microgrid_id: MicrogridId ) -> None: """Test that Inverter base class cannot be instantiated.""" with pytest.raises(TypeError, match="Cannot instantiate Inverter directly"): @@ -78,7 +78,9 @@ def test_abstract_inverter_cannot_be_instantiated( ids=lambda case: case.name, ) def test_recognized_inverter_types( - case: InverterTestCase, component_id: ComponentId, microgrid_id: MicrogridId + case: InverterTestCase, + component_id: ElectricalComponentId, + microgrid_id: MicrogridId, ) -> None: """Test initialization and properties of different recognized inverter types.""" inverter = case.cls( @@ -94,12 +96,12 @@ def test_recognized_inverter_types( assert inverter.name == case.name assert inverter.manufacturer == "test_manufacturer" assert inverter.model_name == "test_model" - assert inverter.category == ComponentCategory.INVERTER + assert inverter.category == ElectricalComponentCategory.INVERTER assert inverter.type == case.expected_type def test_unrecognized_inverter_type( - component_id: ComponentId, microgrid_id: MicrogridId + component_id: ElectricalComponentId, microgrid_id: MicrogridId ) -> None: """Test initialization and properties of unrecognized inverter type.""" inverter = UnrecognizedInverter( @@ -116,5 +118,5 @@ def test_unrecognized_inverter_type( assert inverter.name == "unrecognized_inverter" assert inverter.manufacturer == "test_manufacturer" assert inverter.model_name == "test_model" - assert inverter.category == ComponentCategory.INVERTER + assert inverter.category == ElectricalComponentCategory.INVERTER assert inverter.type == 999