Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ description = "High-level interface to grid pools for the Frequenz platform."
readme = "README.md"
license = { text = "MIT" }
keywords = ["frequenz", "python", "lib", "library", "gridpool"]
# TODO(cookiecutter): Remove and add more classifiers if appropriate
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
Expand All @@ -26,17 +25,17 @@ classifiers = [
"Typing :: Typed",
]
requires-python = ">= 3.11, < 4"
# TODO(cookiecutter): Remove and add more dependencies if appropriate
dependencies = [
"typing-extensions >= 4.14.1, < 5",
"frequenz-microgrid-component-graph @ git+https://github.com/shsms/frequenz-microgrid-component-graph-python.git@c4a458e06e541846de0a25142d5b821dd7934f47",
"frequenz-client-assets @ git+https://github.com/shsms/frequenz-client-assets-python@bbf09104df24ddfac497e2a3dd66fe68cfdacd25"
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Missing trailing comma after the last dependency. For consistency and easier future modifications, add a trailing comma.

Suggested change
"frequenz-client-assets @ git+https://github.com/shsms/frequenz-client-assets-python@bbf09104df24ddfac497e2a3dd66fe68cfdacd25"
"frequenz-client-assets @ git+https://github.com/shsms/frequenz-client-assets-python@bbf09104df24ddfac497e2a3dd66fe68cfdacd25",

Copilot uses AI. Check for mistakes.
]
dynamic = ["version"]

[[project.authors]]
name = "Frequenz Energy-as-a-Service GmbH"
email = "[email protected]"

