From ff4974ecb68fe285ed7cfe611a17b884e1a08e84 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Mon, 20 Jul 2026 12:58:29 +0000 Subject: [PATCH 1/5] build(deps): run mypy and pytest against the graph renderer dev-pytest pulled only the cli extra, so matplotlib and networkx were missing. mypy could not find the matplotlib stubs and failed on _render_graph.py, and pytest skipped tests/test_render_graph.py, so its 11 tests never ran. Pull the render-graph extra instead, which already includes cli. Signed-off-by: Sahas Subramanian --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 33508c7..dc1f293 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,7 +95,9 @@ dev-pytest = [ "pytest-mock == 3.15.1", "pytest-asyncio == 1.4.0", "async-solipsism == 0.9", - "frequenz-gridpool[cli]", # The tests cover the cli package + # The tests cover the cli package and the graph renderer. render-graph + # already pulls in cli. + "frequenz-gridpool[render-graph]", ] dev = [ "frequenz-gridpool[dev-mkdocs,dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest,render-graph]", From f08e269fac1b9bb1d1171f7063d7174b58195e1e Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Mon, 20 Jul 2026 12:50:54 +0000 Subject: [PATCH 2/5] build(deps): update component graph to v0.5.0 v0.5.0 flips the default of prefer_meters_in_component_formulas to False, so the per-category formulas now read the component first and use the meter as the fallback. We take the new default, which changes the per-category formulas in the generated config. The test graph only has a solar inverter, so the pinned pv formulas flip their expected order. Signed-off-by: Sahas Subramanian --- pyproject.toml | 2 +- tests/test_graph_generator.py | 2 +- tests/test_load.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dc1f293..872330c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ requires-python = ">= 3.11, < 4" dependencies = [ "marshmallow-dataclass >= 8.7.1, < 9", "typing-extensions >= 4.14.1, < 5", - "frequenz-microgrid-component-graph >= 0.4.0, < 0.5", + "frequenz-microgrid-component-graph >= 0.5.0, < 0.6", "frequenz-client-assets >= 0.3.1, < 0.4", ] dynamic = ["version"] diff --git a/tests/test_graph_generator.py b/tests/test_graph_generator.py index a5901b3..019b235 100644 --- a/tests/test_graph_generator.py +++ b/tests/test_graph_generator.py @@ -63,4 +63,4 @@ async def test_formula_generation() -> None: graph = await g.get_component_graph(MicrogridId(10)) assert graph.grid_formula() == "COALESCE(#2, #4, 0.0) + #3" - assert graph.pv_formula(None) == "COALESCE(#2, #4, 0.0)" + assert graph.pv_formula(None) == "COALESCE(#4, #2, 0.0)" diff --git a/tests/test_load.py b/tests/test_load.py index 59be309..d76b284 100644 --- a/tests/test_load.py +++ b/tests/test_load.py @@ -66,7 +66,7 @@ async def test_load_configs_from_api_derives_formulas_and_ids() -> None: configs = await load_configs_from_api(_mock_client(), [10]) cfg = configs["10"] - assert cfg.ctype["pv"].formula == {"AC_POWER_ACTIVE": "COALESCE(#2, #4, 0.0)"} + assert cfg.ctype["pv"].formula == {"AC_POWER_ACTIVE": "COALESCE(#4, #2, 0.0)"} assert cfg.ctype["pv"].inverter == [4] assert cfg.ctype["pv"].meter == [2] assert cfg.ctype["grid"].meter == [2, 3] @@ -94,7 +94,7 @@ async def test_derive_component_configs_builds_formulas_and_ids() -> None: configs = _derive_component_configs(graph) - assert configs["pv"].formula == {"AC_POWER_ACTIVE": "COALESCE(#2, #4, 0.0)"} + assert configs["pv"].formula == {"AC_POWER_ACTIVE": "COALESCE(#4, #2, 0.0)"} assert configs["pv"].inverter == [4] assert configs["pv"].meter == [2] assert configs["grid"].meter == [2, 3] From 7a7d32be2b736d38eadad996aff74fd5196e54bd Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Mon, 20 Jul 2026 13:54:33 +0000 Subject: [PATCH 3/5] feat: let callers choose how the component graph is built The graph was built with no ComponentGraphConfig, so callers had no way to change the source order or any other graph setting. In particular they could not keep the meter-first per-category formulas that v0.5.0 of the component graph library moved away from. Take an optional config in ComponentGraphGenerator and thread it through load_configs_from_api and load_configs. Re-export ComponentGraphConfig and FormulaOverrides from frequenz.gridpool and frequenz.gridpool.config so callers do not have to import them from the library. load_configs raises when the config is given without an assets_client, the same way it already does for microgrid_ids, so a config that could never be applied is an error rather than a silent no-op. The default is unchanged: with no config the library's own defaults apply. Signed-off-by: Sahas Subramanian --- src/frequenz/gridpool/__init__.py | 4 +++ src/frequenz/gridpool/_graph_generator.py | 10 ++++-- src/frequenz/gridpool/config/__init__.py | 4 +++ src/frequenz/gridpool/config/load.py | 22 +++++++++--- tests/test_graph_generator.py | 24 +++++++++++-- tests/test_load.py | 42 +++++++++++++++++++++++ 6 files changed, 97 insertions(+), 9 deletions(-) diff --git a/src/frequenz/gridpool/__init__.py b/src/frequenz/gridpool/__init__.py index 10dc0b9..fa68cdb 100644 --- a/src/frequenz/gridpool/__init__.py +++ b/src/frequenz/gridpool/__init__.py @@ -3,6 +3,8 @@ """High-level interface to grid pools for the Frequenz platform.""" +from frequenz.microgrid_component_graph import ComponentGraphConfig, FormulaOverrides + from ._graph_generator import ComponentGraphGenerator from .config import ( Metadata, @@ -15,7 +17,9 @@ ) __all__ = [ + "ComponentGraphConfig", "ComponentGraphGenerator", + "FormulaOverrides", "Metadata", "MicrogridConfig", "load_configs", diff --git a/src/frequenz/gridpool/_graph_generator.py b/src/frequenz/gridpool/_graph_generator.py index 0430960..1b6dc3e 100644 --- a/src/frequenz/gridpool/_graph_generator.py +++ b/src/frequenz/gridpool/_graph_generator.py @@ -26,7 +26,7 @@ ) from frequenz.client.common.microgrid import MicrogridId from frequenz.client.common.microgrid.electrical_components import ElectricalComponentId -from frequenz.microgrid_component_graph import ComponentGraph +from frequenz.microgrid_component_graph import ComponentGraph, ComponentGraphConfig _logger = logging.getLogger(__name__) @@ -42,14 +42,20 @@ class ComponentGraphGenerator: def __init__( self, client: AssetsApiClient, + config: ComponentGraphConfig | None = None, ) -> None: """Initialize this instance. Args: client: The Assets API client to use for fetching components and connections. + config: How to build the graph and generate its formulas. See + `ComponentGraphConfig`. Defaults to that class's own defaults. """ self._client: AssetsApiClient = client + self._config: ComponentGraphConfig = ( + config if config is not None else ComponentGraphConfig() + ) async def get_component_graph( self, microgrid_id: MicrogridId @@ -100,7 +106,7 @@ async def get_component_graph( graph = ComponentGraph[ ElectricalComponent, ComponentConnection, ElectricalComponentId - ](components, connections) + ](components, connections, self._config) return graph diff --git a/src/frequenz/gridpool/config/__init__.py b/src/frequenz/gridpool/config/__init__.py index edbf760..7be40f1 100644 --- a/src/frequenz/gridpool/config/__init__.py +++ b/src/frequenz/gridpool/config/__init__.py @@ -3,6 +3,8 @@ """Microgrid configuration data model and loading.""" +from frequenz.microgrid_component_graph import ComponentGraphConfig, FormulaOverrides + from .load import ( load_configs, load_configs_from_api, @@ -24,8 +26,10 @@ __all__ = [ "BatteryConfig", "ComponentCategory", + "ComponentGraphConfig", "ComponentType", "ComponentTypeConfig", + "FormulaOverrides", "Metadata", "MicrogridConfig", "PVConfig", diff --git a/src/frequenz/gridpool/config/load.py b/src/frequenz/gridpool/config/load.py index ddacd14..5aaf170 100644 --- a/src/frequenz/gridpool/config/load.py +++ b/src/frequenz/gridpool/config/load.py @@ -8,6 +8,7 @@ from frequenz.client.assets import AssetsApiClient from frequenz.client.common.microgrid import MicrogridId +from frequenz.microgrid_component_graph import ComponentGraphConfig from .._graph_generator import ( ComponentGraphGenerator, @@ -38,6 +39,7 @@ async def load_configs( assets_client: AssetsApiClient | None = None, override_files: str | Path | list[str | Path] | None = None, microgrid_ids: list[int] | None = None, + component_graph_config: ComponentGraphConfig | None = None, ) -> dict[str, "MicrogridConfig"]: """Load configs from up to three sources and merge them in layers. @@ -72,6 +74,10 @@ async def load_configs( Optional explicit microgrid IDs to fetch from the Assets API. When given, these replace the IDs derived from the files, so the Assets API layer can be used without any files. + component_graph_config: + How to build the component graph and generate its formulas. See + `ComponentGraphConfig`. Defaults to that class's own defaults. + Requires an `assets_client`. Returns: dict[str, MicrogridConfig]: @@ -80,7 +86,8 @@ async def load_configs( Raises: ValueError: If none of the three sources is provided, or if - `microgrid_ids` is given without an `assets_client`. + `microgrid_ids` or `component_graph_config` is given without an + `assets_client`. """ if default_files is None and assets_client is None and override_files is None: raise ValueError("At least one config source must be provided.") @@ -88,6 +95,9 @@ async def load_configs( if microgrid_ids is not None and assets_client is None: raise ValueError("microgrid_ids requires an assets_client.") + if component_graph_config is not None and assets_client is None: + raise ValueError("component_graph_config requires an assets_client.") + configs: dict[str, MicrogridConfig] = {} if default_files is not None: configs = load_configs_from_files( @@ -106,6 +116,7 @@ async def load_configs( assets_configs = await load_configs_from_api( assets_client=assets_client, microgrid_ids=microgrid_ids, + component_graph_config=component_graph_config, ) configs = merge_config_maps(base=configs, override=assets_configs) @@ -166,6 +177,7 @@ def load_configs_from_files( async def load_configs_from_api( assets_client: AssetsApiClient, microgrid_ids: list[int], + component_graph_config: ComponentGraphConfig | None = None, ) -> dict[str, "MicrogridConfig"]: """Load microgrid configs from the Assets API. @@ -184,6 +196,9 @@ async def load_configs_from_api( component graph. microgrid_ids: List of microgrid IDs to load configurations for. + component_graph_config: + How to build the component graph and generate its formulas. See + `ComponentGraphConfig`. Defaults to that class's own defaults. Returns: dict[str, MicrogridConfig]: @@ -192,6 +207,7 @@ async def load_configs_from_api( loaded are omitted, so the returned mapping may cover fewer microgrids than were requested. """ + generator = ComponentGraphGenerator(assets_client, config=component_graph_config) configs: dict[str, MicrogridConfig] = {} for microgrid_id in microgrid_ids: try: @@ -205,9 +221,7 @@ async def load_configs_from_api( continue try: - graph = await ComponentGraphGenerator(assets_client).get_component_graph( - MicrogridId(microgrid_id) - ) + graph = await generator.get_component_graph(MicrogridId(microgrid_id)) cfg.ctype = _derive_component_configs(graph) except Exception as exc: # pylint: disable=broad-except _logger.warning( diff --git a/tests/test_graph_generator.py b/tests/test_graph_generator.py index 019b235..342e10d 100644 --- a/tests/test_graph_generator.py +++ b/tests/test_graph_generator.py @@ -15,11 +15,12 @@ from frequenz.client.common.microgrid import MicrogridId from frequenz.client.common.microgrid.electrical_components import ElectricalComponentId +from frequenz.gridpool import ComponentGraphConfig from frequenz.gridpool._graph_generator import ComponentGraphGenerator -async def test_formula_generation() -> None: - """Test formula generation from component graph created from Assets API.""" +def _mock_client() -> MagicMock: + """Mock an Assets API client: grid 1 -> meter 2 -> solar inverter 4, + meter 3.""" assets_client_mock = MagicMock(spec=AssetsApiClient) assets_client_mock.list_microgrid_electrical_components = AsyncMock( return_value=[ @@ -59,8 +60,25 @@ async def test_formula_generation() -> None: ] ) - g = ComponentGraphGenerator(assets_client_mock) + return assets_client_mock + + +async def test_formula_generation() -> None: + """Test formula generation from component graph created from Assets API.""" + g = ComponentGraphGenerator(_mock_client()) 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)" + + +async def test_formula_generation_with_a_component_graph_config() -> None: + """A component graph config reaches the generated formulas.""" + g = ComponentGraphGenerator( + _mock_client(), + ComponentGraphConfig(prefer_meters_in_component_formulas=True), + ) + graph = await g.get_component_graph(MicrogridId(10)) + + # Meter first, the opposite of the default order. + assert graph.pv_formula(None) == "COALESCE(#2, #4, 0.0)" diff --git a/tests/test_load.py b/tests/test_load.py index d76b284..da71aa2 100644 --- a/tests/test_load.py +++ b/tests/test_load.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, MagicMock +import pytest from frequenz.client.assets import AssetsApiClient from frequenz.client.assets.electrical_component import ( ComponentConnection, @@ -15,12 +16,14 @@ from frequenz.client.common.microgrid import MicrogridId from frequenz.client.common.microgrid.electrical_components import ElectricalComponentId +from frequenz.gridpool import ComponentGraphConfig from frequenz.gridpool._graph_generator import ( ComponentGraphGenerator, MicrogridComponentGraph, ) from frequenz.gridpool.config.load import ( _derive_component_configs, + load_configs, load_configs_from_api, ) @@ -74,6 +77,45 @@ async def test_load_configs_from_api_derives_formulas_and_ids() -> None: assert set(cfg.ctype) == {"grid", "consumption", "pv"} +async def test_load_configs_from_api_honours_the_component_graph_config() -> None: + """A component graph config reaches the derived formulas.""" + configs = await load_configs_from_api( + _mock_client(), + [10], + component_graph_config=ComponentGraphConfig( + prefer_meters_in_component_formulas=True + ), + ) + + # Meter first, the opposite of the default order asserted above. + ctype = configs["10"].ctype + assert ctype["pv"].formula == {"AC_POWER_ACTIVE": "COALESCE(#2, #4, 0.0)"} + + +async def test_load_configs_forwards_the_component_graph_config() -> None: + """`load_configs` passes its component graph config down to the API layer.""" + configs = await load_configs( + assets_client=_mock_client(), + microgrid_ids=[10], + component_graph_config=ComponentGraphConfig( + prefer_meters_in_component_formulas=True + ), + ) + + assert configs["10"].ctype["pv"].formula == { + "AC_POWER_ACTIVE": "COALESCE(#2, #4, 0.0)" + } + + +async def test_load_configs_rejects_a_component_graph_config_without_a_client() -> None: + """A component graph config is only meaningful with an Assets API client.""" + with pytest.raises(ValueError, match="requires an assets_client"): + await load_configs( + default_files=[], + component_graph_config=ComponentGraphConfig(), + ) + + async def test_load_configs_from_api_keeps_metadata_when_graph_fails() -> None: """A graph-derivation failure still yields a metadata-only config.""" client = _mock_client() From 07659b33484e9c8197651f8a3a55040de635fe6c Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Mon, 20 Jul 2026 14:17:10 +0000 Subject: [PATCH 4/5] feat(cli): add --prefer-meters-in-component-formulas The config knob was only reachable from Python, but generate-config is the tool that regenerates stored configs. An operator who wanted to keep the meter-first order had to write a script instead. The generate-config and print-formulas commands now take --prefer-meters-in-component-formulas, which builds a ComponentGraphConfig with prefer_meters_in_component_formulas set. Without the flag no config is passed, so the defaults still apply. Signed-off-by: Sahas Subramanian --- README.md | 17 +++++ src/frequenz/gridpool/cli/__main__.py | 48 +++++++++++++- tests/test_cli.py | 94 +++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 tests/test_cli.py diff --git a/README.md b/README.md index 9e84c67..3a6f34d 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,15 @@ Optional prefix formatting: gridpool-cli print-formulas --prefix "{microgrid_id}.{component}" ``` +The per-category formulas (`pv`, `battery`, `chp`, `ev`) read the component +first and use the meter as the fallback. Use +`--prefer-meters-in-component-formulas` for the opposite order, which is what +versions before component graph v0.5.0 produced: + +```bash +gridpool-cli print-formulas --prefer-meters-in-component-formulas +``` + ### Render component graph Rendering requires optional dependencies. Install with: @@ -94,6 +103,14 @@ If no microgrid IDs are given, they are taken from the supplied files: gridpool-cli generate-config --override existing.toml > microgrid.toml ``` +`--prefer-meters-in-component-formulas` works here too, so a regenerated +config can keep the meter-first order of the per-category formulas: + +```bash +gridpool-cli generate-config \ + --prefer-meters-in-component-formulas > microgrid.toml +``` + ## Contributing If you want to know how to build this project and contribute to it, please diff --git a/src/frequenz/gridpool/cli/__main__.py b/src/frequenz/gridpool/cli/__main__.py index dee11c9..159cc67 100644 --- a/src/frequenz/gridpool/cli/__main__.py +++ b/src/frequenz/gridpool/cli/__main__.py @@ -11,7 +11,12 @@ from frequenz.client.assets import AssetsApiClient from frequenz.client.common.microgrid import MicrogridId -from frequenz.gridpool import ComponentGraphGenerator, MicrogridConfig, load_configs +from frequenz.gridpool import ( + ComponentGraphConfig, + ComponentGraphGenerator, + MicrogridConfig, + load_configs, +) from frequenz.gridpool.cli._dump_config import dump_map from frequenz.gridpool.cli._patch_config import patch_file from frequenz.gridpool.cli._render_graph import ComponentGraphRenderer, RenderOptions @@ -22,6 +27,17 @@ async def cli() -> None: """CLI tool for gridpool functionality.""" +def _graph_config(prefer_meters: bool) -> ComponentGraphConfig | None: + """Build the graph config for `--prefer-meters-in-component-formulas`. + + Returns `None` when the flag is not set, so the component graph library's + own defaults apply. + """ + if not prefer_meters: + return None + return ComponentGraphConfig(prefer_meters_in_component_formulas=True) + + @cli.command() @click.argument("microgrid_id", type=int) @click.option( @@ -30,9 +46,17 @@ async def cli() -> None: default="{component}", help="Prefix format for the output (Supports {microgrid_id} and {component} placeholders).", ) +@click.option( + "--prefer-meters-in-component-formulas", + is_flag=True, + default=False, + help="Read the meter before the component in the per-category formulas. " + "This is the order used before component graph v0.5.0.", +) async def print_formulas( microgrid_id: int, prefix: str, + prefer_meters_in_component_formulas: bool, ) -> None: """Fetch and print component graph formulas for a microgrid.""" url = os.environ.get("ASSETS_API_URL") @@ -48,7 +72,9 @@ async def print_formulas( auth_key=key, sign_secret=secret, ) as client: - cgg = ComponentGraphGenerator(client) + cgg = ComponentGraphGenerator( + client, config=_graph_config(prefer_meters_in_component_formulas) + ) graph = await cgg.get_component_graph(MicrogridId(microgrid_id)) power_formulas = { @@ -133,11 +159,19 @@ async def render_graph(microgrid_id: int, output: str, show: bool) -> None: "existing comments, ordering and formatting in that file; only fills in " "values it is missing. Requires --default.", ) +@click.option( + "--prefer-meters-in-component-formulas", + is_flag=True, + default=False, + help="Read the meter before the component in the per-category formulas. " + "This is the order used before component graph v0.5.0.", +) async def generate_config( microgrid_ids: tuple[int, ...], default_file: Path | None, override_file: Path | None, inplace: bool, + prefer_meters_in_component_formulas: bool, ) -> None: """Generate microgrid config from the Assets API as TOML. @@ -149,6 +183,10 @@ async def generate_config( IDs are given, they are taken from the supplied files. Files are only read; redirect stdout to save the result. + With `--prefer-meters-in-component-formulas`, the per-category formulas + read the meter before the component, which is the order used before + component graph v0.5.0. + With `--inplace`, `--default` is patched directly instead: candidate values come from the Assets API (with `--override` layered on top), and only leaves `--default` is missing are added, preserving its existing comments, @@ -178,6 +216,9 @@ async def generate_config( assets_client=client, override_files=override_file, microgrid_ids=ids, + component_graph_config=_graph_config( + prefer_meters_in_component_formulas + ), ) else: configs = await load_configs( @@ -185,6 +226,9 @@ async def generate_config( assets_client=client, override_files=override_file, microgrid_ids=ids, + component_graph_config=_graph_config( + prefer_meters_in_component_formulas + ), ) if not configs: diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..4e48557 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,94 @@ +# License: MIT +# Copyright © 2025 Frequenz Energy-as-a-Service GmbH + +"""Tests for the gridpool CLI.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +from asyncclick.testing import CliRunner +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 + +from frequenz.gridpool.cli.__main__ import _graph_config, cli + +_ENV = { + "ASSETS_API_URL": "grpc://localhost", + "ASSETS_API_AUTH_KEY": "key", + "ASSETS_API_SIGN_SECRET": "secret", +} + + +def _mock_client() -> MagicMock: + """Mock an Assets API client: grid 1 -> meter 2 -> solar inverter 4.""" + client = MagicMock(spec=AssetsApiClient) + client.get_microgrid = AsyncMock(return_value=MagicMock(location=None)) + client.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)), + SolarInverter(id=ElectricalComponentId(4), microgrid_id=MicrogridId(10)), + ] + ) + client.list_microgrid_electrical_component_connections = AsyncMock( + return_value=[ + ComponentConnection( + source=ElectricalComponentId(1), destination=ElectricalComponentId(2) + ), + ComponentConnection( + source=ElectricalComponentId(2), destination=ElectricalComponentId(4) + ), + ] + ) + return client + + +def _patched_client() -> MagicMock: + """Patch `AssetsApiClient` so the CLI's `async with` yields the mock.""" + ctx = MagicMock() + ctx.__aenter__ = AsyncMock(return_value=_mock_client()) + ctx.__aexit__ = AsyncMock(return_value=False) + return MagicMock(return_value=ctx) + + +async def test_print_formulas_prefer_meters_flips_the_order() -> None: + """The flag makes the CLI print the meter first.""" + with patch("frequenz.gridpool.cli.__main__.AssetsApiClient", _patched_client()): + result = await CliRunner().invoke( + cli, + ["print-formulas", "10", "--prefer-meters-in-component-formulas"], + env=_ENV, + ) + + assert result.exit_code == 0, result.output + assert 'pv = "COALESCE(#2, #4, 0.0)"' in result.output + + +async def test_generate_config_prefer_meters_flips_the_order() -> None: + """The flag reaches the formulas written into the config.""" + with patch("frequenz.gridpool.cli.__main__.AssetsApiClient", _patched_client()): + result = await CliRunner().invoke( + cli, + ["generate-config", "10", "--prefer-meters-in-component-formulas"], + env=_ENV, + ) + + assert result.exit_code == 0, result.output + assert ( + '10.ctype.pv.formula.AC_POWER_ACTIVE = "COALESCE(#2, #4, 0.0)"' in result.output + ) + + +def test_graph_config_is_none_without_the_flag() -> None: + """With no flag no config is built, so the library's defaults apply.""" + assert _graph_config(False) is None From 8d3027bcac9bf1954fdca6f43580571acc00c18b Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Mon, 20 Jul 2026 12:50:57 +0000 Subject: [PATCH 5/5] docs: add release notes for the component graph v0.5.0 update Signed-off-by: Sahas Subramanian --- RELEASE_NOTES.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d466455..7db6201 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,15 +2,18 @@ ## Summary - +This release updates the microgrid component graph library to v0.5.0 and adds a way to configure how the component graph is built. ## Upgrading - +* The per-category formulas (`pv`, `battery`, `chp`, `ev`) now read the component first and use the meter as the fallback. This is the opposite of the old order, and it changes the generated config. Regenerate stored configs with this release and review the diff. +* To keep the old order, pass `ComponentGraphConfig(prefer_meters_in_component_formulas=True)` to `load_configs`, `load_configs_from_api` or `ComponentGraphGenerator`, or use the new `--prefer-meters-in-component-formulas` flag of the CLI. +* The update brings more changes, for example to the `consumption` formula and to the graph validation error messages. See the [v0.5.0 release notes](https://github.com/frequenz-floss/frequenz-microgrid-component-graph-python/releases/tag/v0.5.0) of the component graph library for the full list. ## New Features - +* `load_configs` and `load_configs_from_api` take a `component_graph_config` argument, and `ComponentGraphGenerator` takes it as `config`. It controls how the component graph is built and how its formulas are generated. `ComponentGraphConfig` and `FormulaOverrides` are re-exported from `frequenz.gridpool` and `frequenz.gridpool.config`. +* The `generate-config` and `print-formulas` CLI commands take a `--prefer-meters-in-component-formulas` flag, which reads the meter before the component in the per-category formulas. ## Bug Fixes