Skip to content

Commit f85abb5

Browse files
committed
Implement ListComponents
Implement the `ListComponents` RPC call in the `MicrogridApiClient`. To do this we also need to implement the data structures that will be used to wrap the data returned by the API. These wrappers use a more Pythonic approach than the protobuf messages, and translate most enums that encode types of components to concrete Python classes. This way, we can use the type system to ensure we are using the right type of component in the right place, and enables using match statements to handle different types of components in an exhaustive way. Most wrappers should probably be moved to `api-common` in the future. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 9b6bb03 commit f85abb5

29 files changed

+2304
-3
lines changed

src/frequenz/client/microgrid/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
UnrecognizedGrpcStatus,
3333
)
3434
from ._id import ComponentId, EnterpriseId, MicrogridId
35+
from ._lifetime import Lifetime
3536
from ._location import Location
3637
from ._microgrid_info import MicrogridInfo, MicrogridStatus
3738

@@ -48,6 +49,7 @@
4849
"GrpcError",
4950
"InternalError",
5051
"InvalidArgument",
52+
"Lifetime",
5153
"Location",
5254
"MicrogridApiClient",
5355
"MicrogridId",

src/frequenz/client/microgrid/_client.py

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,27 @@
55

66
from __future__ import annotations
77

8+
from collections.abc import Iterable
89
from dataclasses import replace
9-
from typing import Any
10+
from typing import Any, assert_never
1011

11-
from frequenz.api.microgrid.v1 import microgrid_pb2_grpc
12+
from frequenz.api.common.v1.microgrid.components import components_pb2
13+
from frequenz.api.microgrid.v1 import microgrid_pb2, microgrid_pb2_grpc
1214
from frequenz.client.base import channel, client, retry, streaming
1315
from google.protobuf.empty_pb2 import Empty
1416

1517
from ._exception import ClientNotConnected
1618
from ._id import ComponentId
1719
from ._microgrid_info import MicrogridInfo
1820
from ._microgrid_info_proto import microgrid_info_from_proto
21+
from .component._base import Component
22+
from .component._category import ComponentCategory
23+
from .component._component import ComponentTypes
24+
from .component._component_proto import component_from_proto
1925

2026
DEFAULT_GRPC_CALL_TIMEOUT = 60.0
2127
"""The default timeout for gRPC calls made by this client (in seconds)."""
2228

23-
2429
DEFAULT_CHANNEL_OPTIONS = replace(
2530
channel.ChannelOptions(), ssl=channel.SslOptions(enabled=False)
2631
)
@@ -109,3 +114,82 @@ async def get_microgrid_info( # noqa: DOC502 (raises ApiClientError indirectly)
109114
)
110115

