From 78f8f6f366ca1cb29015564b625994bbb941069d Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Mon, 5 Jan 2026 13:33:39 +0100 Subject: [PATCH 1/6] Clear release notes Signed-off-by: Sahas Subramanian --- RELEASE_NOTES.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 05fb4498c..61ee6f2ad 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,17 @@ # Frequenz Python SDK Release Notes -## Bug fixes +## Summary -- `FormulaEngine` and `FormulaEngine3Phase` are now type aliases to `Formula` and `Formula3Phase`, fixing a typing issue introduced in `v1.0.0-rc2202`. + + +## Upgrading + + + +## New Features + + + +## Bug Fixes + + From 04250034580ceb84c58691c378fb23f4289225dc Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Mon, 5 Jan 2026 13:25:58 +0100 Subject: [PATCH 2/6] Validate component IDs when creating BatteryPool Signed-off-by: Sahas Subramanian --- .../battery_pool/_battery_pool_reference_store.py | 14 +++++++++++++- tests/microgrid/test_datapipeline.py | 11 +++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/frequenz/sdk/timeseries/battery_pool/_battery_pool_reference_store.py b/src/frequenz/sdk/timeseries/battery_pool/_battery_pool_reference_store.py index 382809082..68a03f597 100644 --- a/src/frequenz/sdk/timeseries/battery_pool/_battery_pool_reference_store.py +++ b/src/frequenz/sdk/timeseries/battery_pool/_battery_pool_reference_store.py @@ -81,12 +81,24 @@ def __init__( # pylint: disable=too-many-arguments batteries_id: Subset of the batteries that should be included in the battery pool. If None or empty, then all batteries from the microgrid will be used. + + Raises: + ValueError: If any of the specified batteries is not present in the + microgrid. """ self._batteries: frozenset[ComponentId] + all_batteries = self._get_all_batteries() if batteries_id: self._batteries = frozenset(batteries_id) + if not self._batteries.issubset(all_batteries): + unknown_ids = self._batteries - all_batteries + raise ValueError( + "Unable to create a BatteryPool. These component IDs are either " + + "not batteries or are unknown: " + + f"{unknown_ids}" + ) else: - self._batteries = self._get_all_batteries() + self._batteries = all_batteries self._working_batteries: set[ComponentId] = set() diff --git a/tests/microgrid/test_datapipeline.py b/tests/microgrid/test_datapipeline.py index 651a710d5..53a2e2d31 100644 --- a/tests/microgrid/test_datapipeline.py +++ b/tests/microgrid/test_datapipeline.py @@ -79,6 +79,17 @@ async def test_actors_started( datapipeline.new_battery_pool(priority=5) + datapipeline.new_battery_pool(priority=1, component_ids={ComponentId(15)}) + + with pytest.raises( + ValueError, + match=re.escape( + "Unable to create a BatteryPool. These component IDs are either not " + + "batteries or are unknown: frozenset({ComponentId(4)})" + ), + ): + datapipeline.new_battery_pool(priority=2, component_ids={ComponentId(4)}) + assert datapipeline._battery_power_wrapper._power_distributing_actor is not None await asyncio.sleep(1) assert datapipeline._battery_power_wrapper._power_distributing_actor.is_running From 3fd723bc38737f84621011cf4a5624ab5ec258d5 Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Mon, 5 Jan 2026 13:32:46 +0100 Subject: [PATCH 3/6] Validate component IDs when creating PVPool Signed-off-by: Sahas Subramanian --- .../pv_pool/_pv_pool_reference_store.py | 17 +++++++++++++---- tests/microgrid/test_datapipeline.py | 17 ++++++++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/frequenz/sdk/timeseries/pv_pool/_pv_pool_reference_store.py b/src/frequenz/sdk/timeseries/pv_pool/_pv_pool_reference_store.py index fbba10561..182435e6e 100644 --- a/src/frequenz/sdk/timeseries/pv_pool/_pv_pool_reference_store.py +++ b/src/frequenz/sdk/timeseries/pv_pool/_pv_pool_reference_store.py @@ -72,13 +72,22 @@ def __init__( # pylint: disable=too-many-arguments self.power_manager_bounds_subs_sender = power_manager_bounds_subs_sender self.power_distribution_results_fetcher = power_distribution_results_fetcher + graph = connection_manager.get().component_graph + all_solar_inverters = frozenset( + {inv.id for inv in graph.components(matching_types=SolarInverter)} + ) + if component_ids is not None: self.component_ids: frozenset[ComponentId] = frozenset(component_ids) + if not self.component_ids.issubset(all_solar_inverters): + unknown_ids = self.component_ids - all_solar_inverters + raise ValueError( + "Unable to create a PVPool. These component IDs are either " + + "not PV inverters or are unknown: " + + f"{unknown_ids}" + ) else: - graph = connection_manager.get().component_graph - self.component_ids = frozenset( - {inv.id for inv in graph.components(matching_types=SolarInverter)} - ) + self.component_ids = all_solar_inverters self.power_bounds_subs: dict[str, asyncio.Task[None]] = {} diff --git a/tests/microgrid/test_datapipeline.py b/tests/microgrid/test_datapipeline.py index 53a2e2d31..fcc1b2340 100644 --- a/tests/microgrid/test_datapipeline.py +++ b/tests/microgrid/test_datapipeline.py @@ -4,6 +4,7 @@ """Basic tests for the DataPipeline.""" import asyncio +import re from datetime import timedelta import async_solipsism @@ -16,6 +17,7 @@ ComponentConnection, GridConnectionPoint, LiIonBattery, + SolarInverter, ) from pytest_mock import MockerFixture @@ -68,18 +70,22 @@ async def test_actors_started( ) bat_inverter_4 = BatteryInverter(id=ComponentId(4), microgrid_id=_MICROGRID_ID) battery_15 = LiIonBattery(id=ComponentId(15), microgrid_id=_MICROGRID_ID) + pv_inverter_7 = SolarInverter(id=ComponentId(7), microgrid_id=_MICROGRID_ID) mock_client = MockMicrogridClient( - components={grid_1, bat_inverter_4, battery_15}, + components={grid_1, bat_inverter_4, battery_15, pv_inverter_7}, connections={ ComponentConnection(source=grid_1.id, destination=bat_inverter_4.id), ComponentConnection(source=bat_inverter_4.id, destination=battery_15.id), + ComponentConnection(source=grid_1.id, destination=pv_inverter_7.id), }, ) mock_client.initialize(mocker) datapipeline.new_battery_pool(priority=5) + datapipeline.new_pv_pool(priority=3) datapipeline.new_battery_pool(priority=1, component_ids={ComponentId(15)}) + datapipeline.new_pv_pool(priority=2, component_ids={ComponentId(7)}) with pytest.raises( ValueError, @@ -90,6 +96,15 @@ async def test_actors_started( ): datapipeline.new_battery_pool(priority=2, component_ids={ComponentId(4)}) + with pytest.raises( + ValueError, + match=re.escape( + "Unable to create a PVPool. These component IDs are either not PV " + + "inverters or are unknown: frozenset({ComponentId(1)})" + ), + ): + datapipeline.new_pv_pool(priority=4, component_ids={ComponentId(1)}) + assert datapipeline._battery_power_wrapper._power_distributing_actor is not None await asyncio.sleep(1) assert datapipeline._battery_power_wrapper._power_distributing_actor.is_running From 5c757070852e84c196a3e93ba1672cbdc8dabf0f Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Mon, 5 Jan 2026 13:44:19 +0100 Subject: [PATCH 4/6] Validate component IDs when creating EVChargerPool Signed-off-by: Sahas Subramanian --- .../_ev_charger_pool_reference_store.py | 16 +++++++++++---- tests/microgrid/test_datapipeline.py | 20 ++++++++++++++++++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool_reference_store.py b/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool_reference_store.py index 0e4542aa7..c91c8db34 100644 --- a/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool_reference_store.py +++ b/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool_reference_store.py @@ -71,13 +71,21 @@ def __init__( # pylint: disable=too-many-arguments self.power_manager_bounds_subs_sender = power_manager_bounds_subs_sender self.power_distribution_results_fetcher = power_distribution_results_fetcher + graph = connection_manager.get().component_graph + all_ev_chargers = frozenset( + {evc.id for evc in graph.components(matching_types=EvCharger)} + ) + if component_ids is not None: self.component_ids: frozenset[ComponentId] = frozenset(component_ids) + if not self.component_ids.issubset(all_ev_chargers): + unknown_ids = self.component_ids - all_ev_chargers + raise ValueError( + "Unable to create an EVChargerPool. These component IDs are either " + + f"not EV chargers or are unknown: {unknown_ids}" + ) else: - graph = connection_manager.get().component_graph - self.component_ids = frozenset( - {evc.id for evc in graph.components(matching_types=EvCharger)} - ) + self.component_ids = all_ev_chargers self.power_bounds_subs: dict[str, asyncio.Task[None]] = {} diff --git a/tests/microgrid/test_datapipeline.py b/tests/microgrid/test_datapipeline.py index fcc1b2340..8142a0281 100644 --- a/tests/microgrid/test_datapipeline.py +++ b/tests/microgrid/test_datapipeline.py @@ -13,6 +13,7 @@ from frequenz.client.common.microgrid import MicrogridId from frequenz.client.common.microgrid.components import ComponentId from frequenz.client.microgrid.component import ( + AcEvCharger, BatteryInverter, ComponentConnection, GridConnectionPoint, @@ -21,6 +22,7 @@ ) from pytest_mock import MockerFixture +from frequenz.sdk.microgrid import _data_pipeline from frequenz.sdk.microgrid._data_pipeline import _DataPipeline from frequenz.sdk.timeseries import ResamplerConfig2 @@ -71,21 +73,28 @@ async def test_actors_started( bat_inverter_4 = BatteryInverter(id=ComponentId(4), microgrid_id=_MICROGRID_ID) battery_15 = LiIonBattery(id=ComponentId(15), microgrid_id=_MICROGRID_ID) pv_inverter_7 = SolarInverter(id=ComponentId(7), microgrid_id=_MICROGRID_ID) + ev_charger_9 = AcEvCharger(id=ComponentId(9), microgrid_id=_MICROGRID_ID) + mock_client = MockMicrogridClient( - components={grid_1, bat_inverter_4, battery_15, pv_inverter_7}, + components={grid_1, bat_inverter_4, battery_15, pv_inverter_7, ev_charger_9}, connections={ ComponentConnection(source=grid_1.id, destination=bat_inverter_4.id), ComponentConnection(source=bat_inverter_4.id, destination=battery_15.id), ComponentConnection(source=grid_1.id, destination=pv_inverter_7.id), + ComponentConnection(source=grid_1.id, destination=ev_charger_9.id), }, ) mock_client.initialize(mocker) + _data_pipeline._DATA_PIPELINE = datapipeline + datapipeline.new_battery_pool(priority=5) datapipeline.new_pv_pool(priority=3) + datapipeline.new_ev_charger_pool(priority=4) datapipeline.new_battery_pool(priority=1, component_ids={ComponentId(15)}) datapipeline.new_pv_pool(priority=2, component_ids={ComponentId(7)}) + datapipeline.new_ev_charger_pool(priority=2, component_ids={ComponentId(9)}) with pytest.raises( ValueError, @@ -105,6 +114,15 @@ async def test_actors_started( ): datapipeline.new_pv_pool(priority=4, component_ids={ComponentId(1)}) + with pytest.raises( + ValueError, + match=re.escape( + "Unable to create an EVChargerPool. These component IDs are either " + + "not EV chargers or are unknown: frozenset({ComponentId(4)})" + ), + ): + datapipeline.new_ev_charger_pool(priority=5, component_ids={ComponentId(4)}) + assert datapipeline._battery_power_wrapper._power_distributing_actor is not None await asyncio.sleep(1) assert datapipeline._battery_power_wrapper._power_distributing_actor.is_running From a16200004e95b2fcd5af9f4f40ed35fd1e81bf5a Mon Sep 17 00:00:00 2001 From: Sahas Subramanian Date: Mon, 5 Jan 2026 13:46:57 +0100 Subject: [PATCH 5/6] Update release notes Signed-off-by: Sahas Subramanian --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 61ee6f2ad..1ecdb4847 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -14,4 +14,4 @@ ## Bug Fixes - +- Component IDs are validated during creation of battery, pv and ev charger pools, so that errors are caught early and we don't end up getting cryptic failures from somewhere else. From 1c3574d2bc71dab60e4d2f2dd7d103500de1ad6a Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Wed, 14 Jan 2026 12:04:02 +0100 Subject: [PATCH 6/6] Add missing Raises section to docstrings Signed-off-by: Leandro Lucarella --- .../ev_charger_pool/_ev_charger_pool_reference_store.py | 4 ++++ .../sdk/timeseries/pv_pool/_pv_pool_reference_store.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool_reference_store.py b/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool_reference_store.py index c91c8db34..7e4d4c27b 100644 --- a/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool_reference_store.py +++ b/src/frequenz/sdk/timeseries/ev_charger_pool/_ev_charger_pool_reference_store.py @@ -63,6 +63,10 @@ def __init__( # pylint: disable=too-many-arguments component_ids: An optional list of component_ids belonging to this pool. If not specified, IDs of all EV Chargers in the microgrid will be fetched from the component graph. + + Raises: + ValueError: If any of the specified component_ids are not EV chargers + or are unknown to the component graph. """ self.channel_registry = channel_registry self.resampler_subscription_sender = resampler_subscription_sender diff --git a/src/frequenz/sdk/timeseries/pv_pool/_pv_pool_reference_store.py b/src/frequenz/sdk/timeseries/pv_pool/_pv_pool_reference_store.py index 182435e6e..e893f9a36 100644 --- a/src/frequenz/sdk/timeseries/pv_pool/_pv_pool_reference_store.py +++ b/src/frequenz/sdk/timeseries/pv_pool/_pv_pool_reference_store.py @@ -64,6 +64,10 @@ def __init__( # pylint: disable=too-many-arguments component_ids: An optional list of component_ids belonging to this pool. If not specified, IDs of all PV inverters in the microgrid will be fetched from the component graph. + + Raises: + ValueError: If any of the provided component_ids are not PV inverters or + are unknown to the component graph. """ self.channel_registry = channel_registry self.resampler_subscription_sender = resampler_subscription_sender