Skip to content

Commit 62dcbaf

Browse files
authored
Add Inverter class and subclasses wrappers (#206)
Import inverter component module from the microgrid client and adapt it to the common client layout.
2 parents 1adcf8c + 28839c5 commit 62dcbaf

3 files changed

Lines changed: 297 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
@@ -16,6 +16,16 @@
1616
from ._diagnostic_code import ElectricalComponentDiagnosticCode
1717
from ._electrical_component import ElectricalComponent
1818
from ._ids import ElectricalComponentId
19+
from ._inverter import (
20+
BatteryInverter,
21+
HybridInverter,
22+
Inverter,
23+
InverterType,
24+
InverterTypes,
25+
SolarInverter,
26+
UnrecognizedInverter,
27+
UnspecifiedInverter,
28+
)
1929
from ._problematic import (
2030
MismatchedCategoryComponent,
2131
ProblematicComponent,
@@ -26,19 +36,27 @@
2636

2737
__all__ = [
2838
"Battery",
39+
"BatteryInverter",
2940
"BatteryType",
3041
"BatteryTypes",
3142
"ElectricalComponent",
3243
"ElectricalComponentCategory",
3344
"ElectricalComponentDiagnosticCode",
3445
"ElectricalComponentId",
3546
"ElectricalComponentStateCode",
47+
"HybridInverter",
48+
"Inverter",
49+
"InverterType",
50+
"InverterTypes",
3651
"LiIonBattery",
3752
"MismatchedCategoryComponent",
3853
"NaIonBattery",
3954
"ProblematicComponent",
55+
"SolarInverter",
4056
"UnrecognizedBattery",
4157
"UnrecognizedComponent",
58+
"UnrecognizedInverter",
4259
"UnspecifiedBattery",
4360
"UnspecifiedComponent",
61+
"UnspecifiedInverter",
4462
]
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+
"""Inverter 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 InverterType(enum.Enum):
20+
"""The known types of inverters."""
21+
22+
UNSPECIFIED = electrical_components_pb2.INVERTER_TYPE_UNSPECIFIED
23+
"""The type of the inverter is unspecified."""
24+
25+
BATTERY = electrical_components_pb2.INVERTER_TYPE_BATTERY
26+
"""The inverter is a battery inverter."""
27+
28+
SOLAR = electrical_components_pb2.INVERTER_TYPE_PV
29+
"""The inverter is a solar inverter."""
30+
31+
HYBRID = electrical_components_pb2.INVERTER_TYPE_HYBRID
32+
"""The inverter is a hybrid inverter."""
33+
34+
35+
@dataclasses.dataclass(frozen=True, kw_only=True)
36+
class Inverter(ElectricalComponent):
37+
"""An abstract inverter electrical component."""
38+
39+
category: Literal[ElectricalComponentCategory.INVERTER] = (
40+
ElectricalComponentCategory.INVERTER
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 electrical component 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 level
51+
code needs to know the category of an electrical component.
52+
"""
53+
54+
type: InverterType | int
55+
"""The type of this inverter.
56+
57+
Note:
58+
This should not be used normally, you should test if a inverter
59+
[`isinstance`][] of a concrete inverter 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 inverter type yet (i.e. for use with
63+
[`UnrecognizedInverter`][...UnrecognizedInverter]).
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 Inverter:
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 UnspecifiedInverter(Inverter):
76+
"""An inverter of an unspecified type."""
77+
78+
type: Literal[InverterType.UNSPECIFIED] = InverterType.UNSPECIFIED
79+
"""The type of this inverter.
80+
81+
Note:
82+
This should not be used normally, you should test if a inverter
83+
[`isinstance`][] of a concrete inverter 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 inverter type yet (i.e. for use with
87+
[`UnrecognizedInverter`][...UnrecognizedInverter]).
88+
"""
89+
90+
91+
@dataclasses.dataclass(frozen=True, kw_only=True)
92+
class BatteryInverter(Inverter):
93+
"""A battery inverter."""
94+
95+
type: Literal[InverterType.BATTERY] = InverterType.BATTERY
96+
"""The type of this inverter.
97+
98+
Note:
99+
This should not be used normally, you should test if a inverter
100+
[`isinstance`][] of a concrete inverter 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 inverter type yet (i.e. for use with
104+
[`UnrecognizedInverter`][...UnrecognizedInverter]).
105+
"""
106+
107+
108+
@dataclasses.dataclass(frozen=True, kw_only=True)
109+
class SolarInverter(Inverter):
110+
"""A solar inverter."""
111+
112+
type: Literal[InverterType.SOLAR] = InverterType.SOLAR
113+
"""The type of this inverter.
114+
115+
Note:
116+
This should not be used normally, you should test if a inverter
117+
[`isinstance`][] of a concrete inverter 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 inverter type yet (i.e. for use with
121+
[`UnrecognizedInverter`][...UnrecognizedInverter]).
122+
"""
123+
124+
125+
@dataclasses.dataclass(frozen=True, kw_only=True)
126+
class HybridInverter(Inverter):
127+
"""A hybrid inverter."""
128+
129+
type: Literal[InverterType.HYBRID] = InverterType.HYBRID
130+
"""The type of this inverter.
131+
132+
Note:
133+
This should not be used normally, you should test if a inverter
134+
[`isinstance`][] of a concrete inverter 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 inverter type yet (i.e. for use with
138+
[`UnrecognizedInverter`][...UnrecognizedInverter]).
139+
"""
140+
141+
142+
@dataclasses.dataclass(frozen=True, kw_only=True)
143+
class UnrecognizedInverter(Inverter):
144+
"""An inverter of an unrecognized type."""
145+
146+
type: int
147+
"""The unrecognized type of this inverter."""
148+
149+
150+
InverterTypes: TypeAlias = (
151+
UnspecifiedInverter
152+
| BatteryInverter
153+
| SolarInverter
154+
| HybridInverter
155+
| UnrecognizedInverter
156+
)
157+
"""All possible inverter types."""
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Tests for Inverter 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+
BatteryInverter,
13+
ElectricalComponentCategory,
14+
ElectricalComponentId,
15+
HybridInverter,
16+
Inverter,
17+
InverterType,
18+
SolarInverter,
19+
UnrecognizedInverter,
20+
UnspecifiedInverter,
21+
)
22+
23+
24+
@dataclasses.dataclass(frozen=True, kw_only=True)
25+
class InverterTestCase:
26+
"""Test case for Inverter components."""
27+
28+
cls: type[UnspecifiedInverter | BatteryInverter | SolarInverter | HybridInverter]
29+
expected_type: InverterType
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_inverter_cannot_be_instantiated(
46+
component_id: ElectricalComponentId, microgrid_id: MicrogridId
47+
) -> None:
48+
"""Test that Inverter base class cannot be instantiated."""
49+
with pytest.raises(TypeError, match="Cannot instantiate Inverter directly"):
50+
Inverter(
51+
id=component_id,
52+
microgrid_id=microgrid_id,
53+
name="test_inverter",
54+
manufacturer="test_manufacturer",
55+
model_name="test_model",
56+
type=InverterType.BATTERY,
57+
)
58+
59+
60+
@pytest.mark.parametrize(
61+
"case",
62+
[
63+
InverterTestCase(
64+
cls=UnspecifiedInverter,
65+
expected_type=InverterType.UNSPECIFIED,
66+
name="unspecified",
67+
),
68+
InverterTestCase(
69+
cls=BatteryInverter, expected_type=InverterType.BATTERY, name="battery"
70+
),
71+
InverterTestCase(
72+
cls=SolarInverter, expected_type=InverterType.SOLAR, name="solar"
73+
),
74+
InverterTestCase(
75+
cls=HybridInverter, expected_type=InverterType.HYBRID, name="hybrid"
76+
),
77+
],
78+
ids=lambda case: case.name,
79+
)
80+
def test_recognized_inverter_types(
81+
case: InverterTestCase,
82+
component_id: ElectricalComponentId,
83+
microgrid_id: MicrogridId,
84+
) -> None:
85+
"""Test initialization and properties of different recognized inverter types."""
86+
inverter = case.cls(
87+
id=component_id,
88+
microgrid_id=microgrid_id,
89+
name=case.name,
90+
manufacturer="test_manufacturer",
91+
model_name="test_model",
92+
)
93+
94+
assert inverter.id == component_id
95+
assert inverter.microgrid_id == microgrid_id
96+
assert inverter.name == case.name
97+
assert inverter.manufacturer == "test_manufacturer"
98+
assert inverter.model_name == "test_model"
99+
assert inverter.category == ElectricalComponentCategory.INVERTER
100+
assert inverter.type == case.expected_type
101+
102+
103+
def test_unrecognized_inverter_type(
104+
component_id: ElectricalComponentId, microgrid_id: MicrogridId
105+
) -> None:
106+
"""Test initialization and properties of unrecognized inverter type."""
107+
inverter = UnrecognizedInverter(
108+
id=component_id,
109+
microgrid_id=microgrid_id,
110+
name="unrecognized_inverter",
111+
manufacturer="test_manufacturer",
112+
model_name="test_model",
113+
type=999, # type is passed here for UnrecognizedInverter
114+
)
115+
116+
assert inverter.id == component_id
117+
assert inverter.microgrid_id == microgrid_id
118+
assert inverter.name == "unrecognized_inverter"
119+
assert inverter.manufacturer == "test_manufacturer"
120+
assert inverter.model_name == "test_model"
121+
assert inverter.category == ElectricalComponentCategory.INVERTER
122+
assert inverter.type == 999

0 commit comments

Comments
 (0)