111116
return microgrid_info_from_proto(microgrid.microgrid)
117+
118+
async def list_components( # noqa: DOC502 (raises ApiClientError indirectly)
119+
self,
120+
*,
121+
components: Iterable[ComponentId | Component] = (),
122+
categories: Iterable[ComponentCategory | int] = (),
123+
) -> Iterable[ComponentTypes]:
124+
"""Fetch all the components present in the local microgrid.
125+
126+
Electrical components are a part of a microgrid's electrical infrastructure
127+
are can be connected to each other to form an electrical circuit, which can
128+
then be represented as a graph.
129+
130+
If provided, the filters for component and categories have an `AND`
131+
relationship with one another, meaning that they are applied serially,
132+
but the elements within a single filter list have an `OR` relationship with
133+
each other.
134+
135+
Example:
136+
If `ids = {1, 2, 3}`, and `categories = {ComponentCategory.INVERTER,
137+
ComponentCategory.BATTERY}`, then the results will consist of elements that
138+
have:
139+
140+
* The IDs 1, `OR` 2, `OR` 3; `AND`
141+
* Are of the categories `ComponentCategory.INVERTER` `OR`
142+
`ComponentCategory.BATTERY`.
143+
144+
If a filter list is empty, then that filter is not applied.
145+
146+
Args:
147+
components: The components to fetch. See the method description for details.
148+
categories: The categories of the components to fetch. See the method
149+
description for details.
150+
151+
Returns:
152+
Iterator whose elements are all the components in the local microgrid.
153+
154+
Raises:
155+
ApiClientError: If the are any errors communicating with the Microgrid API,
156+
most likely a subclass of
157+
[GrpcError][frequenz.client.microgrid.GrpcError].
158+
"""
159+
component_list = await client.call_stub_method(
160+
self,
161+
lambda: self.stub.ListComponents(
162+
microgrid_pb2.ListComponentsRequest(
163+
component_ids=map(_get_component_id, components),
164+
categories=map(_get_category_value, categories),
165+
),
166+
timeout=int(DEFAULT_GRPC_CALL_TIMEOUT),
167+
),
168+
method_name="ListComponents",
169+
)
170+
171+
return map(component_from_proto, component_list.components)
172+
173+
174+
def _get_component_id(component: ComponentId | Component) -> int:
175+
"""Get the component ID from a component or component ID."""
176+
match component:
177+
case ComponentId():
178+
return int(component)
179+
case Component():
180+
return int(component.id)
181+
case unexpected:
182+
assert_never(unexpected)
183+
184+
185+
def _get_category_value(
186+
category: ComponentCategory | int,
187+
) -> components_pb2.ComponentCategory.ValueType:
188+
"""Get the category value from a component or component category."""
189+
match category:
190+
case ComponentCategory():
191+
return components_pb2.ComponentCategory.ValueType(category.value)
192+
case int():
193+
return components_pb2.ComponentCategory.ValueType(category)
194+
case unexpected:
195+
assert_never(unexpected)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Lifetime of a microgrid asset."""
5+
6+
7+
from dataclasses import dataclass
8+
from datetime import datetime
9+
from functools import cached_property
10+
11+
12+
@dataclass(frozen=True, kw_only=True)
13+
class Lifetime:
14+
"""An active operational period of a microgrid asset.
15+
16+
Warning:
17+
The [`end`][frequenz.client.microgrid.Lifetime.end] timestamp indicates that the
18+
asset has been permanently removed from the system.
19+
"""
20+
21+
start: datetime | None = None
22+
"""The moment when the asset became operationally active.
23+
24+
If `None`, the asset is considered to be active in any past moment previous to the
25+
[`end`][frequenz.client.microgrid.Lifetime.end].
26+
"""
27+
28+
end: datetime | None = None
29+
"""The moment when the asset's operational activity ceased.
30+
31+
If `None`, the asset is considered to be active with no plans to be deactivated.
32+
"""
33+
34+
def __post_init__(self) -> None:
35+
"""Validate this lifetime."""
36+
if self.start is not None and self.end is not None and self.start > self.end:
37+
raise ValueError("Start must be before or equal to end.")
38+
39+
@cached_property
40+
def active(self) -> bool:
41+
"""Whether this lifetime is currently active."""
42+
now = datetime.now()
43+
if self.start is not None and self.start > now:
44+
return False
45+
return self.end is None or self.end >= now
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Loading of Lifetime objects from protobuf messages."""
5+
6+
from frequenz.api.common.v1.microgrid import lifetime_pb2
7+
from frequenz.client.base.conversion import to_datetime
8+
9+
from ._lifetime import Lifetime
10+
11+
12+
def lifetime_from_proto(
13+
message: lifetime_pb2.Lifetime,
14+
) -> Lifetime:
15+
"""Create a [`Lifetime`][frequenz.client.microgrid.Lifetime] from a protobuf message."""
16+
start = (
17+
to_datetime(message.start_timestamp)
18+
if message.HasField("start_timestamp")
19+
else None
20+
)
21+
end = (
22+
to_datetime(message.end_timestamp)
23+
if message.HasField("end_timestamp")
24+
else None
25+
)
26+
return Lifetime(start=start, end=end)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""All classes and functions related to microgrid components."""
5+
6+
from ._base import Component
7+
from ._battery import (
8+
Battery,
9+
BatteryType,
10+
BatteryTypes,
11+
LiIonBattery,
12+
NaIonBattery,
13+
UnrecognizedBattery,
14+
UnspecifiedBattery,
15+
)
16+
from ._category import ComponentCategory
17+
from ._chp import Chp
18+
from ._component import (
19+
ComponentTypes,
20+
ProblematicComponentTypes,
21+
UnrecognizedComponentTypes,
22+
UnspecifiedComponentTypes,
23+
)
24+
from ._converter import Converter
25+
from ._crypto_miner import CryptoMiner
26+
from ._electrolyzer import Electrolyzer
27+
from ._ev_charger import (
28+
AcEvCharger,
29+
DcEvCharger,
30+
EvCharger,
31+
EvChargerType,
32+
EvChargerTypes,
33+
HybridEvCharger,
34+
UnrecognizedEvCharger,
35+
UnspecifiedEvCharger,
36+
)
37+
from ._fuse import Fuse
38+
from ._grid_connection_point import GridConnectionPoint
39+
from ._hvac import Hvac
40+
from ._inverter import (
41+
BatteryInverter,
42+
HybridInverter,
43+
Inverter,
44+
InverterType,
45+
SolarInverter,
46+
UnrecognizedInverter,
47+
UnspecifiedInverter,
48+
)
49+
from ._meter import Meter
50+
from ._precharger import Precharger
51+
from ._problematic import (
52+
MismatchedCategoryComponent,
53+
ProblematicComponent,
54+
UnrecognizedComponent,
55+
UnspecifiedComponent,
56+
)
57+
from ._relay import Relay
58+
from ._status import ComponentStatus
59+
from ._voltage_transformer import VoltageTransformer
60+
61+
__all__ = [
62+
"AcEvCharger",
63+
"Battery",
64+
"BatteryInverter",
65+
"BatteryType",
66+
"BatteryTypes",
67+
"Chp",
68+
"Component",
69+
"ComponentCategory",
70+
"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",
106+
]

0 commit comments

Comments
 (0)