Skip to content

Commit 4f11a39

Browse files
committed
feat(assets): List electrical component connections
Add a new client method to retrieve microgrid electrical components connections. Signed-off-by: eduardiazf <[email protected]>
1 parent cd8cc65 commit 4f11a39

File tree

6 files changed

+360
-5
lines changed

6 files changed

+360
-5
lines changed

src/frequenz/client/assets/_client.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@
1313
from frequenz.client.base import channel
1414
from frequenz.client.base.client import BaseApiClient, call_stub_method
1515

16-
from frequenz.client.assets.electrical_component._electrical_component import (
17-
ElectricalComponent,
18-
)
19-
2016
from ._microgrid import Microgrid
2117
from ._microgrid_proto import microgrid_from_proto
18+
from .electrical_component._connection import ComponentConnection
19+
from .electrical_component._connection_proto import component_connection_from_proto
20+
from .electrical_component._electrical_component import ElectricalComponent
2221
from .electrical_component._electrical_component_proto import electrical_component_proto
2322
from .exceptions import ClientNotConnected
2423

@@ -138,3 +137,47 @@ async def list_microgrid_electrical_components(
138137
return [
139138
electrical_component_proto(component) for component in response.components
140139
]
140+
141+
async def list_microgrid_electrical_component_connections(
142+
self,
143+
microgrid_id: int,
144+
source_component_ids: list[int] | None = None,
145+
destination_component_ids: list[int] | None = None,
146+
) -> list[ComponentConnection | None]:
147+
"""
148+
Get the electrical component connections of a microgrid.
149+
150+
Args:
151+
microgrid_id: The ID of the microgrid to get the electrical
152+
component connections of.
153+
source_component_ids: Only return connections that originate from
154+
these component IDs. If None or empty, no filtering is applied.
155+
destination_component_ids: Only return connections that terminate at
156+
these component IDs. If None or empty, no filtering is applied.
157+
158+
Returns:
159+
The electrical component connections of the microgrid.
160+
"""
161+
request = assets_pb2.ListMicrogridElectricalComponentConnectionsRequest(
162+
microgrid_id=microgrid_id,
163+
)
164+
165+
if source_component_ids:
166+
request.source_component_ids.extend(source_component_ids)
167+
168+
if destination_component_ids:
169+
request.destination_component_ids.extend(destination_component_ids)
170+
171+
response = await call_stub_method(
172+
self,
173+
lambda: self.stub.ListMicrogridElectricalComponentConnections(
174+
request,
175+
timeout=DEFAULT_GRPC_CALL_TIMEOUT,
176+
),
177+
method_name="ListMicrogridElectricalComponentConnections",
178+
)
179+
180+
return [
181+
component_connection_from_proto(connection)
182+
for connection in response.connections
183+
]

src/frequenz/client/assets/cli/__main__.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@
4040
from frequenz.client.assets._client import AssetsApiClient
4141
from frequenz.client.assets.exceptions import ApiClientError
4242

43-
from ._utils import print_electrical_components, print_microgrid_details
43+
from ._utils import (
44+
print_component_connections,
45+
print_electrical_components,
46+
print_microgrid_details,
47+
)
4448

4549

4650
@click.group(invoke_without_command=True)
@@ -201,6 +205,97 @@ async def list_microgrid_electrical_components(
201205
raise click.Abort()
202206

203207

208+
@cli.command("component-connections")
209+
@click.pass_context
210+
@click.argument("microgrid-id", required=True, type=int)
211+
@click.option(
212+
"--source",
213+
"source_component_ids",
214+
help="Filter connections by source component ID(s). Can be specified multiple times.",
215+
type=int,
216+
multiple=True,
217+
required=False,
218+
)
219+
@click.option(
220+
"--destination",
221+
"destination_component_ids",
222+
help="Filter connections by destination component ID(s). Can be specified multiple times.",
223+
type=int,
224+
multiple=True,
225+
required=False,
226+
)
227+
async def list_microgrid_electrical_component_connections(
228+
ctx: click.Context,
229+
microgrid_id: int,
230+
source_component_ids: tuple[int, ...],
231+
destination_component_ids: tuple[int, ...],
232+
) -> None:
233+
"""
234+
Get and display electrical component connections by microgrid ID.
235+
236+
This command fetches detailed information about all electrical component connections
237+
in a specific microgrid from the Assets API and displays it in JSON format.
238+
The output can be piped to other tools for further processing.
239+
240+
Args:
241+
ctx: Click context object containing the initialized API client.
242+
microgrid_id: The unique identifier of the microgrid to retrieve.
243+
source_component_ids: Optional filter for connections from specific
244+
source component IDs.
245+
destination_component_ids: Optional filter for connections to specific
246+
destination component IDs.
247+
248+
Raises:
249+
click.Abort: If there is an error printing the electrical component connections.
250+
251+
Example:
252+
```bash
253+
# Get all connections for microgrid with ID 123
254+
assets-cli component-connections 123
255+
256+
# Filter by source component
257+
assets-cli component-connections 123 --source 5
258+
259+
# Filter by destination component
260+
assets-cli component-connections 123 --destination 10
261+
262+
# Filter by both source and destination
263+
assets-cli component-connections 123 --source 5 --destination 10
264+
265+
# Filter by multiple source components
266+
assets-cli component-connections 123 --source 5 --source 6 --source 7
267+
268+
# Pipe output to jq for filtering
269+
assets-cli component-connections 123 | jq ".[]"
270+
```
271+
"""
272+
try:
273+
client = ctx.obj["client"]
274+
component_connections = (
275+
await client.list_microgrid_electrical_component_connections(
276+
microgrid_id,
277+
source_component_ids=(
278+
list(source_component_ids) if source_component_ids else None
279+
),
280+
destination_component_ids=(
281+
list(destination_component_ids)
282+
if destination_component_ids
283+
else None
284+
),
285+
)
286+
)
287+
print_component_connections(component_connections)
288+
except ApiClientError as e:
289+
error_dict = {
290+
"error_type": type(e).__name__,
291+
"server_url": e.server_url,
292+
"operation": e.operation,
293+
"description": e.description,
294+
}
295+
click.echo(json.dumps(error_dict, indent=2))
296+
raise click.Abort()
297+
298+
204299
def main() -> None:
205300
"""
206301
Initialize and run the CLI application.

src/frequenz/client/assets/cli/_utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
from frequenz.client.assets._microgrid import Microgrid
66
from frequenz.client.assets._microgrid_json import microgrid_to_json
7+
from frequenz.client.assets.electrical_component._connection import ComponentConnection
8+
from frequenz.client.assets.electrical_component._connection_json import (
9+
component_connections_to_json,
10+
)
711
from frequenz.client.assets.electrical_component._electrical_component import (
812
ElectricalComponent,
913
)
@@ -42,3 +46,17 @@ def print_electrical_components(
4246
electrical_components: The list of ElectricalComponent instances to print to console.
4347
"""
4448
click.echo(electrical_components_to_json(electrical_components))
49+
50+
51+
def print_component_connections(
52+
component_connections: list[ComponentConnection],
53+
) -> None:
54+
"""
55+
Print electrical component connections to console in JSON format using custom encoder.
56+
57+
This function converts the ComponentConnection instances to JSON using a custom
58+
encoder and outputs it as formatted JSON to the console. The output is
59+
designed to be machine-readable and can be piped to tools like jq for
60+
further processing.
61+
"""
62+
click.echo(component_connections_to_json(component_connections))
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Component connection."""
5+
6+
import dataclasses
7+
from datetime import datetime, timezone
8+
9+
from frequenz.client.common.microgrid.components import ComponentId
10+
11+
from .._lifetime import Lifetime
12+
13+
14+
@dataclasses.dataclass(frozen=True, kw_only=True)
15+
class ComponentConnection:
16+
"""A single electrical link between two components within a microgrid.
17+
18+
A component connection represents the physical wiring as viewed from the grid
19+
connection point, if one exists, or from the islanding point, in case of an islanded
20+
microgrids.
21+
22+
Note: Physical Representation
23+
This object is not about data flow but rather about the physical
24+
electrical connections between components. Therefore, the IDs for the
25+
source and destination components correspond to the actual setup within
26+
the microgrid.
27+
28+
Note: Direction
29+
The direction of the connection follows the flow of current away from the
30+
grid connection point, or in case of islands, away from the islanding
31+
point. This direction is aligned with positive current according to the
32+
[Passive Sign Convention]
33+
(https://en.wikipedia.org/wiki/Passive_sign_convention).
34+
35+
Note: Historical Data
36+
The timestamps of when a connection was created and terminated allow for
37+
tracking the changes over time to a microgrid, providing insights into
38+
when and how the microgrid infrastructure has been modified.
39+
"""
40+
41+
source: ComponentId
42+
"""The unique identifier of the component where the connection originates.
43+
44+
This is aligned with the direction of current flow away from the grid connection
45+
point, or in case of islands, away from the islanding point.
46+
"""
47+
48+
destination: ComponentId
49+
"""The unique ID of the component where the connection terminates.
50+
51+
This is the component towards which the current flows.
52+
"""
53+
54+
operational_lifetime: Lifetime = dataclasses.field(default_factory=Lifetime)
55+
"""The operational lifetime of the connection."""
56+
57+
def __post_init__(self) -> None:
58+
"""Ensure that the source and destination components are different."""
59+
if self.source == self.destination:
60+
raise ValueError("Source and destination components must be different")
61+
62+
def is_operational_at(self, timestamp: datetime) -> bool:
63+
"""Check whether this connection is operational at a specific timestamp."""
64+
return self.operational_lifetime.is_operational_at(timestamp)
65+
66+
def is_operational_now(self) -> bool:
67+
"""Whether this connection is currently operational."""
68+
return self.is_operational_at(datetime.now(timezone.utc))
69+
70+
def __str__(self) -> str:
71+
"""Return a human-readable string representation of this instance."""
72+
return f"{self.source}->{self.destination}"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""JSON encoder for ComponentConnection objects."""
5+
6+
import json
7+
from dataclasses import asdict
8+
9+
from .._utils import AssetsJSONEncoder
10+
from ._connection import ComponentConnection
11+
12+
13+
def component_connections_to_json(
14+
component_connections: list[ComponentConnection],
15+
) -> str:
16+
"""Convert a list of ElectricalComponent objects to a JSON string."""
17+
return json.dumps(
18+
[asdict(connection) for connection in component_connections],
19+
cls=AssetsJSONEncoder,
20+
indent=2,
21+
)

0 commit comments

Comments
 (0)