Skip to content

Commit 509ae9c

Browse files
committed
Add ProblematicComponent and sub-classes
Problematic components are components that can't be mapped to known category types (or are `UNSPECIFIED`). They also include components with mismatched a category, i.e. a component with a particular known category but that also has category-specific information that doesn't match the specified category. For example if the category is `BATTERY` but the category-specific information is for a `INVERTER`. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 76f4c34 commit 509ae9c

File tree

4 files changed

+205
-3
lines changed

4 files changed

+205
-3
lines changed

src/frequenz/client/microgrid/component/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@
4242
)
4343
from ._meter import Meter
4444
from ._precharger import Precharger
45+
from ._problematic import (
46+
MismatchedCategoryComponent,
47+
ProblematicComponent,
48+
UnrecognizedComponent,
49+
UnspecifiedComponent,
50+
)
4551
from ._relay import Relay
4652
from ._status import ComponentStatus
4753
from ._voltage_transformer import VoltageTransformer
@@ -72,14 +78,18 @@
7278
"InverterType",
7379
"LiIonBattery",
7480
"Meter",
81+
"MismatchedCategoryComponent",
7582
"NaIonBattery",
7683
"Precharger",
84+
"ProblematicComponent",
7785
"Relay",
7886
"SolarInverter",
7987
"UnrecognizedBattery",
88+
"UnrecognizedComponent",
8089
"UnrecognizedEvCharger",
8190
"UnrecognizedInverter",
8291
"UnspecifiedBattery",
92+
"UnspecifiedComponent",
8393
"UnspecifiedEvCharger",
8494
"UnspecifiedInverter",
8595
"VoltageTransformer",

src/frequenz/client/microgrid/component/_component.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ class Component: # pylint: disable=too-many-instance-attributes
3939
[`isinstance`][] of a concrete component class instead.
4040
4141
It is only provided for using with a newer version of the API where the client
42-
doesn't know about a new category yet, and in case some low level code needs to
43-
know the category of a component.
42+
doesn't know about a new category yet (i.e. for use with
43+
[`UnrecognizedComponent`][frequenz.client.microgrid.component.UnrecognizedComponent])
44+
and in case some low level code needs to know the category of a component.
4445
"""
4546

4647
status: ComponentStatus | int = ComponentStatus.UNSPECIFIED
@@ -89,7 +90,8 @@ class Component: # pylint: disable=too-many-instance-attributes
8990
Note:
9091
This should not be used normally, it is only useful when accessing a newer
9192
version of the API where the client doesn't know about the new metadata fields
92-
yet.
93+
yet (i.e. for use with
94+
[`UnrecognizedComponent`][frequenz.client.microgrid.component.UnrecognizedComponent]).
9395
"""
9496

