Skip to content

Commit 178a496

Browse files
committed
feat(assets): Add functionality to list electrical component connections for microgrids
This commit introduces a new command to the CLI for retrieving and displaying electrical component connections within a specified microgrid. The new method `list_microgrid_electrical_component_connections` in the `AssetsApiClient` allows filtering by source and destination component IDs. Additionally, utility functions for printing component connections in JSON format have been added, along with the necessary data structures and protobuf handling for `ComponentConnection` objects. The CLI command enhances usability by allowing users to filter connections and output results in a machine-readable format. Signed-off-by: eduardiazf <[email protected]>
1 parent 29010a7 commit 178a496

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)