From 13dd5ea7b8a422ebf4e45af9f8869e77ca5e4923 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Tue, 18 Nov 2025 11:45:27 +0100 Subject: [PATCH 1/6] Delete `delete_me` and todo comments Add placeholder test until there are actual tests Signed-off-by: Sahas Subramanian --- pyproject.toml | 3 --- src/frequenz/gridpool/__init__.py | 23 +---------------------- tests/test_gridpool.py | 15 ++++----------- 3 files changed, 5 insertions(+), 36 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ccf1d16..ca507c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", @@ -26,7 +25,6 @@ classifiers = [ "Typing :: Typed", ] requires-python = ">= 3.11, < 4" -# TODO(cookiecutter): Remove and add more dependencies if appropriate dependencies = [ "typing-extensions >= 4.14.1, < 5", ] @@ -36,7 +34,6 @@ dynamic = ["version"] name = "Frequenz Energy-as-a-Service GmbH" email = "floss@frequenz.com" -# TODO(cookiecutter): Remove and add more optional dependencies if appropriate [project.optional-dependencies] dev-flake8 = [ "flake8 == 7.3.0", diff --git a/src/frequenz/gridpool/__init__.py b/src/frequenz/gridpool/__init__.py index 362fe14..59a2c80 100644 --- a/src/frequenz/gridpool/__init__.py +++ b/src/frequenz/gridpool/__init__.py @@ -1,25 +1,4 @@ # License: MIT # Copyright © 2025 Frequenz Energy-as-a-Service GmbH -"""High-level interface to grid pools for the Frequenz platform.. - -TODO(cookiecutter): Add a more descriptive module description. -""" - - -# 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 +"""High-level interface to grid pools for the Frequenz platform.""" diff --git a/tests/test_gridpool.py b/tests/test_gridpool.py index 0eea1e6..c899622 100644 --- a/tests/test_gridpool.py +++ b/tests/test_gridpool.py @@ -2,17 +2,10 @@ # Copyright © 2025 Frequenz Energy-as-a-Service GmbH """Tests for the frequenz.gridpool package.""" -import pytest -from frequenz.gridpool import delete_me +import frequenz.gridpool -def test_gridpool_succeeds() -> None: # TODO(cookiecutter): Remove - """Test that the delete_me function succeeds.""" - assert delete_me() is True - - -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) +def test_loading() -> None: + """Test that the frequenz.gridpool package loads correctly.""" + assert frequenz.gridpool is not None From 8b7552ffedfcd5317d35cc92ce9752fffcebb527 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Tue, 18 Nov 2025 16:07:55 +0100 Subject: [PATCH 2/6] WIP: Add assets and component graph as dependencies Signed-off-by: Sahas Subramanian --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ca507c4..b32f1f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,8 @@ classifiers = [ requires-python = ">= 3.11, < 4" 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" ] dynamic = ["version"] From 2e8ddf6e1c84c3cc882935490040b7c4be2884d5 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Tue, 18 Nov 2025 16:12:20 +0100 Subject: [PATCH 3/6] Implement a `ComponentGraphGenerator` from the assets API Signed-off-by: Sahas Subramanian --- src/frequenz/gridpool/__init__.py | 4 + src/frequenz/gridpool/_graph_generator.py | 90 +++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/frequenz/gridpool/_graph_generator.py diff --git a/src/frequenz/gridpool/__init__.py b/src/frequenz/gridpool/__init__.py index 59a2c80..31343c5 100644 --- a/src/frequenz/gridpool/__init__.py +++ b/src/frequenz/gridpool/__init__.py @@ -2,3 +2,7 @@ # Copyright © 2025 Frequenz Energy-as-a-Service GmbH """High-level interface to grid pools for the Frequenz platform.""" + +from ._graph_generator import ComponentGraphGenerator + +__all__ = ["ComponentGraphGenerator"] diff --git a/src/frequenz/gridpool/_graph_generator.py b/src/frequenz/gridpool/_graph_generator.py new file mode 100644 index 0000000..1a83138 --- /dev/null +++ b/src/frequenz/gridpool/_graph_generator.py @@ -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 + 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.") + + graph = ComponentGraph[ + ElectricalComponent, ComponentConnection, ElectricalComponentId + ](components, cast(list[ComponentConnection], connections)) + + return graph From 179cb7a658240128d9050b88ccc8f4603ad085ad Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Tue, 18 Nov 2025 16:12:56 +0100 Subject: [PATCH 4/6] Add tests for the `ComponentGraphGenerator` Signed-off-by: Sahas Subramanian --- tests/test_gridpool.py | 62 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/tests/test_gridpool.py b/tests/test_gridpool.py index c899622..550589c 100644 --- a/tests/test_gridpool.py +++ b/tests/test_gridpool.py @@ -3,9 +3,65 @@ """Tests for the frequenz.gridpool package.""" -import frequenz.gridpool +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_loading() -> None: +from frequenz.gridpool._graph_generator import ComponentGraphGenerator + + +async def test_formula_generation() -> None: """Test that the frequenz.gridpool package loads correctly.""" - assert frequenz.gridpool is not None + 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)" From 747bd8617ebff8d72f9a69d960d7496bed7eae69 Mon Sep 17 00:00:00 2001 From: cwasicki <126617870+cwasicki@users.noreply.github.com> Date: Fri, 21 Nov 2025 17:15:43 +0100 Subject: [PATCH 5/6] Add gridpool cli tool to print formulas Signed-off-by: cwasicki <126617870+cwasicki@users.noreply.github.com> --- pyproject.toml | 4 ++ src/frequenz/gridpool/cli/__init__.py | 1 + src/frequenz/gridpool/cli/__main__.py | 65 +++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 src/frequenz/gridpool/cli/__init__.py create mode 100644 src/frequenz/gridpool/cli/__main__.py diff --git a/pyproject.toml b/pyproject.toml index b32f1f5..82e6220 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,12 +26,16 @@ classifiers = [ ] requires-python = ">= 3.11, < 4" dependencies = [ + "asyncclick >= 8.3.0.4, < 9", "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" ] dynamic = ["version"] +[project.scripts] +gridpool-cli = "frequenz.gridpool.cli.__main__:main" + [[project.authors]] name = "Frequenz Energy-as-a-Service GmbH" email = "floss@frequenz.com" diff --git a/src/frequenz/gridpool/cli/__init__.py b/src/frequenz/gridpool/cli/__init__.py new file mode 100644 index 0000000..ffedaa5 --- /dev/null +++ b/src/frequenz/gridpool/cli/__init__.py @@ -0,0 +1 @@ +"""Package for CLI tool for gridpool functionality.""" diff --git a/src/frequenz/gridpool/cli/__main__.py b/src/frequenz/gridpool/cli/__main__.py new file mode 100644 index 0000000..84705d1 --- /dev/null +++ b/src/frequenz/gridpool/cli/__main__.py @@ -0,0 +1,65 @@ +# License: MIT +# Copyright © 2025 Frequenz Energy-as-a-Service GmbH + +"""CLI tool for gridpool functionality.""" + +import os + +import asyncclick as click +from frequenz.client.common.microgrid import MicrogridId + +from frequenz.gridpool import ComponentGraphGenerator + + +@click.group() +async def cli() -> None: + """CLI tool for gridpool functionality.""" + + +@cli.command() +@click.argument("microgrid_id", type=int) +@click.option( + "--prefix", + type=str, + default="{component}", + help="Prefix format for the output (Supports wildcards {microgrid_id} and {component}).", +) +async def print_formulas( + microgrid_id: int, + prefix: str, +) -> None: + """Fetch and print component graph formulas for a microgrid.""" + url = os.environ.get("ASSETS_API_URL") + key = os.environ.get("ASSETS_API_KEY") + secret = os.environ.get("ASSETS_API_SECRET") + if not url or not key or not secret: + raise click.ClickException( + "ASSETS_API_URL, ASSETS_API_KEY, ASSETS_API_SECRET must be set." + ) + cgg = ComponentGraphGenerator(url, auth_key=key, sign_secret=secret) + + graph = await cgg.get_component_graph(MicrogridId(microgrid_id)) + power_formulas = { + "consumption": graph.consumer_formula(), + "generation": graph.producer_formula(), + "grid": graph.grid_formula(), + "pv": graph.pv_formula(None), + "battery": graph.battery_formula(None), + "chp": graph.chp_formula(None), + "ev": graph.ev_charger_formula(None), + } + + for component, formula in power_formulas.items(): + print( + prefix.format(component=component, microgrid_id=microgrid_id) + + f' = "{formula}"' + ) + + +def main() -> None: + """Run the CLI tool.""" + cli() + + +if __name__ == "__main__": + main() From f0c0dc67bc9441b7696eb8bd30e0a0135028e8ef Mon Sep 17 00:00:00 2001 From: cwasicki <126617870+cwasicki@users.noreply.github.com> Date: Fri, 21 Nov 2025 17:35:09 +0100 Subject: [PATCH 6/6] Update release notes --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d466455..bec9d27 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -10,6 +10,8 @@ ## New Features +* Add CLI tool to print formulas from assets API component graph. + ## Bug Fixes