Skip to content

Commit d6b565c

Browse files
committed
Implement ListConnections
Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 00bebaf commit d6b565c

File tree

3 files changed

+189
-0
lines changed

3 files changed

+189
-0
lines changed

src/frequenz/client/microgrid/_client.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from .component._category import ComponentCategory
2020
from .component._component import ComponentTypes
2121
from .component._component_proto import component_from_proto
22+
from .component._connection import ComponentConnection
23+
from .component._connection_proto import component_connection_from_proto
2224

2325
DEFAULT_GRPC_CALL_TIMEOUT = 60.0
2426
"""The default timeout for gRPC calls made by this client (in seconds)."""
@@ -155,6 +157,60 @@ async def list_components( # noqa: DOC502 (raises ApiClientError indirectly)
155157

156158
return map(component_from_proto, component_list.components)
157159

160+
async def list_connections( # noqa: DOC502 (raises ApiClientError indirectly)
161+
self,
162+
*,
163+
sources: Iterable[ComponentId | Component] = (),
164+
destinations: Iterable[ComponentId | Component] = (),
165+
) -> Iterable[ComponentConnection]:
166+
"""Fetch all the connections present in the local microgrid.
167+
168+
Electrical components are a part of a microgrid's electrical infrastructure
169+
are can be connected to each other to form an electrical circuit, which can
170+
then be represented as a graph.
171+
172+
The direction of a connection is always away from the grid endpoint, i.e.
173+
aligned with the direction of positive current according to the passive sign
174+
convention: https://en.wikipedia.org/wiki/Passive_sign_convention
175+
176+
The request may be filtered by `source`/`destination` component(s) of individual
177+
connections. If provided, the `sources` and `destinations` filters have an
178+
`AND` relationship between each other, meaning that they are applied serially,
179+
but an `OR` relationship with other elements in the same list.
180+
181+
Example:
182+
If `sources = {1, 2, 3}`, and `destinations = {4,
183+
5, 6}`, then the result should have all the connections where:
184+
185+
* Each `source` component ID is either `1`, `2`, OR `3`; **AND**
186+
* Each `destination` component ID is either `4`, `5`, OR `6`.
187+
188+
Args:
189+
sources: The component from which the connections originate.
190+
destinations: The component at which the connections terminate.
191+
192+
Returns:
193+
Iterator whose elements are all the connections in the local microgrid.
194+
195+
Raises:
196+
ApiClientError: If the are any errors communicating with the Microgrid API,
197+
most likely a subclass of
198+
[GrpcError][frequenz.client.microgrid.GrpcError].
199+
"""
200+
connection_list = await client.call_stub_method(
201+
self,
202+
lambda: self._async_stub.ListConnections(
203+
microgrid_pb2.ListConnectionsRequest(
204+
starts=map(_get_component_id, sources),
205+
ends=map(_get_component_id, destinations),
206+
),
207+
timeout=int(DEFAULT_GRPC_CALL_TIMEOUT),
208+
),
209+
method_name="ListConnections",
210+
)
211+
212+
return map(component_connection_from_proto, connection_list.connections)
213+
158214

159215
def _get_component_id(component: ComponentId | Component) -> int:
160216
"""Get the component ID from a component or component ID."""
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Component connection."""
5+
6+
import dataclasses
7+
from functools import cached_property
8+
9+
from .._id import ComponentId
10+
from .._lifetime import Lifetime
11+
12+
13+
@dataclasses.dataclass(frozen=True, kw_only=True)
14+
class ComponentConnection:
15+
"""A single electrical link between two components within a microgrid.
16+
17+
A component connection represents the physical wiring as viewed from the grid
18+
connection point, if one exists, or from the islanding point, in case of an islanded
19+
microgrids.
20+
21+
Note: Physical Representation
22+
This message is not about data flow but rather about the physical
23+
electrical connections between components. Therefore, the IDs for the
24+
source and destination components correspond to the actual setup within
25+
the microgrid.
26+
27+
Note: Direction
28+
The direction of the connection follows the flow of current away from the
29+
grid connection point, or in case of islands, away from the islanding
30+
point. This direction is aligned with positive current according to the
31+
[Passive Sign Convention]
32+
(https://en.wikipedia.org/wiki/Passive_sign_convention).
33+
34+
Note: Historical Data
35+
The timestamps of when a connection was created and terminated allows for
36+
tracking the changes over time to a microgrid, providing insights into
37+
when and how the microgrid infrastructure has been modified.
38+
"""
39+
40+
source: ComponentId
41+
"""The unique identifier of the component where the connection originates.
42+
43+
This is aligned with the direction of current flow away from the grid connection
44+
point, or in case of islands, away from the islanding point.
45+
"""
46+
47+
destination: ComponentId
48+
"""The unique ID of the component where the connection terminates.
49+
50+
This is the component towards which the current flows.
51+
"""
52+
53+
operational_lifetime: Lifetime
54+
"""The operational lifetime of the connection."""
55+
56+
@cached_property
57+
def active(self) -> bool:
58+
"""Whether this connection is currently active."""
59+
return self.operational_lifetime.active
60+
61+
def __str__(self) -> str:
62+
"""Return a human-readable string representation of this instance."""
63+
return f"{self.source}->{self.destination}"
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Loading of ComponentConnection objects from protobuf messages."""
5+
6+
import logging
7+
8+
from frequenz.api.common.v1.microgrid.components import components_pb2
9+
10+
from .._id import ComponentId
11+
from .._lifetime import Lifetime
12+
from .._lifetime_proto import lifetime_from_proto
13+
from ._connection import ComponentConnection
14+
15+
_logger = logging.getLogger(__name__)
16+
17+
18+
def component_connection_from_proto(
19+
message: components_pb2.ComponentConnection,
20+
) -> ComponentConnection:
21+
"""Create a `ComponentConnection` from a protobuf message."""
22+
major_issues: list[str] = []
23+
minor_issues: list[str] = []
24+
25+
source_component_id = ComponentId(message.source_component_id)
26+
destination_component_id = ComponentId(message.destination_component_id)
27+
lifetime = _get_operational_lifetime_from_proto(
28+
message, major_issues=major_issues, minor_issues=minor_issues
29+
)
30+
31+
if major_issues:
32+
_logger.warning(
33+
"Found issues in component connection: %s | Protobuf message:\n%s",
34+
", ".join(major_issues),
35+
message,
36+
)
37+
if minor_issues:
38+
_logger.debug(
39+
"Found minor issues in component connection: %s | Protobuf message:\n%s",
40+
", ".join(minor_issues),
41+
message,
42+
)
43+
44+
return ComponentConnection(
45+
source=source_component_id,
46+
destination=destination_component_id,
47+
operational_lifetime=lifetime,
48+
)
49+
50+
51+
def _get_operational_lifetime_from_proto(
52+
message: components_pb2.ComponentConnection,
53+
*,
54+
major_issues: list[str],
55+
minor_issues: list[str],
56+
) -> Lifetime:
57+
"""Get the operational lifetime from a protobuf message."""
58+
if message.HasField("operational_lifetime"):
59+
try:
60+
return lifetime_from_proto(message.operational_lifetime)
61+
except ValueError as exc:
62+
major_issues.append(
63+
f"invalid operational lifetime ({exc}), considering it as missing "
64+
"(i.e. always operational)",
65+
)
66+
else:
67+
minor_issues.append(
68+
"missing operational lifetime, considering it always operational",
69+
)
70+
return Lifetime()

0 commit comments

Comments
 (0)