Skip to content

Commit f6dfe48

Browse files
authored
Implement ListComponents (#165)
2 parents bd5828f + dad8574 commit f6dfe48

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+4013
-7
lines changed

RELEASE_NOTES.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
## Summary
44

5-
<!-- Here goes a general summary of what this release is about -->
5+
This release is a major breaking change, as we jump to the API specification version 0.17.x, which introduces big and fundamental breaking changes. This also starts using the `v1` namespace in `frequenz-api-common`, which also introduces major breaking changes. It would be very hard to detail all the API changes here, please refer to the [Microgrid API releases](https://github.com/frequenz-floss/frequenz-api-microgrid/releases) and [Common API releases](https://github.com/frequenz-floss/frequenz-api-common/releases).
66

77
## Upgrading
88

9-
<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
9+
- `MicrogridApiClient`:
10+
11+
* The client now follows the v0.17 API names, so most methods changed names and signatures.
1012

1113
## New Features
1214

src/frequenz/client/microgrid/_client.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from typing import Any, assert_never
1616

1717
from frequenz.api.common.v1.metrics import bounds_pb2, metric_sample_pb2
18+
from frequenz.api.common.v1.microgrid.components import components_pb2
1819
from frequenz.api.microgrid.v1 import microgrid_pb2, microgrid_pb2_grpc
1920
from frequenz.client.base import channel, client, conversion, retry, streaming
2021
from frequenz.client.common.microgrid.components import ComponentId
@@ -24,7 +25,10 @@
2425
from ._exception import ClientNotConnected
2526
from ._microgrid_info import MicrogridInfo
2627
from ._microgrid_info_proto import microgrid_info_from_proto
28+
from .component._category import ComponentCategory
2729
from .component._component import Component
30+
from .component._component_proto import component_from_proto
31+
from .component._types import ComponentTypes
2832
from .metrics._bounds import Bounds
2933
from .metrics._metric import Metric
3034

@@ -162,6 +166,61 @@ async def get_microgrid_info( # noqa: DOC502 (raises ApiClientError indirectly)
162166

163167
return microgrid_info_from_proto(microgrid.microgrid)
164168

169+
async def list_components( # noqa: DOC502 (raises ApiClientError indirectly)
170+
self,
171+
*,
172+
components: Iterable[ComponentId | Component] = (),
173+
categories: Iterable[ComponentCategory | int] = (),
174+
) -> Iterable[ComponentTypes]:
175+
"""Fetch all the components present in the local microgrid.
176+
177+
Electrical components are a part of a microgrid's electrical infrastructure
178+
are can be connected to each other to form an electrical circuit, which can
179+
then be represented as a graph.
180+
181+
If provided, the filters for component and categories have an `AND`
182+
relationship with one another, meaning that they are applied serially,
183+
but the elements within a single filter list have an `OR` relationship with
184+
each other.
185+
186+
Example:
187+
If `ids = {1, 2, 3}`, and `categories = {ComponentCategory.INVERTER,
188+
ComponentCategory.BATTERY}`, then the results will consist of elements that
189+
have:
190+
191+
* The IDs 1, `OR` 2, `OR` 3; `AND`
192+
* Are of the categories `ComponentCategory.INVERTER` `OR`
193+
`ComponentCategory.BATTERY`.
194+
195+
If a filter list is empty, then that filter is not applied.
196+
197+
Args:
198+
components: The components to fetch. See the method description for details.
199+
categories: The categories of the components to fetch. See the method
200+
description for details.
201+
202+
Returns:
203+
Iterator whose elements are all the components in the local microgrid.
204+
205+
Raises:
206+
ApiClientError: If the are any errors communicating with the Microgrid API,
207+
most likely a subclass of
208+
[GrpcError][frequenz.client.microgrid.GrpcError].
209+
"""
210+
component_list = await client.call_stub_method(
211+
self,
212+
lambda: self.stub.ListComponents(
213+
microgrid_pb2.ListComponentsRequest(
214+
component_ids=map(_get_component_id, components),
215+
categories=map(_get_category_value, categories),
216+
),
217+
timeout=DEFAULT_GRPC_CALL_TIMEOUT,
218+
),
219+
method_name="ListComponents",
220+
)
221+
222+
return map(component_from_proto, component_list.components)
223+
165224
async def set_component_power_active( # noqa: DOC502 (raises ApiClientError indirectly)
166225
self,
167226
component: ComponentId | Component,
@@ -456,6 +515,19 @@ def _get_metric_value(metric: Metric | int) -> metric_sample_pb2.Metric.ValueTyp
456515
assert_never(unexpected)
457516

458517

518+
def _get_category_value(
519+
category: ComponentCategory | int,
520+
) -> components_pb2.ComponentCategory.ValueType:
521+
"""Get the category value from a component or component category."""
522+
match category:
523+
case ComponentCategory():
524+
return components_pb2.ComponentCategory.ValueType(category.value)
525+
case int():
526+
return components_pb2.ComponentCategory.ValueType(category)
527+
case unexpected:
528+
assert_never(unexpected)
529+
530+
459531
def _delta_to_seconds(delta: timedelta | None) -> int | None:
460532
"""Convert a `timedelta` to seconds (or `None` if `None`)."""
461533
return round(delta.total_seconds()) if delta is not None else None

src/frequenz/client/microgrid/_lifetime.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ class Lifetime:
3333
def __post_init__(self) -> None:
3434
"""Validate this lifetime."""
3535
if self.start is not None and self.end is not None and self.start > self.end:
36-
raise ValueError("Start must be before or equal to end.")
36+
raise ValueError(
37+
f"Start ({self.start}) must be before or equal to end ({self.end})"
38+
)
3739

3840
def is_operational_at(self, timestamp: datetime) -> bool:
3941
"""Check whether this lifetime is active at a specific timestamp."""

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,104 @@
33

44
"""All classes and functions related to microgrid components."""
55

6+
from ._battery import (
7+
Battery,
8+
BatteryType,
9+
BatteryTypes,
10+
LiIonBattery,
11+
NaIonBattery,
12+
UnrecognizedBattery,
13+
UnspecifiedBattery,
14+
)
615
from ._category import ComponentCategory
16+
from ._chp import Chp
717
from ._component import Component
18+
from ._converter import Converter
19+
from ._crypto_miner import CryptoMiner
20+
from ._electrolyzer import Electrolyzer
21+
from ._ev_charger import (
22+
AcEvCharger,
23+
DcEvCharger,
24+
EvCharger,
25+
EvChargerType,
26+
EvChargerTypes,
27+
HybridEvCharger,
28+
UnrecognizedEvCharger,
29+
UnspecifiedEvCharger,
30+
)
31+
from ._fuse import Fuse
32+
from ._grid_connection_point import GridConnectionPoint
33+
from ._hvac import Hvac
34+
from ._inverter import (
35+
BatteryInverter,
36+
HybridInverter,
37+
Inverter,
38+
InverterType,
39+
SolarInverter,
40+
UnrecognizedInverter,
41+
UnspecifiedInverter,
42+
)
43+
from ._meter import Meter
44+
from ._precharger import Precharger
45+
from ._problematic import (
46+
MismatchedCategoryComponent,
47+
ProblematicComponent,
48+
UnrecognizedComponent,
49+
UnspecifiedComponent,
50+
)
51+
from ._relay import Relay
852
from ._status import ComponentStatus
53+
from ._types import (
54+
ComponentTypes,
55+
ProblematicComponentTypes,
56+
UnrecognizedComponentTypes,
57+
UnspecifiedComponentTypes,
58+
)
59+
from ._voltage_transformer import VoltageTransformer
960

1061
__all__ = [
62+
"AcEvCharger",
63+
"Battery",
64+
"BatteryInverter",
65+
"BatteryType",
66+
"BatteryTypes",
67+
"Chp",
1168
"Component",
1269
"ComponentCategory",
1370
"ComponentStatus",
71+
"ComponentTypes",
72+
"Converter",
73+
"CryptoMiner",
74+
"DcEvCharger",
75+
"Electrolyzer",
76+
"EvCharger",
77+
"EvChargerType",
78+
"EvChargerTypes",
79+
"Fuse",
80+
"GridConnectionPoint",
81+
"Hvac",
82+
"HybridEvCharger",
83+
"HybridInverter",
84+
"Inverter",
85+
"InverterType",
86+
"LiIonBattery",
87+
"Meter",
88+
"MismatchedCategoryComponent",
89+
"NaIonBattery",
90+
"Precharger",
91+
"ProblematicComponent",
92+
"ProblematicComponentTypes",
93+
"Relay",
94+
"SolarInverter",
95+
"UnrecognizedBattery",
96+
"UnrecognizedComponent",
97+
"UnrecognizedComponentTypes",
98+
"UnrecognizedEvCharger",
99+
"UnrecognizedInverter",
100+
"UnspecifiedBattery",
101+
"UnspecifiedComponent",
102+
"UnspecifiedComponentTypes",
103+
"UnspecifiedEvCharger",
104+
"UnspecifiedInverter",
105+
"VoltageTransformer",
14106
]
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Battery component."""
5+
6+
import dataclasses
7+
import enum
8+
from typing import Any, Literal, Self, TypeAlias
9+
10+
from frequenz.api.common.v1.microgrid.components import battery_pb2
11+
12+
from ._category import ComponentCategory
13+
from ._component import Component
14+
15+
16+
@enum.unique
17+
class BatteryType(enum.Enum):
18+
"""The known types of batteries."""
19+
20+
UNSPECIFIED = battery_pb2.BATTERY_TYPE_UNSPECIFIED
21+
"""The battery type is unspecified."""
22+
23+
LI_ION = battery_pb2.BATTERY_TYPE_LI_ION
24+
"""Lithium-ion (Li-ion) battery."""
25+
26+
NA_ION = battery_pb2.BATTERY_TYPE_NA_ION
27+
"""Sodium-ion (Na-ion) battery."""
28+
29+
30+
@dataclasses.dataclass(frozen=True, kw_only=True)
31+
class Battery(Component):
32+
"""An abstract battery component."""
33+
34+
category: Literal[ComponentCategory.BATTERY] = ComponentCategory.BATTERY
35+
"""The category of this component.
36+
37+
Note:
38+
This should not be used normally, you should test if a component
39+
[`isinstance`][] of a concrete component class instead.
40+
41+
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 (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.
45+
"""
46+
47+
type: BatteryType | int
48+
"""The type of this battery.
49+
50+
Note:
51+
This should not be used normally, you should test if a battery
52+
[`isinstance`][] of a concrete battery class instead.
53+
54+
It is only provided for using with a newer version of the API where the client
55+
doesn't know about the new battery type yet (i.e. for use with
56+
[`UnrecognizedBattery`][frequenz.client.microgrid.component.UnrecognizedBattery]).
57+
"""
58+
59+
# pylint: disable-next=unused-argument
60+
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
61+
"""Prevent instantiation of this class."""
62+
if cls is Battery:
63+
raise TypeError(f"Cannot instantiate {cls.__name__} directly")
64+
return super().__new__(cls)
65+
66+
67+
@dataclasses.dataclass(frozen=True, kw_only=True)
68+
class UnspecifiedBattery(Battery):
69+
"""A battery of a unspecified type."""
70+
71+
type: Literal[BatteryType.UNSPECIFIED] = BatteryType.UNSPECIFIED
72+
"""The type of this battery.
73+
74+
Note:
75+
This should not be used normally, you should test if a battery
76+
[`isinstance`][] of a concrete battery class instead.
77+
78+
It is only provided for using with a newer version of the API where the client
79+
doesn't know about the new battery type yet (i.e. for use with
80+
[`UnrecognizedBattery`][frequenz.client.microgrid.component.UnrecognizedBattery]).
81+
"""
82+
83+
84+
@dataclasses.dataclass(frozen=True, kw_only=True)
85+
class LiIonBattery(Battery):
86+
"""A Li-ion battery."""
87+
88+
type: Literal[BatteryType.LI_ION] = BatteryType.LI_ION
89+
"""The type of this battery.
90+
91+
Note:
92+
This should not be used normally, you should test if a battery
93+
[`isinstance`][] of a concrete battery class instead.
94+
95+
It is only provided for using with a newer version of the API where the client
96+
doesn't know about the new battery type yet (i.e. for use with
97+
[`UnrecognizedBattery`][frequenz.client.microgrid.component.UnrecognizedBattery]).
98+
"""
99+
100+
101+
@dataclasses.dataclass(frozen=True, kw_only=True)
102+
class NaIonBattery(Battery):
103+
"""A Na-ion battery."""
104+
105+
type: Literal[BatteryType.NA_ION] = BatteryType.NA_ION
106+
"""The type of this battery.
107+
108+
Note:
109+
This should not be used normally, you should test if a battery
110+
[`isinstance`][] of a concrete battery class instead.
111+
112+
It is only provided for using with a newer version of the API where the client
113+
doesn't know about the new battery type yet (i.e. for use with
114+
[`UnrecognizedBattery`][frequenz.client.microgrid.component.UnrecognizedBattery]).
115+
"""
116+
117+
118+
@dataclasses.dataclass(frozen=True, kw_only=True)
119+
class UnrecognizedBattery(Battery):
120+
"""A battery of an unrecognized type."""
121+
122+
type: int
123+
"""The unrecognized type of this battery."""
124+
125+
126+
BatteryTypes: TypeAlias = (
127+
LiIonBattery | NaIonBattery | UnrecognizedBattery | UnspecifiedBattery
128+
)
129+
"""All possible battery types."""
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""CHP component."""
5+
6+
import dataclasses
7+
from typing import Literal
8+
9+
from ._category import ComponentCategory
10+
from ._component import Component
11+
12+
13+
@dataclasses.dataclass(frozen=True, kw_only=True)
14+
class Chp(Component):
15+
"""A combined heat and power (CHP) component."""
16+
17+
category: Literal[ComponentCategory.CHP] = ComponentCategory.CHP
18+
"""The category of this component."""

0 commit comments

Comments
 (0)