diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a9c69b887..ad7420e2e 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -19,3 +19,6 @@ component. So far we only had configurations like this: Meter -> Inverter -> PV. However the scenario with Inverter -> PV is also possible and now handled correctly. +- Fix `consumer_power()` not working certain configurations. + In microgrids without consumers and no main meter, the formula + would never return any values. diff --git a/src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_consumer_power_formula.py b/src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_consumer_power_formula.py index 22c8fbad9..97ef78ad1 100644 --- a/src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_consumer_power_formula.py +++ b/src/frequenz/sdk/timeseries/_formula_engine/_formula_generators/_consumer_power_formula.py @@ -5,6 +5,7 @@ from __future__ import annotations +import logging from collections import abc from ....microgrid import connection_manager @@ -12,7 +13,13 @@ from ..._quantities import Power from .._formula_engine import FormulaEngine from .._resampled_formula_builder import ResampledFormulaBuilder -from ._formula_generator import ComponentNotFound, FormulaGenerator +from ._formula_generator import ( + NON_EXISTING_COMPONENT_ID, + ComponentNotFound, + FormulaGenerator, +) + +_logger = logging.getLogger(__name__) class ConsumerPowerFormula(FormulaGenerator[Power]): @@ -121,4 +128,17 @@ def _gen_without_grid_meter( is_first = False builder.push_component_metric(successor.component_id, nones_are_zeros=False) + if len(builder.finalize()[0]) == 0: + # If there are no consumer components, we have to send 0 values at the same + # frequency as the other streams. So we subscribe with a non-existing + # component id, just to get a `None` message at the resampling interval. + builder.push_component_metric( + NON_EXISTING_COMPONENT_ID, nones_are_zeros=True + ) + _logger.warning( + "Unable to find any consumers in the component graph. " + "Subscribing to the resampling actor with a non-existing " + "component id, so that `0` values are sent from the formula." + ) + return builder.build() diff --git a/tests/timeseries/test_logical_meter.py b/tests/timeseries/test_logical_meter.py index 4ef4ba0eb..50de6d4f1 100644 --- a/tests/timeseries/test_logical_meter.py +++ b/tests/timeseries/test_logical_meter.py @@ -267,6 +267,21 @@ async def test_consumer_power_no_grid_meter(self, mocker: MockerFixture) -> None await mockgrid.mock_resampler.send_meter_power([20.0, 2.0, 3.0, 4.0, 5.0]) assert (await consumer_power_receiver.receive()).value == Power.from_watts(20.0) + async def test_consumer_power_no_grid_meter_no_consumer_meter( + self, mocker: MockerFixture + ) -> None: + """Test the consumer power formula without a grid meter.""" + mockgrid = MockMicrogrid(grid_meter=False) + mockgrid.add_batteries(2) + mockgrid.add_solar_inverters(2) + await mockgrid.start(mocker) + + logical_meter = microgrid.logical_meter() + consumer_power_receiver = logical_meter.consumer_power.new_receiver() + + await mockgrid.mock_resampler.send_non_existing_component_value() + assert (await consumer_power_receiver.receive()).value == Power.from_watts(0.0) + async def test_producer_power(self, mocker: MockerFixture) -> None: """Test the producer power formula.""" mockgrid = MockMicrogrid(grid_meter=False)