# TODO(cookiecutter): Remove and add more optional dependencies if appropriate
[project.optional-dependencies]
dev-flake8 = [
"flake8 == 7.3.0",
Expand Down
23 changes: 3 additions & 20 deletions src/frequenz/gridpool/__init__.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,8 @@
# License: MIT
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH

"""High-level interface to grid pools for the Frequenz platform..
"""High-level interface to grid pools for the Frequenz platform."""

TODO(cookiecutter): Add a more descriptive module description.
"""
from ._graph_generator import ComponentGraphGenerator


# TODO(cookiecutter): Remove this function
def delete_me(*, blow_up: bool = False) -> bool:
"""Do stuff for demonstration purposes.

Args:
blow_up: If True, raise an exception.

Returns:
True if no exception was raised.

Raises:
RuntimeError: if blow_up is True.
"""
if blow_up:
raise RuntimeError("This function should be removed!")
return True
__all__ = ["ComponentGraphGenerator"]
90 changes: 90 additions & 0 deletions src/frequenz/gridpool/_graph_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# License: MIT
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH

"""Formula generation from assets API component/connection configurations."""

from typing import cast

from frequenz.client.assets import AssetsApiClient
from frequenz.client.assets.electrical_component import (
ComponentConnection,
ElectricalComponent,
)
from frequenz.client.base import channel
from frequenz.client.common.microgrid import MicrogridId
from frequenz.client.common.microgrid.electrical_components import ElectricalComponentId
from frequenz.microgrid_component_graph import ComponentGraph


class ComponentGraphGenerator:
"""Generates component graphs for microgrids using the Assets API."""

def __init__( # pylint: disable=too-many-arguments
self,
server_url: str,
*,
auth_key: str | None = None,
sign_secret: str | None = None,
channel_defaults: channel.ChannelOptions = channel.ChannelOptions(),
connect: bool = True,
) -> None:
"""Initialize this instance.
Args:
server_url: The location of the microgrid API server in the form of a URL.
The following format is expected:
"grpc://hostname{:`port`}{?ssl=`ssl`}",
where the `port` should be an int between 0 and 65535 (defaulting to
9090) and `ssl` should be a boolean (defaulting to `true`).
For example: `grpc://localhost:1090?ssl=true`.
auth_key: The authentication key to use for the connection.
sign_secret: The secret to use for signing requests.
channel_defaults: The default options use to create the channel when not
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected 'use' to 'used' in the documentation.

Suggested change
channel_defaults: The default options use to create the channel when not
channel_defaults: The default options used to create the channel when not

Copilot uses AI. Check for mistakes.
specified in the URL.
connect: Whether to connect to the server as soon as a client instance is
created. If `False`, the client will not connect to the server until
[connect()][frequenz.client.base.client.BaseApiClient.connect] is
called.
"""
self._client: AssetsApiClient = AssetsApiClient(
server_url,
auth_key=auth_key,
sign_secret=sign_secret,
channel_defaults=channel_defaults,
connect=connect,
)

async def get_component_graph(
self, microgrid_id: MicrogridId
) -> ComponentGraph[
ElectricalComponent, ComponentConnection, ElectricalComponentId
]:
"""Generate a component graph for the given microgrid ID.
Args:
microgrid_id: The ID of the microgrid to generate the graph for.
Returns:
The component graph representing the microgrid's electrical
components and their connections.
Raises:
ValueError: If any component connections could not be loaded.
"""
components = await self._client.list_microgrid_electrical_components(
microgrid_id
)
connections = (
await self._client.list_microgrid_electrical_component_connections(
microgrid_id
)
)

if any(c is None for c in connections):
raise ValueError("Failed to load all electrical component connections.")

Comment on lines +83 to +85
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The None check at line 83 is unnecessary since the type hint indicates list_microgrid_electrical_component_connections returns a list of connections, not optional connections. If None values are genuinely possible, the type hints should reflect this. Consider removing this check or updating the type hints for clarity.

Suggested change
if any(c is None for c in connections):
raise ValueError("Failed to load all electrical component connections.")

Copilot uses AI. Check for mistakes.
graph = ComponentGraph[
ElectricalComponent, ComponentConnection, ElectricalComponentId
](components, cast(list[ComponentConnection], connections))

return graph
67 changes: 58 additions & 9 deletions tests/test_gridpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,66 @@
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH

"""Tests for the frequenz.gridpool package."""
import pytest

from frequenz.gridpool import delete_me
from unittest.mock import AsyncMock, MagicMock, patch

from frequenz.client.assets import AssetsApiClient
from frequenz.client.assets.electrical_component import (
ComponentConnection,
GridConnectionPoint,
Meter,
SolarInverter,
)
from frequenz.client.common.microgrid import MicrogridId
from frequenz.client.common.microgrid.electrical_components import ElectricalComponentId

def test_gridpool_succeeds() -> None: # TODO(cookiecutter): Remove
"""Test that the delete_me function succeeds."""
assert delete_me() is True
from frequenz.gridpool._graph_generator import ComponentGraphGenerator


def test_gridpool_fails() -> None: # TODO(cookiecutter): Remove
"""Test that the delete_me function fails."""
with pytest.raises(RuntimeError, match="This function should be removed!"):
delete_me(blow_up=True)
async def test_formula_generation() -> None:
"""Test that the frequenz.gridpool package loads correctly."""
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring is misleading. This test validates formula generation from component graphs, not package loading. Consider updating to: 'Test formula generation from component graph.'

Suggested change
"""Test that the frequenz.gridpool package loads correctly."""
"""Test formula generation from component graph."""

Copilot uses AI. Check for mistakes.
assets_client_mock = MagicMock(spec=AssetsApiClient)
assets_client_mock.list_microgrid_electrical_components = AsyncMock(
return_value=[
GridConnectionPoint(
id=ElectricalComponentId(1),
microgrid_id=MicrogridId(10),
rated_fuse_current=100,
),
Meter(
id=ElectricalComponentId(2),
microgrid_id=MicrogridId(10),
),
Meter(
id=ElectricalComponentId(3),
microgrid_id=MicrogridId(10),
),
SolarInverter(
id=ElectricalComponentId(4),
microgrid_id=MicrogridId(10),
),
]
)
assets_client_mock.list_microgrid_electrical_component_connections = AsyncMock(
return_value=[
ComponentConnection(
source=ElectricalComponentId(1),
destination=ElectricalComponentId(2),
),
ComponentConnection(
source=ElectricalComponentId(1),
destination=ElectricalComponentId(3),
),
ComponentConnection(
source=ElectricalComponentId(2),
destination=ElectricalComponentId(4),
),
]
)

g = ComponentGraphGenerator("grpc://never.where:-55", connect=False)
with patch.object(g, "_client", assets_client_mock):
graph = await g.get_component_graph(MicrogridId(10))

assert graph.grid_formula() == "COALESCE(#2, #4, 0.0) + #3"
assert graph.pv_formula(None) == "COALESCE(#4, #2, 0.0)"
Loading