From 9ee328eb3852a376d408af4601023267bfac1d3d Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 11 Jun 2026 10:22:07 +0000 Subject: [PATCH 1/4] Import EV charger component module from the microgrid client Signed-off-by: Leandro Lucarella --- .../electrical_components/_ev_charger.py | 155 ++++++++++++++++++ .../electrical_components/test_ev_charger.py | 118 +++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 src/frequenz/client/common/microgrid/electrical_components/_ev_charger.py create mode 100644 tests/microgrid/electrical_components/test_ev_charger.py diff --git a/src/frequenz/client/common/microgrid/electrical_components/_ev_charger.py b/src/frequenz/client/common/microgrid/electrical_components/_ev_charger.py new file mode 100644 index 00000000..5a7deb34 --- /dev/null +++ b/src/frequenz/client/common/microgrid/electrical_components/_ev_charger.py @@ -0,0 +1,155 @@ +# License: MIT +# Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +"""Electric vehicle (EV) charger 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 EvChargerType(enum.Enum): + """The known types of electric vehicle (EV) chargers.""" + + UNSPECIFIED = electrical_components_pb2.EV_CHARGER_TYPE_UNSPECIFIED + """The type of the EV charger is unspecified.""" + + AC = electrical_components_pb2.EV_CHARGER_TYPE_AC + """The EV charging station supports AC charging only.""" + + DC = electrical_components_pb2.EV_CHARGER_TYPE_DC + """The EV charging station supports DC charging only.""" + + HYBRID = electrical_components_pb2.EV_CHARGER_TYPE_HYBRID + """The EV charging station supports both AC and DC.""" + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class EvCharger(Component): + """An abstract EV charger component.""" + + category: Literal[ComponentCategory.EV_CHARGER] = ComponentCategory.EV_CHARGER + """The category of this component. + + Note: + This should not be used normally, you should test if a component + [`isinstance`][] of a concrete EV charger 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: EvChargerType | int + """The type of this EV charger. + + Note: + This should not be used normally, you should test if a EV charger + [`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 the new EV charger type yet (i.e. for use with + [`UnrecognizedEvCharger`][frequenz.client.microgrid.component.UnrecognizedEvCharger]). + """ + + # pylint: disable-next=unused-argument + def __new__(cls, *args: Any, **kwargs: Any) -> Self: + """Prevent instantiation of this class.""" + if cls is EvCharger: + raise TypeError(f"Cannot instantiate {cls.__name__} directly") + return super().__new__(cls) + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class UnspecifiedEvCharger(EvCharger): + """An EV charger of an unspecified type.""" + + type: Literal[EvChargerType.UNSPECIFIED] = EvChargerType.UNSPECIFIED + """The type of this EV charger. + + Note: + This should not be used normally, you should test if a EV charger + [`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 the new EV charger type yet (i.e. for use with + [`UnrecognizedEvCharger`][frequenz.client.microgrid.component.UnrecognizedEvCharger]). + """ + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class AcEvCharger(EvCharger): + """An EV charger that supports AC charging only.""" + + type: Literal[EvChargerType.AC] = EvChargerType.AC + """The type of this EV charger. + + Note: + This should not be used normally, you should test if a EV charger + [`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 the new EV charger type yet (i.e. for use with + [`UnrecognizedEvCharger`][frequenz.client.microgrid.component.UnrecognizedEvCharger]). + """ + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class DcEvCharger(EvCharger): + """An EV charger that supports DC charging only.""" + + type: Literal[EvChargerType.DC] = EvChargerType.DC + """The type of this EV charger. + + Note: + This should not be used normally, you should test if a EV charger + [`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 the new EV charger type yet (i.e. for use with + [`UnrecognizedEvCharger`][frequenz.client.microgrid.component.UnrecognizedEvCharger]). + """ + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class HybridEvCharger(EvCharger): + """An EV charger that supports both AC and DC charging.""" + + type: Literal[EvChargerType.HYBRID] = EvChargerType.HYBRID + """The type of this EV charger. + + Note: + This should not be used normally, you should test if a EV charger + [`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 the new EV charger type yet (i.e. for use with + [`UnrecognizedEvCharger`][frequenz.client.microgrid.component.UnrecognizedEvCharger]). + """ + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class UnrecognizedEvCharger(EvCharger): + """An EV charger of an unrecognized type.""" + + type: int + """The unrecognized type of this EV charger.""" + + +EvChargerTypes: TypeAlias = ( + UnspecifiedEvCharger + | AcEvCharger + | DcEvCharger + | HybridEvCharger + | UnrecognizedEvCharger +) +"""All possible EV charger types.""" diff --git a/tests/microgrid/electrical_components/test_ev_charger.py b/tests/microgrid/electrical_components/test_ev_charger.py new file mode 100644 index 00000000..705970b9 --- /dev/null +++ b/tests/microgrid/electrical_components/test_ev_charger.py @@ -0,0 +1,118 @@ +# License: MIT +# Copyright © 2025 Frequenz Energy-as-a-Service GmbH + +"""Tests for EV charger 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 ( + AcEvCharger, + ComponentCategory, + DcEvCharger, + EvCharger, + EvChargerType, + HybridEvCharger, + UnrecognizedEvCharger, + UnspecifiedEvCharger, +) + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class EvChargerTestCase: + """Test case for EV charger components.""" + + cls: type[UnspecifiedEvCharger | AcEvCharger | DcEvCharger | HybridEvCharger] + expected_type: EvChargerType + 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_ev_charger_cannot_be_instantiated( + component_id: ComponentId, microgrid_id: MicrogridId +) -> None: + """Test that EvCharger base class cannot be instantiated.""" + with pytest.raises(TypeError, match="Cannot instantiate EvCharger directly"): + EvCharger( + id=component_id, + microgrid_id=microgrid_id, + name="test_charger", + manufacturer="test_manufacturer", + model_name="test_model", + type=EvChargerType.AC, + ) + + +@pytest.mark.parametrize( + "case", + [ + EvChargerTestCase( + cls=UnspecifiedEvCharger, + expected_type=EvChargerType.UNSPECIFIED, + name="unspecified", + ), + EvChargerTestCase(cls=AcEvCharger, expected_type=EvChargerType.AC, name="ac"), + EvChargerTestCase(cls=DcEvCharger, expected_type=EvChargerType.DC, name="dc"), + EvChargerTestCase( + cls=HybridEvCharger, + expected_type=EvChargerType.HYBRID, + name="hybrid", + ), + ], + ids=lambda case: case.name, +) +def test_recognized_ev_charger_types( # Renamed from test_ev_charger_types + case: EvChargerTestCase, component_id: ComponentId, microgrid_id: MicrogridId +) -> None: + """Test initialization and properties of different recognized EV charger types.""" + charger = case.cls( + id=component_id, + microgrid_id=microgrid_id, + name=case.name, + manufacturer="test_manufacturer", + model_name="test_model", + ) + + assert charger.id == component_id + assert charger.microgrid_id == microgrid_id + assert charger.name == case.name + assert charger.manufacturer == "test_manufacturer" + assert charger.model_name == "test_model" + assert charger.category == ComponentCategory.EV_CHARGER + assert charger.type == case.expected_type + + +def test_unrecognized_ev_charger_type( + component_id: ComponentId, microgrid_id: MicrogridId +) -> None: + """Test initialization and properties of unrecognized EV charger type.""" + charger = UnrecognizedEvCharger( + id=component_id, + microgrid_id=microgrid_id, + name="unrecognized_charger", + manufacturer="test_manufacturer", + model_name="test_model", + type=999, # type is passed here for UnrecognizedEvCharger + ) + + assert charger.id == component_id + assert charger.microgrid_id == microgrid_id + assert charger.name == "unrecognized_charger" + assert charger.manufacturer == "test_manufacturer" + assert charger.model_name == "test_model" + assert charger.category == ComponentCategory.EV_CHARGER + assert charger.type == 999 From a046e3e2771d7f49340631ae07794ced52406e7a Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 11 Jun 2026 10:25:40 +0000 Subject: [PATCH 2/4] Generalize EV charger component docstrings Signed-off-by: Leandro Lucarella --- .../electrical_components/_ev_charger.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/frequenz/client/common/microgrid/electrical_components/_ev_charger.py b/src/frequenz/client/common/microgrid/electrical_components/_ev_charger.py index 5a7deb34..7670a674 100644 --- a/src/frequenz/client/common/microgrid/electrical_components/_ev_charger.py +++ b/src/frequenz/client/common/microgrid/electrical_components/_ev_charger.py @@ -1,7 +1,7 @@ # License: MIT # Copyright © 2024 Frequenz Energy-as-a-Service GmbH -"""Electric vehicle (EV) charger component.""" +"""Electric vehicle (EV) charger 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 EvChargerType(enum.Enum): @dataclasses.dataclass(frozen=True, kw_only=True) -class EvCharger(Component): - """An abstract EV charger component.""" +class EvCharger(ElectricalComponent): + """An abstract EV charger electrical component.""" category: Literal[ComponentCategory.EV_CHARGER] = ComponentCategory.EV_CHARGER - """The category of this component. + """The category of this electrical component. Note: - This should not be used normally, you should test if a component + This should not be used normally, you should test if an electrical component [`isinstance`][] of a concrete EV charger 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: EvChargerType | int From 681371ae5098a6ec6b2e85dee0bfafb13c675d19 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 11 Jun 2026 10:29:19 +0000 Subject: [PATCH 3/4] Adapt EV charger component module to the common client layout Signed-off-by: Leandro Lucarella --- .../electrical_components/__init__.py | 18 ++++++++++++++++ .../electrical_components/_ev_charger.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 724ee2ec..84a2b0f3 100644 --- a/src/frequenz/client/common/microgrid/electrical_components/__init__.py +++ b/src/frequenz/client/common/microgrid/electrical_components/__init__.py @@ -15,6 +15,16 @@ from ._category import ElectricalComponentCategory from ._diagnostic_code import ElectricalComponentDiagnosticCode from ._electrical_component import ElectricalComponent +from ._ev_charger import ( + AcEvCharger, + DcEvCharger, + EvCharger, + EvChargerType, + EvChargerTypes, + HybridEvCharger, + UnrecognizedEvCharger, + UnspecifiedEvCharger, +) from ._ids import ElectricalComponentId from ._inverter import ( BatteryInverter, @@ -35,15 +45,21 @@ from ._state_code import ElectricalComponentStateCode __all__ = [ + "AcEvCharger", "Battery", "BatteryInverter", "BatteryType", "BatteryTypes", + "DcEvCharger", "ElectricalComponent", "ElectricalComponentCategory", "ElectricalComponentDiagnosticCode", "ElectricalComponentId", "ElectricalComponentStateCode", + "EvCharger", + "EvChargerType", + "EvChargerTypes", + "HybridEvCharger", "HybridInverter", "Inverter", "InverterType", @@ -55,8 +71,10 @@ "SolarInverter", "UnrecognizedBattery", "UnrecognizedComponent", + "UnrecognizedEvCharger", "UnrecognizedInverter", "UnspecifiedBattery", "UnspecifiedComponent", + "UnspecifiedEvCharger", "UnspecifiedInverter", ] diff --git a/src/frequenz/client/common/microgrid/electrical_components/_ev_charger.py b/src/frequenz/client/common/microgrid/electrical_components/_ev_charger.py index 7670a674..b5ff1e5c 100644 --- a/src/frequenz/client/common/microgrid/electrical_components/_ev_charger.py +++ b/src/frequenz/client/common/microgrid/electrical_components/_ev_charger.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 EvChargerType(enum.Enum): class EvCharger(ElectricalComponent): """An abstract EV charger electrical component.""" - category: Literal[ComponentCategory.EV_CHARGER] = ComponentCategory.EV_CHARGER + category: Literal[ElectricalComponentCategory.EV_CHARGER] = ( + ElectricalComponentCategory.EV_CHARGER + ) """The category of this electrical component. Note: @@ -45,9 +47,8 @@ class EvCharger(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: EvChargerType | int @@ -59,7 +60,7 @@ class EvCharger(ElectricalComponent): It is only provided for using with a newer version of the API where the client doesn't know about the new EV charger type yet (i.e. for use with - [`UnrecognizedEvCharger`][frequenz.client.microgrid.component.UnrecognizedEvCharger]). + [`UnrecognizedEvCharger`][...UnrecognizedEvCharger]). """ # pylint: disable-next=unused-argument @@ -83,7 +84,7 @@ class UnspecifiedEvCharger(EvCharger): It is only provided for using with a newer version of the API where the client doesn't know about the new EV charger type yet (i.e. for use with - [`UnrecognizedEvCharger`][frequenz.client.microgrid.component.UnrecognizedEvCharger]). + [`UnrecognizedEvCharger`][...UnrecognizedEvCharger]). """ @@ -100,7 +101,7 @@ class AcEvCharger(EvCharger): It is only provided for using with a newer version of the API where the client doesn't know about the new EV charger type yet (i.e. for use with - [`UnrecognizedEvCharger`][frequenz.client.microgrid.component.UnrecognizedEvCharger]). + [`UnrecognizedEvCharger`][...UnrecognizedEvCharger]). """ @@ -117,7 +118,7 @@ class DcEvCharger(EvCharger): It is only provided for using with a newer version of the API where the client doesn't know about the new EV charger type yet (i.e. for use with - [`UnrecognizedEvCharger`][frequenz.client.microgrid.component.UnrecognizedEvCharger]). + [`UnrecognizedEvCharger`][...UnrecognizedEvCharger]). """ @@ -134,7 +135,7 @@ class HybridEvCharger(EvCharger): It is only provided for using with a newer version of the API where the client doesn't know about the new EV charger type yet (i.e. for use with - [`UnrecognizedEvCharger`][frequenz.client.microgrid.component.UnrecognizedEvCharger]). + [`UnrecognizedEvCharger`][...UnrecognizedEvCharger]). """ From ac1a472b5f2802749281b332526214344d629527 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 11 Jun 2026 10:32:14 +0000 Subject: [PATCH 4/4] Adapt EV charger component tests to the common client layout Signed-off-by: Leandro Lucarella --- .../electrical_components/test_ev_charger.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/microgrid/electrical_components/test_ev_charger.py b/tests/microgrid/electrical_components/test_ev_charger.py index 705970b9..f9351909 100644 --- a/tests/microgrid/electrical_components/test_ev_charger.py +++ b/tests/microgrid/electrical_components/test_ev_charger.py @@ -6,13 +6,13 @@ 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 ( AcEvCharger, - ComponentCategory, DcEvCharger, + ElectricalComponentCategory, + ElectricalComponentId, EvCharger, EvChargerType, HybridEvCharger, @@ -31,9 +31,9 @@ class EvChargerTestCase: @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_ev_charger_cannot_be_instantiated( - component_id: ComponentId, microgrid_id: MicrogridId + component_id: ElectricalComponentId, microgrid_id: MicrogridId ) -> None: """Test that EvCharger base class cannot be instantiated.""" with pytest.raises(TypeError, match="Cannot instantiate EvCharger directly"): @@ -76,7 +76,9 @@ def test_abstract_ev_charger_cannot_be_instantiated( ids=lambda case: case.name, ) def test_recognized_ev_charger_types( # Renamed from test_ev_charger_types - case: EvChargerTestCase, component_id: ComponentId, microgrid_id: MicrogridId + case: EvChargerTestCase, + component_id: ElectricalComponentId, + microgrid_id: MicrogridId, ) -> None: """Test initialization and properties of different recognized EV charger types.""" charger = case.cls( @@ -92,12 +94,12 @@ def test_recognized_ev_charger_types( # Renamed from test_ev_charger_types assert charger.name == case.name assert charger.manufacturer == "test_manufacturer" assert charger.model_name == "test_model" - assert charger.category == ComponentCategory.EV_CHARGER + assert charger.category == ElectricalComponentCategory.EV_CHARGER assert charger.type == case.expected_type def test_unrecognized_ev_charger_type( - component_id: ComponentId, microgrid_id: MicrogridId + component_id: ElectricalComponentId, microgrid_id: MicrogridId ) -> None: """Test initialization and properties of unrecognized EV charger type.""" charger = UnrecognizedEvCharger( @@ -114,5 +116,5 @@ def test_unrecognized_ev_charger_type( assert charger.name == "unrecognized_charger" assert charger.manufacturer == "test_manufacturer" assert charger.model_name == "test_model" - assert charger.category == ComponentCategory.EV_CHARGER + assert charger.category == ElectricalComponentCategory.EV_CHARGER assert charger.type == 999