9597
def __new__(cls, *_: Any, **__: Any) -> Self:
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Unknown component."""
5+
6+
import dataclasses
7+
from typing import Any, Literal, Self
8+
9+
from ._category import ComponentCategory
10+
from ._component import Component
11+
12+
13+
@dataclasses.dataclass(frozen=True, kw_only=True)
14+
class ProblematicComponent(Component):
15+
"""An abstract component with a problem."""
16+
17+
# pylint: disable-next=unused-argument
18+
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
19+
"""Prevent instantiation of this class."""
20+
if cls is ProblematicComponent:
21+
raise TypeError(f"Cannot instantiate {cls.__name__} directly")
22+
return super().__new__(cls)
23+
24+
25+
@dataclasses.dataclass(frozen=True, kw_only=True)
26+
class UnspecifiedComponent(ProblematicComponent):
27+
"""A component of unspecified type."""
28+
29+
category: Literal[ComponentCategory.UNSPECIFIED] = ComponentCategory.UNSPECIFIED
30+
"""The category of this component."""
31+
32+
33+
@dataclasses.dataclass(frozen=True, kw_only=True)
34+
class UnrecognizedComponent(ProblematicComponent):
35+
"""A component of an unrecognized type."""
36+
37+
category: int
38+
"""The category of this component."""
39+
40+
41+
@dataclasses.dataclass(frozen=True, kw_only=True)
42+
class MismatchedCategoryComponent(ProblematicComponent):
43+
"""A component with a mismatch in the category.
44+
45+
This component declared a category but carries category specific metadata that
46+
doesn't match the declared category.
47+
"""
48+
49+
category: ComponentCategory | int
50+
"""The category of this component."""
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Tests for ProblematicComponent components."""
5+
6+
import pytest
7+
from frequenz.client.common.microgrid import MicrogridId
8+
from frequenz.client.common.microgrid.components import ComponentId
9+
10+
from frequenz.client.microgrid.component import (
11+
ComponentCategory,
12+
ComponentStatus,
13+
MismatchedCategoryComponent,
14+
ProblematicComponent,
15+
UnrecognizedComponent,
16+
UnspecifiedComponent,
17+
)
18+
19+
20+
@pytest.fixture
21+
def component_id() -> ComponentId:
22+
"""Provide a test component ID."""
23+
return ComponentId(42)
24+
25+
26+
@pytest.fixture
27+
def microgrid_id() -> MicrogridId:
28+
"""Provide a test microgrid ID."""
29+
return MicrogridId(1)
30+
31+
32+
def test_abstract_problematic_component_cannot_be_instantiated(
33+
component_id: ComponentId, microgrid_id: MicrogridId
34+
) -> None:
35+
"""Test that ProblematicComponent base class cannot be instantiated."""
36+
with pytest.raises(
37+
TypeError, match="Cannot instantiate ProblematicComponent directly"
38+
):
39+
ProblematicComponent(
40+
id=component_id,
41+
microgrid_id=microgrid_id,
42+
name="test_problematic",
43+
manufacturer="test_manufacturer",
44+
model_name="test_model",
45+
status=ComponentStatus.ACTIVE,
46+
category=ComponentCategory.UNSPECIFIED,
47+
)
48+
49+
50+
def test_unspecified_component(
51+
component_id: ComponentId, microgrid_id: MicrogridId
52+
) -> None:
53+
"""Test initialization and properties of UnspecifiedComponent."""
54+
component = UnspecifiedComponent(
55+
id=component_id,
56+
microgrid_id=microgrid_id,
57+
name="unspecified_component",
58+
manufacturer="test_manufacturer",
59+
model_name="test_model",
60+
status=ComponentStatus.ACTIVE,
61+
)
62+
63+
assert component.id == component_id
64+
assert component.microgrid_id == microgrid_id
65+
assert component.name == "unspecified_component"
66+
assert component.manufacturer == "test_manufacturer"
67+
assert component.model_name == "test_model"
68+
assert component.status == ComponentStatus.ACTIVE
69+
assert component.category == ComponentCategory.UNSPECIFIED
70+
71+
72+
def test_mismatched_category_component_with_known_category(
73+
component_id: ComponentId, microgrid_id: MicrogridId
74+
) -> None:
75+
"""Test MismatchedCategoryComponent with a known ComponentCategory."""
76+
expected_category = ComponentCategory.BATTERY
77+
component = MismatchedCategoryComponent(
78+
id=component_id,
79+
microgrid_id=microgrid_id,
80+
name="mismatched_battery",
81+
manufacturer="test_manufacturer",
82+
model_name="test_model",
83+
status=ComponentStatus.ACTIVE,
84+
category=expected_category,
85+
)
86+
87+
assert component.id == component_id
88+
assert component.microgrid_id == microgrid_id
89+
assert component.name == "mismatched_battery"
90+
assert component.manufacturer == "test_manufacturer"
91+
assert component.model_name == "test_model"
92+
assert component.status == ComponentStatus.ACTIVE
93+
assert component.category == expected_category
94+
95+
96+
def test_mismatched_category_component_with_unrecognized_category(
97+
component_id: ComponentId, microgrid_id: MicrogridId
98+
) -> None:
99+
"""Test MismatchedCategoryComponent with an unrecognized integer category."""
100+
expected_category = 999
101+
component = MismatchedCategoryComponent(
102+
id=component_id,
103+
microgrid_id=microgrid_id,
104+
name="mismatched_unrecognized",
105+
manufacturer="test_manufacturer",
106+
model_name="test_model",
107+
status=ComponentStatus.ACTIVE,
108+
category=expected_category,
109+
)
110+
111+
assert component.id == component_id
112+
assert component.microgrid_id == microgrid_id
113+
assert component.name == "mismatched_unrecognized"
114+
assert component.manufacturer == "test_manufacturer"
115+
assert component.model_name == "test_model"
116+
assert component.status == ComponentStatus.ACTIVE
117+
assert component.category == expected_category
118+
119+
120+
def test_unrecognized_component_type(
121+
component_id: ComponentId, microgrid_id: MicrogridId
122+
) -> None:
123+
"""Test initialization and properties of UnrecognizedComponent type."""
124+
component = UnrecognizedComponent(
125+
id=component_id,
126+
microgrid_id=microgrid_id,
127+
name="unrecognized_component",
128+
manufacturer="test_manufacturer",
129+
model_name="test_model",
130+
status=ComponentStatus.ACTIVE,
131+
category=999,
132+
)
133+
134+
assert component.id == component_id
135+
assert component.microgrid_id == microgrid_id
136+
assert component.name == "unrecognized_component"
137+
assert component.manufacturer == "test_manufacturer"
138+
assert component.model_name == "test_model"
139+
assert component.status == ComponentStatus.ACTIVE
140+
assert component.category == 999

0 commit comments

Comments
 (0)