Skip to content

Commit 3c805e7

Browse files
authored
Add EvCharger class and subclasses wrappers (#207)
Import EV charger component module from the microgrid client and adapt it to the common client layout. Part of #203.
2 parents 62dcbaf + ac1a472 commit 3c805e7

3 files changed

Lines changed: 295 additions & 0 deletions

File tree

src/frequenz/client/common/microgrid/electrical_components/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@
1515
from ._category import ElectricalComponentCategory
1616
from ._diagnostic_code import ElectricalComponentDiagnosticCode
1717
from ._electrical_component import ElectricalComponent
18+
from ._ev_charger import (
19+
AcEvCharger,
20+
DcEvCharger,
21+
EvCharger,
22+
EvChargerType,
23+
EvChargerTypes,
24+
HybridEvCharger,
25+
UnrecognizedEvCharger,
26+
UnspecifiedEvCharger,
27+
)
1828
from ._ids import ElectricalComponentId
1929
from ._inverter import (
2030
BatteryInverter,
@@ -35,15 +45,21 @@
3545
from ._state_code import ElectricalComponentStateCode
3646

3747
__all__ = [
48+
"AcEvCharger",
3849
"Battery",
3950
"BatteryInverter",
4051
"BatteryType",
4152
"BatteryTypes",
53+
"DcEvCharger",
4254
"ElectricalComponent",
4355
"ElectricalComponentCategory",
4456
"ElectricalComponentDiagnosticCode",
4557
"ElectricalComponentId",
4658
"ElectricalComponentStateCode",
59+
"EvCharger",
60+
"EvChargerType",
61+
"EvChargerTypes",
62+
"HybridEvCharger",
4763
"HybridInverter",
4864
"Inverter",
4965
"InverterType",
@@ -55,8 +71,10 @@
5571
"SolarInverter",
5672
"UnrecognizedBattery",
5773
"UnrecognizedComponent",
74+
"UnrecognizedEvCharger",
5875
"UnrecognizedInverter",
5976
"UnspecifiedBattery",
6077
"UnspecifiedComponent",
78+
"UnspecifiedEvCharger",
6179
"UnspecifiedInverter",
6280
]
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Electric vehicle (EV) charger electrical component."""
5+
6+
import dataclasses
7+
import enum
8+
from typing import Any, Literal, Self, TypeAlias
9+
10+
from frequenz.api.common.v1alpha8.microgrid.electrical_components import (
11+
electrical_components_pb2,
12+
)
13+
14+
from ._category import ElectricalComponentCategory
15+
from ._electrical_component import ElectricalComponent
16+
17+
18+
@enum.unique
19+
class EvChargerType(enum.Enum):
20+
"""The known types of electric vehicle (EV) chargers."""
21+
22+
UNSPECIFIED = electrical_components_pb2.EV_CHARGER_TYPE_UNSPECIFIED
23+
"""The type of the EV charger is unspecified."""
24+
25+
AC = electrical_components_pb2.EV_CHARGER_TYPE_AC
26+
"""The EV charging station supports AC charging only."""
27+
28+
DC = electrical_components_pb2.EV_CHARGER_TYPE_DC
29+
"""The EV charging station supports DC charging only."""
30+
31+
HYBRID = electrical_components_pb2.EV_CHARGER_TYPE_HYBRID
32+
"""The EV charging station supports both AC and DC."""
33+
34+
35+
@dataclasses.dataclass(frozen=True, kw_only=True)
36+
class EvCharger(ElectricalComponent):
37+
"""An abstract EV charger electrical component."""
38+
39+
category: Literal[ElectricalComponentCategory.EV_CHARGER] = (
40+
ElectricalComponentCategory.EV_CHARGER
41+
)
42+
"""The category of this electrical component.
43+
44+
Note:
45+
This should not be used normally, you should test if an electrical component
46+
[`isinstance`][] of a concrete EV charger class instead.
47+
48+
It is only provided for using with a newer version of the API where the client
49+
doesn't know about a new category yet (i.e. for use with
50+
[`UnrecognizedComponent`][...UnrecognizedComponent]) and in case some low
51+
level code needs to know the category of an electrical component.
52+
"""
53+
54+
type: EvChargerType | int
55+
"""The type of this EV charger.
56+
57+
Note:
58+
This should not be used normally, you should test if a EV charger
59+
[`isinstance`][] of a concrete component class instead.
60+
61+
It is only provided for using with a newer version of the API where the client
62+
doesn't know about the new EV charger type yet (i.e. for use with
63+
[`UnrecognizedEvCharger`][...UnrecognizedEvCharger]).
64+
"""
65+
66+
# pylint: disable-next=unused-argument
67+
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
68+
"""Prevent instantiation of this class."""
69+
if cls is EvCharger:
70+
raise TypeError(f"Cannot instantiate {cls.__name__} directly")
71+
return super().__new__(cls)
72+
73+
74+
@dataclasses.dataclass(frozen=True, kw_only=True)
75+
class UnspecifiedEvCharger(EvCharger):
76+
"""An EV charger of an unspecified type."""
77+
78+
type: Literal[EvChargerType.UNSPECIFIED] = EvChargerType.UNSPECIFIED
79+
"""The type of this EV charger.
80+
81+
Note:
82+
This should not be used normally, you should test if a EV charger
83+
[`isinstance`][] of a concrete component class instead.
84+
85+
It is only provided for using with a newer version of the API where the client
86+
doesn't know about the new EV charger type yet (i.e. for use with
87+
[`UnrecognizedEvCharger`][...UnrecognizedEvCharger]).
88+
"""
89+
90+
91+
@dataclasses.dataclass(frozen=True, kw_only=True)
92+
class AcEvCharger(EvCharger):
93+
"""An EV charger that supports AC charging only."""
94+
95+
type: Literal[EvChargerType.AC] = EvChargerType.AC
96+
"""The type of this EV charger.
97+
98+
Note:
99+
This should not be used normally, you should test if a EV charger
100+
[`isinstance`][] of a concrete component class instead.
101+
102+
It is only provided for using with a newer version of the API where the client
103+
doesn't know about the new EV charger type yet (i.e. for use with
104+
[`UnrecognizedEvCharger`][...UnrecognizedEvCharger]).
105+
"""
106+
107+
108+
@dataclasses.dataclass(frozen=True, kw_only=True)
109+
class DcEvCharger(EvCharger):
110+
"""An EV charger that supports DC charging only."""
111+
112+
type: Literal[EvChargerType.DC] = EvChargerType.DC
113+
"""The type of this EV charger.
114+
115+
Note:
116+
This should not be used normally, you should test if a EV charger
117+
[`isinstance`][] of a concrete component class instead.
118+
119+
It is only provided for using with a newer version of the API where the client
120+
doesn't know about the new EV charger type yet (i.e. for use with
121+
[`UnrecognizedEvCharger`][...UnrecognizedEvCharger]).
122+
"""
123+
124+
125+
@dataclasses.dataclass(frozen=True, kw_only=True)
126+
class HybridEvCharger(EvCharger):
127+
"""An EV charger that supports both AC and DC charging."""
128+
129+
type: Literal[EvChargerType.HYBRID] = EvChargerType.HYBRID
130+
"""The type of this EV charger.
131+
132+
Note:
133+
This should not be used normally, you should test if a EV charger
134+
[`isinstance`][] of a concrete component class instead.
135+
136+
It is only provided for using with a newer version of the API where the client
137+
doesn't know about the new EV charger type yet (i.e. for use with
138+
[`UnrecognizedEvCharger`][...UnrecognizedEvCharger]).
139+
"""
140+
141+
142+
@dataclasses.dataclass(frozen=True, kw_only=True)
143+
class UnrecognizedEvCharger(EvCharger):
144+
"""An EV charger of an unrecognized type."""
145+
146+
type: int
147+
"""The unrecognized type of this EV charger."""
148+
149+
150+
EvChargerTypes: TypeAlias = (
151+
UnspecifiedEvCharger
152+
| AcEvCharger
153+
| DcEvCharger
154+
| HybridEvCharger
155+
| UnrecognizedEvCharger
156+
)
157+
"""All possible EV charger types."""
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Tests for EV charger components."""
5+
6+
import dataclasses
7+
8+
import pytest
9+
10+
from frequenz.client.common.microgrid import MicrogridId
11+
from frequenz.client.common.microgrid.electrical_components import (
12+
AcEvCharger,
13+
DcEvCharger,
14+
ElectricalComponentCategory,
15+
ElectricalComponentId,
16+
EvCharger,
17+
EvChargerType,
18+
HybridEvCharger,
19+
UnrecognizedEvCharger,
20+
UnspecifiedEvCharger,
21+
)
22+
23+
24+
@dataclasses.dataclass(frozen=True, kw_only=True)
25+
class EvChargerTestCase:
26+
"""Test case for EV charger components."""
27+
28+
cls: type[UnspecifiedEvCharger | AcEvCharger | DcEvCharger | HybridEvCharger]
29+
expected_type: EvChargerType
30+
name: str
31+
32+
33+
@pytest.fixture
34+
def component_id() -> ElectricalComponentId:
35+
"""Provide a test component ID."""
36+
return ElectricalComponentId(42)
37+
38+
39+
@pytest.fixture
40+
def microgrid_id() -> MicrogridId:
41+
"""Provide a test microgrid ID."""
42+
return MicrogridId(1)
43+
44+
45+
def test_abstract_ev_charger_cannot_be_instantiated(
46+
component_id: ElectricalComponentId, microgrid_id: MicrogridId
47+
) -> None:
48+
"""Test that EvCharger base class cannot be instantiated."""
49+
with pytest.raises(TypeError, match="Cannot instantiate EvCharger directly"):
50+
EvCharger(
51+
id=component_id,
52+
microgrid_id=microgrid_id,
53+
name="test_charger",
54+
manufacturer="test_manufacturer",
55+
model_name="test_model",
56+
type=EvChargerType.AC,
57+
)
58+
59+
60+
@pytest.mark.parametrize(
61+
"case",
62+
[
63+
EvChargerTestCase(
64+
cls=UnspecifiedEvCharger,
65+
expected_type=EvChargerType.UNSPECIFIED,
66+
name="unspecified",
67+
),
68+
EvChargerTestCase(cls=AcEvCharger, expected_type=EvChargerType.AC, name="ac"),
69+
EvChargerTestCase(cls=DcEvCharger, expected_type=EvChargerType.DC, name="dc"),
70+
EvChargerTestCase(
71+
cls=HybridEvCharger,
72+
expected_type=EvChargerType.HYBRID,
73+
name="hybrid",
74+
),
75+
],
76+
ids=lambda case: case.name,
77+
)
78+
def test_recognized_ev_charger_types( # Renamed from test_ev_charger_types
79+
case: EvChargerTestCase,
80+
component_id: ElectricalComponentId,
81+
microgrid_id: MicrogridId,
82+
) -> None:
83+
"""Test initialization and properties of different recognized EV charger types."""
84+
charger = case.cls(
85+
id=component_id,
86+
microgrid_id=microgrid_id,
87+
name=case.name,
88+
manufacturer="test_manufacturer",
89+
model_name="test_model",
90+
)
91+
92+
assert charger.id == component_id
93+
assert charger.microgrid_id == microgrid_id
94+
assert charger.name == case.name
95+
assert charger.manufacturer == "test_manufacturer"
96+
assert charger.model_name == "test_model"
97+
assert charger.category == ElectricalComponentCategory.EV_CHARGER
98+
assert charger.type == case.expected_type
99+
100+
101+
def test_unrecognized_ev_charger_type(
102+
component_id: ElectricalComponentId, microgrid_id: MicrogridId
103+
) -> None:
104+
"""Test initialization and properties of unrecognized EV charger type."""
105+
charger = UnrecognizedEvCharger(
106+
id=component_id,
107+
microgrid_id=microgrid_id,
108+
name="unrecognized_charger",
109+
manufacturer="test_manufacturer",
110+
model_name="test_model",
111+
type=999, # type is passed here for UnrecognizedEvCharger
112+
)
113+
114+
assert charger.id == component_id
115+
assert charger.microgrid_id == microgrid_id
116+
assert charger.name == "unrecognized_charger"
117+
assert charger.manufacturer == "test_manufacturer"
118+
assert charger.model_name == "test_model"
119+
assert charger.category == ElectricalComponentCategory.EV_CHARGER
120+
assert charger.type == 999

0 commit comments

Comments
 (0)