From 02c2c7b5e78c220f2cfcb31282608bb6ee458505 Mon Sep 17 00:00:00 2001 From: Mohammad Tayyab Date: Thu, 21 May 2026 13:41:57 +0200 Subject: [PATCH 1/9] feat: add load_configs_from_assets_api in gridpool Signed-off-by: Mohammad Tayyab --- src/frequenz/gridpool/__init__.py | 15 ++- src/frequenz/gridpool/_microgrid_config.py | 125 +++++++++++++++++++++ 2 files changed, 138 insertions(+), 2 deletions(-) diff --git a/src/frequenz/gridpool/__init__.py b/src/frequenz/gridpool/__init__.py index 78c4629..c539309 100644 --- a/src/frequenz/gridpool/__init__.py +++ b/src/frequenz/gridpool/__init__.py @@ -4,6 +4,17 @@ """High-level interface to grid pools for the Frequenz platform.""" from ._graph_generator import ComponentGraphGenerator -from ._microgrid_config import Metadata, MicrogridConfig +from ._microgrid_config import ( + Metadata, + MicrogridConfig, + merge_config_maps, + merge_microgrid_configs, +) -__all__ = ["ComponentGraphGenerator", "Metadata", "MicrogridConfig"] +__all__ = [ + "ComponentGraphGenerator", + "Metadata", + "MicrogridConfig", + "merge_config_maps", + "merge_microgrid_configs", +] diff --git a/src/frequenz/gridpool/_microgrid_config.py b/src/frequenz/gridpool/_microgrid_config.py index f83fe8f..c3dcf3b 100644 --- a/src/frequenz/gridpool/_microgrid_config.py +++ b/src/frequenz/gridpool/_microgrid_config.py @@ -6,6 +6,7 @@ import logging import re import tomllib +from copy import deepcopy from dataclasses import field from datetime import datetime from pathlib import Path @@ -420,6 +421,67 @@ def load_configs( return microgrid_configs + @staticmethod + async def load_configs_from_assets_api( + assets_url: str, + assets_auth_key: str, + assets_sign_secret: str, + microgrid_ids: list[int], + populate_formulas: bool = True, + ) -> dict[str, "MicrogridConfig"]: + """Load microgrid configs with location metadata from the Assets API. + + Fetches each microgrid's location (latitude, longitude) and optionally + populates formulas from the component graph. This is the canonical + single-source loader for both metadata and formulas so that callers + (e.g. the forecast pipeline) do not have to re-implement this logic. + + Args: + assets_url: + Base URL of the Assets API. + assets_auth_key: + Authentication key used to access the Assets API. + assets_sign_secret: + Signing secret used for authenticated API requests. + microgrid_ids: + List of microgrid IDs to load configurations for. + populate_formulas: + When ``True`` (default), formulas are derived from the component + graph and written into each config via + :func:`populate_missing_formulas`. Set to ``False`` to load + metadata only. + + Returns: + dict[str, MicrogridConfig]: + Mapping from microgrid ID (as string) to the populated + ``MicrogridConfig`` instance. + """ + async with AssetsApiClient( + assets_url, + auth_key=assets_auth_key, + sign_secret=assets_sign_secret, + ) as client: + configs: dict[str, MicrogridConfig] = {} + for microgrid_id in microgrid_ids: + mgrid = await client.get_microgrid(MicrogridId(microgrid_id)) + location = mgrid.location if mgrid.location else None + cfg = MicrogridConfig( + meta=Metadata( + microgrid_id=microgrid_id, + latitude=location.latitude if location else None, + longitude=location.longitude if location else None, + ) + ) + if populate_formulas: + await populate_missing_formulas( + microgrid_id=microgrid_id, + config=cfg, + assets_client=client, + ) + configs[str(microgrid_id)] = cfg + + return configs + @staticmethod async def load_configs_with_formulas( assets_url: str, @@ -479,6 +541,69 @@ async def load_configs_with_formulas( return microgrid_configs +def merge_microgrid_configs( + base: MicrogridConfig, + override: MicrogridConfig, +) -> MicrogridConfig: + """Merge two :class:`MicrogridConfig` objects. + + The *override* config takes precedence over *base*. Nested dictionaries + are merged recursively. If a field in *override* is ``None`` the value + from *base* is retained, so partial overrides never nullify existing data. + + Args: + base: The base MicrogridConfig. + override: The overriding MicrogridConfig. + + Returns: + A new MicrogridConfig representing the merged result. + """ + schema = MicrogridConfig.Schema() + base_dict = schema.dump(base) + override_dict = schema.dump(override) + + def _deep_merge(a: dict[Any, Any], b: dict[Any, Any]) -> dict[Any, Any]: + result = deepcopy(a) + for k, v in b.items(): + if v is None: + continue + if isinstance(v, dict) and isinstance(result.get(k), dict): + result[k] = _deep_merge(result[k], v) + else: + result[k] = v + return result + + merged = schema.load(_deep_merge(base_dict, override_dict)) + assert isinstance(merged, MicrogridConfig) + return merged + + +def merge_config_maps( + base: dict[str, MicrogridConfig], + override: dict[str, MicrogridConfig], +) -> dict[str, MicrogridConfig]: + """Merge two dictionaries of :class:`MicrogridConfig` objects. + + For microgrid IDs present in both maps the configs are merged via + :func:`merge_microgrid_configs`. IDs that exist only in one map are + included unchanged. + + Args: + base: The base dictionary of MicrogridConfig objects. + override: The overriding dictionary of MicrogridConfig objects. + + Returns: + A new dictionary representing the merged result. + """ + merged = dict(base) + for mid, cfg in override.items(): + if mid in merged: + merged[mid] = merge_microgrid_configs(merged[mid], cfg) + else: + merged[mid] = cfg + return merged + + async def populate_missing_formulas( microgrid_id: int, config: "MicrogridConfig", From d2306bddb927c8d2287830dac1b1f015113efda3 Mon Sep 17 00:00:00 2001 From: Mohammad Tayyab Date: Thu, 21 May 2026 13:52:12 +0200 Subject: [PATCH 2/9] docs: update release notes Signed-off-by: Mohammad Tayyab --- RELEASE_NOTES.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 09896ab..698e411 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,7 +2,7 @@ ## Summary - +This release adds a new Assets API based configuration loader, introduces helpers to merge microgrid configs, and updates PV curtailability behavior to support unspecified values. ## Upgrading @@ -10,7 +10,9 @@ ## New Features -* Add start and end time to microgrid configs. +* Added `MicrogridConfig.load_configs_from_assets_api(...)` to load microgrid metadata (latitude/longitude) from the Assets API and optionally populate formulas from the component graph. +* Added `merge_microgrid_configs(...)` for deep-merging two `MicrogridConfig` objects where override values take precedence and `None` does not overwrite base values. +* Added `merge_config_maps(...)` for merging two dictionaries of microgrid configs by microgrid ID. ## Bug Fixes From 48e4571d0a7639bfc883d9ea76a3c8dcf906c7dc Mon Sep 17 00:00:00 2001 From: cwasicki <126617870+cwasicki@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:49:28 +0200 Subject: [PATCH 3/9] docs(config): drop RST roles from docstrings in favour of Markdown Replace `:func:`/`:class:` cross-reference roles and ``double-backtick`` literals with plain single-backtick Markdown, per the project docstring style. Docs-only change, no behaviour change. Signed-off-by: cwasicki <126617870+cwasicki@users.noreply.github.com> --- src/frequenz/gridpool/_microgrid_config.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/frequenz/gridpool/_microgrid_config.py b/src/frequenz/gridpool/_microgrid_config.py index c3dcf3b..059cc45 100644 --- a/src/frequenz/gridpool/_microgrid_config.py +++ b/src/frequenz/gridpool/_microgrid_config.py @@ -446,15 +446,15 @@ async def load_configs_from_assets_api( microgrid_ids: List of microgrid IDs to load configurations for. populate_formulas: - When ``True`` (default), formulas are derived from the component + When `True` (default), formulas are derived from the component graph and written into each config via - :func:`populate_missing_formulas`. Set to ``False`` to load + `populate_missing_formulas`. Set to `False` to load metadata only. Returns: dict[str, MicrogridConfig]: Mapping from microgrid ID (as string) to the populated - ``MicrogridConfig`` instance. + `MicrogridConfig` instance. """ async with AssetsApiClient( assets_url, @@ -513,10 +513,10 @@ async def load_configs_with_formulas( Returns: dict[str, MicrogridConfig]: Mapping from microgrid ID (as string) to the corresponding populated - ``MicrogridConfig`` instance. + `MicrogridConfig` instance. Notes: - - Configuration files are first loaded via ``MicrogridConfig.load_configs``. + - Configuration files are first loaded via `MicrogridConfig.load_configs`. - Any missing formulas are populated by querying the Assets API and generating formulas from the microgrid component graph. """ @@ -545,10 +545,10 @@ def merge_microgrid_configs( base: MicrogridConfig, override: MicrogridConfig, ) -> MicrogridConfig: - """Merge two :class:`MicrogridConfig` objects. + """Merge two `MicrogridConfig` objects. The *override* config takes precedence over *base*. Nested dictionaries - are merged recursively. If a field in *override* is ``None`` the value + are merged recursively. If a field in *override* is `None` the value from *base* is retained, so partial overrides never nullify existing data. Args: @@ -582,10 +582,10 @@ def merge_config_maps( base: dict[str, MicrogridConfig], override: dict[str, MicrogridConfig], ) -> dict[str, MicrogridConfig]: - """Merge two dictionaries of :class:`MicrogridConfig` objects. + """Merge two dictionaries of `MicrogridConfig` objects. For microgrid IDs present in both maps the configs are merged via - :func:`merge_microgrid_configs`. IDs that exist only in one map are + `merge_microgrid_configs`. IDs that exist only in one map are included unchanged. Args: @@ -633,8 +633,8 @@ async def populate_missing_formulas( None. The configuration is modified in place. Notes: - - Existing formulas in ``config`` are never overwritten. - - For missing component types, a new ``ComponentTypeConfig`` is created. + - Existing formulas in `config` are never overwritten. + - For missing component types, a new `ComponentTypeConfig` is created. - The same derived formula is assigned to all supported metric keys for a given component type when missing. """ From f089610b2e384c60b69e1aee0893a83040c2d8c2 Mon Sep 17 00:00:00 2001 From: cwasicki <126617870+cwasicki@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:05:16 +0200 Subject: [PATCH 4/9] fix(config): isolate per-microgrid failures in the Assets API loader Wrap each microgrid's fetch-and-populate in load_configs_from_assets_api in a try/except: a microgrid that cannot be loaded is logged as a warning and skipped, instead of aborting the whole batch. Signed-off-by: cwasicki <126617870+cwasicki@users.noreply.github.com> --- src/frequenz/gridpool/_microgrid_config.py | 38 ++++++++++++++-------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/frequenz/gridpool/_microgrid_config.py b/src/frequenz/gridpool/_microgrid_config.py index 059cc45..722a32e 100644 --- a/src/frequenz/gridpool/_microgrid_config.py +++ b/src/frequenz/gridpool/_microgrid_config.py @@ -454,7 +454,9 @@ async def load_configs_from_assets_api( Returns: dict[str, MicrogridConfig]: Mapping from microgrid ID (as string) to the populated - `MicrogridConfig` instance. + `MicrogridConfig` instance. Microgrids that could not be loaded + are logged as warnings and omitted, so the returned mapping may + cover fewer microgrids than were requested. """ async with AssetsApiClient( assets_url, @@ -463,21 +465,29 @@ async def load_configs_from_assets_api( ) as client: configs: dict[str, MicrogridConfig] = {} for microgrid_id in microgrid_ids: - mgrid = await client.get_microgrid(MicrogridId(microgrid_id)) - location = mgrid.location if mgrid.location else None - cfg = MicrogridConfig( - meta=Metadata( - microgrid_id=microgrid_id, - latitude=location.latitude if location else None, - longitude=location.longitude if location else None, + try: + mgrid = await client.get_microgrid(MicrogridId(microgrid_id)) + location = mgrid.location if mgrid.location else None + cfg = MicrogridConfig( + meta=Metadata( + microgrid_id=microgrid_id, + latitude=location.latitude if location else None, + longitude=location.longitude if location else None, + ) ) - ) - if populate_formulas: - await populate_missing_formulas( - microgrid_id=microgrid_id, - config=cfg, - assets_client=client, + if populate_formulas: + await populate_missing_formulas( + microgrid_id=microgrid_id, + config=cfg, + assets_client=client, + ) + except Exception as exc: # pylint: disable=broad-except + _logger.warning( + "Failed to load microgrid %s from the Assets API: %s", + microgrid_id, + exc, ) + continue configs[str(microgrid_id)] = cfg return configs From ea6dc6e376ee38efe7a157ab712afebd4ca4661d Mon Sep 17 00:00:00 2001 From: cwasicki <126617870+cwasicki@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:07:35 +0200 Subject: [PATCH 5/9] test(config): add unit tests for config merge helpers Cover merge_microgrid_configs and merge_config_maps. Signed-off-by: cwasicki <126617870+cwasicki@users.noreply.github.com> --- tests/test_merge_config.py | 123 +++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/test_merge_config.py diff --git a/tests/test_merge_config.py b/tests/test_merge_config.py new file mode 100644 index 0000000..5971c26 --- /dev/null +++ b/tests/test_merge_config.py @@ -0,0 +1,123 @@ +# License: MIT +# Copyright © 2026 Frequenz Energy-as-a-Service GmbH + +"""Tests for the config merge helpers.""" + +from typing import Any + +from frequenz.gridpool import ( + MicrogridConfig, + merge_config_maps, + merge_microgrid_configs, +) + + +def _make_config(mid: int, entry: dict[str, Any]) -> MicrogridConfig: + """Build a single MicrogridConfig from a table entry. + + Args: + mid: The microgrid ID; also used as the table key. + entry: The microgrid config body (meta, ctype, ...). + + Returns: + The parsed MicrogridConfig instance. + """ + meta = {"microgrid_id": mid, **entry.get("meta", {})} + # pylint: disable=protected-access + return MicrogridConfig._load_table_entries({str(mid): {**entry, "meta": meta}})[ + str(mid) + ] + + +def test_merge_override_wins() -> None: + """Override scalar fields take precedence over the base.""" + base = _make_config(1, {"meta": {"name": "Base Grid", "latitude": 10.0}}) + override = _make_config(1, {"meta": {"name": "New Grid", "latitude": 99.0}}) + + merged = merge_microgrid_configs(base, override) + + assert merged.meta is not None + assert merged.meta.name == "New Grid" + assert merged.meta.latitude == 99.0 + + +def test_merge_none_does_not_nullify() -> None: + """Fields absent from the override retain their base values.""" + base = _make_config( + 1, + {"meta": {"name": "Base Grid", "enterprise_id": 5, "longitude": 20.0}}, + ) + # Override only sets the name; enterprise_id/longitude are None and skipped. + override = _make_config(1, {"meta": {"name": "New Grid"}}) + + merged = merge_microgrid_configs(base, override) + + assert merged.meta is not None + assert merged.meta.name == "New Grid" + assert merged.meta.enterprise_id == 5 + assert merged.meta.longitude == 20.0 + + +def test_merge_nested_ctype() -> None: + """Nested dicts are merged recursively, keeping base-only keys.""" + base = _make_config( + 1, + { + "ctype": { + "pv": {"meter": [1, 2]}, + "battery": {"inverter": [3, 4]}, + } + }, + ) + override = _make_config( + 1, + { + "ctype": { + "pv": {"formula": {"AC_POWER_ACTIVE": "#1"}}, + "grid": {"meter": [9]}, + } + }, + ) + + merged = merge_microgrid_configs(base, override) + + # pv: base meter retained, override formula added. + assert merged.ctype["pv"].meter == [1, 2] + assert merged.ctype["pv"].formula == {"AC_POWER_ACTIVE": "#1"} + # battery: present only in base, kept unchanged. + assert merged.ctype["battery"].inverter == [3, 4] + # grid: present only in override, added. + assert merged.ctype["grid"].meter == [9] + + +def test_merge_does_not_mutate_inputs() -> None: + """Merging returns a new config without mutating base or override.""" + base = _make_config(1, {"meta": {"name": "Base Grid"}}) + override = _make_config(1, {"meta": {"name": "New Grid"}}) + + merge_microgrid_configs(base, override) + + assert base.meta is not None and base.meta.name == "Base Grid" + assert override.meta is not None and override.meta.name == "New Grid" + + +def test_merge_config_maps() -> None: + """Maps are merged per ID; IDs unique to one map are passed through.""" + base = { + "1": _make_config(1, {"meta": {"name": "Base One"}}), + "2": _make_config(2, {"meta": {"name": "Base Two"}}), + } + override = { + "1": _make_config(1, {"meta": {"name": "Override One"}}), + "3": _make_config(3, {"meta": {"name": "Override Three"}}), + } + + merged = merge_config_maps(base, override) + + assert set(merged) == {"1", "2", "3"} + # Shared ID: override wins. + assert merged["1"].meta is not None and merged["1"].meta.name == "Override One" + # Base-only ID: unchanged. + assert merged["2"].meta is not None and merged["2"].meta.name == "Base Two" + # Override-only ID: included. + assert merged["3"].meta is not None and merged["3"].meta.name == "Override Three" From 80024b340bb29763513f357424354c7dbe20707c Mon Sep 17 00:00:00 2001 From: cwasicki <126617870+cwasicki@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:32:44 +0200 Subject: [PATCH 6/9] fix(config): drop generation from auto-derived formulas `generation` is not yet supported. Signed-off-by: cwasicki <126617870+cwasicki@users.noreply.github.com> --- src/frequenz/gridpool/_microgrid_config.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/frequenz/gridpool/_microgrid_config.py b/src/frequenz/gridpool/_microgrid_config.py index 722a32e..8025aae 100644 --- a/src/frequenz/gridpool/_microgrid_config.py +++ b/src/frequenz/gridpool/_microgrid_config.py @@ -623,9 +623,9 @@ async def populate_missing_formulas( """Populate missing component formulas from the assets API graph. Builds a component graph for the given microgrid and derives default formulas - for common component types such as consumption, generation, grid, PV, battery, - CHP, and EV charging. Existing formulas already present in the configuration - are preserved; only missing component-type entries or missing metric formulas + for common component types such as consumption, grid, PV, battery, CHP, and + EV charging. Existing formulas already present in the configuration are + preserved; only missing component-type entries or missing metric formulas are filled in. Args: @@ -653,7 +653,6 @@ async def populate_missing_formulas( auto_formulas = { "consumption": graph.consumer_formula(), - "generation": graph.producer_formula(), "grid": graph.grid_formula(), "pv": graph.pv_formula(None), "battery": graph.battery_formula(None), From b0a072215685298fa7cbf36a2ba980f2df4b2a5c Mon Sep 17 00:00:00 2001 From: cwasicki <126617870+cwasicki@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:33:05 +0200 Subject: [PATCH 7/9] fix(config): only derive the AC_POWER_ACTIVE formula Formulas for all these metrics are the same. The formula name should be renamed to something more generic in the future. Signed-off-by: cwasicki <126617870+cwasicki@users.noreply.github.com> --- src/frequenz/gridpool/_microgrid_config.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/frequenz/gridpool/_microgrid_config.py b/src/frequenz/gridpool/_microgrid_config.py index 8025aae..5149222 100644 --- a/src/frequenz/gridpool/_microgrid_config.py +++ b/src/frequenz/gridpool/_microgrid_config.py @@ -645,8 +645,8 @@ async def populate_missing_formulas( Notes: - Existing formulas in `config` are never overwritten. - For missing component types, a new `ComponentTypeConfig` is created. - - The same derived formula is assigned to all supported metric keys for a - given component type when missing. + - The derived formula is assigned to the `AC_POWER_ACTIVE` metric key + for a given component type when missing. """ cgg = ComponentGraphGenerator(assets_client) graph = await cgg.get_component_graph(MicrogridId(microgrid_id)) @@ -660,14 +660,6 @@ async def populate_missing_formulas( "ev": graph.ev_charger_formula(None), } - metrics = ( - "AC_POWER_ACTIVE", - "AC_ACTIVE_POWER", - "AC_ENERGY_ACTIVE", - "AC_ENERGY_ACTIVE_CONSUMED", - "AC_ENERGY_ACTIVE_DELIVERED", - ) - # Default: only populate component types already present in the config. allowed_ctypes = component_types or set(config.ctype.keys()) @@ -682,6 +674,5 @@ async def populate_missing_formulas( if cfg.formula is None: cfg.formula = {} - for metric in metrics: - if metric not in cfg.formula: - cfg.formula[metric] = formula + if "AC_POWER_ACTIVE" not in cfg.formula: + cfg.formula["AC_POWER_ACTIVE"] = formula From 47f056641415fc953168f43ba37bda2dd1f2c5d5 Mon Sep 17 00:00:00 2001 From: cwasicki <126617870+cwasicki@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:33:33 +0200 Subject: [PATCH 8/9] fix(config): derive formulas for all component types by default The assets-API loader calls populate_missing_formulas on a config with an empty ctype map and no explicit component_types, so the previous default (component types already present in the config) matched nothing and no formulas were ever written. Default to every derivable component type and create a ComponentTypeConfig for types not yet in the config. Signed-off-by: cwasicki <126617870+cwasicki@users.noreply.github.com> --- src/frequenz/gridpool/_microgrid_config.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/frequenz/gridpool/_microgrid_config.py b/src/frequenz/gridpool/_microgrid_config.py index 5149222..ae4fce4 100644 --- a/src/frequenz/gridpool/_microgrid_config.py +++ b/src/frequenz/gridpool/_microgrid_config.py @@ -637,7 +637,9 @@ async def populate_missing_formulas( assets_client: Assets API client used to fetch the component graph. component_types: - Optional set of component types to consider when populating formulas. + Set of component types to consider when populating formulas. When + `None` (the default), every component type a formula can be derived + for is considered. Returns: None. The configuration is modified in place. @@ -660,16 +662,14 @@ async def populate_missing_formulas( "ev": graph.ev_charger_formula(None), } - # Default: only populate component types already present in the config. - allowed_ctypes = component_types or set(config.ctype.keys()) - for ctype, formula in auto_formulas.items(): - if ctype not in allowed_ctypes: + if component_types is not None and ctype not in component_types: continue cfg = config.ctype.get(ctype) if cfg is None: - continue + cfg = ComponentTypeConfig() + config.ctype[ctype] = cfg if cfg.formula is None: cfg.formula = {} From 30c574db73f074fbf7747f1595ca82a71f4ab3e9 Mon Sep 17 00:00:00 2001 From: cwasicki <126617870+cwasicki@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:33:50 +0200 Subject: [PATCH 9/9] fix(config): skip empty or zero derived formulas Component types absent from a microgrid yield an empty formula or a bare zero constant; now that all types are derived by default, guard against storing those no-op formulas. Signed-off-by: cwasicki <126617870+cwasicki@users.noreply.github.com> --- src/frequenz/gridpool/_microgrid_config.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/frequenz/gridpool/_microgrid_config.py b/src/frequenz/gridpool/_microgrid_config.py index ae4fce4..b372d69 100644 --- a/src/frequenz/gridpool/_microgrid_config.py +++ b/src/frequenz/gridpool/_microgrid_config.py @@ -614,6 +614,21 @@ def merge_config_maps( return merged +def _is_zero_formula(formula: str) -> bool: + """Return whether a derived formula is empty or a constant zero. + + Component types absent from a microgrid yield an empty formula or one that + is just a zero constant (e.g. `0.0`), which is not worth storing. + """ + stripped = formula.strip() + if not stripped: + return True + try: + return float(stripped) == 0.0 + except ValueError: + return False + + async def populate_missing_formulas( microgrid_id: int, config: "MicrogridConfig", @@ -666,6 +681,11 @@ async def populate_missing_formulas( if component_types is not None and ctype not in component_types: continue + # Skip component types, whose derived formula + # is empty or evaluates to a constant zero. + if _is_zero_formula(formula): + continue + cfg = config.ctype.get(ctype) if cfg is None: cfg = ComponentTypeConfig()