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/benchmarks/timeseries/benchmark_datasourcing.py b/benchmarks/timeseries/benchmark_datasourcing.py index 9fffbf0b8..ff5a294b3 100644 --- a/benchmarks/timeseries/benchmark_datasourcing.py +++ b/benchmarks/timeseries/benchmark_datasourcing.py @@ -70,7 +70,7 @@ async def benchmark_data_sourcing( num_ev_chargers * len(COMPONENT_METRIC_IDS) * num_msgs_per_battery ) mock_grid = MockMicrogrid( - grid_side_meter=False, num_values=num_msgs_per_battery, sample_rate_s=0.0 + grid_meter=False, num_values=num_msgs_per_battery, sample_rate_s=0.0 ) mock_grid.add_ev_chargers(num_ev_chargers) 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/actor/test_battery_pool_status.py b/tests/actor/test_battery_pool_status.py index b17879a40..4e0f40475 100644 --- a/tests/actor/test_battery_pool_status.py +++ b/tests/actor/test_battery_pool_status.py @@ -30,7 +30,7 @@ async def test_batteries_status(self, mocker: MockerFixture) -> None: Args: mock_microgrid: mock microgrid client """ - mock_microgrid = MockMicrogrid(grid_side_meter=True) + mock_microgrid = MockMicrogrid(grid_meter=True) mock_microgrid.add_batteries(3) await mock_microgrid.start(mocker) diff --git a/tests/actor/test_battery_status.py b/tests/actor/test_battery_status.py index 9d38b90a9..375ebf64b 100644 --- a/tests/actor/test_battery_status.py +++ b/tests/actor/test_battery_status.py @@ -156,7 +156,7 @@ async def test_sync_update_status_with_messages( Args: mock_microgrid: mock_microgrid fixture """ - mock_microgrid = MockMicrogrid(grid_side_meter=True) + mock_microgrid = MockMicrogrid(grid_meter=True) mock_microgrid.add_batteries(3) await mock_microgrid.start(mocker) @@ -318,7 +318,7 @@ async def test_sync_blocking_feature(self, mocker: MockerFixture) -> None: Args: mock_microgrid: mock_microgrid fixture """ - mock_microgrid = MockMicrogrid(grid_side_meter=True) + mock_microgrid = MockMicrogrid(grid_meter=True) mock_microgrid.add_batteries(3) await mock_microgrid.start(mocker) @@ -435,7 +435,7 @@ async def test_sync_blocking_interrupted_with_with_max_data( Args: mock_microgrid: mock_microgrid fixture """ - mock_microgrid = MockMicrogrid(grid_side_meter=True) + mock_microgrid = MockMicrogrid(grid_meter=True) mock_microgrid.add_batteries(3) await mock_microgrid.start(mocker) @@ -486,7 +486,7 @@ async def test_sync_blocking_interrupted_with_invalid_message( Args: mock_microgrid: mock_microgrid fixture """ - mock_microgrid = MockMicrogrid(grid_side_meter=True) + mock_microgrid = MockMicrogrid(grid_meter=True) mock_microgrid.add_batteries(3) await mock_microgrid.start(mocker) @@ -547,7 +547,7 @@ async def test_timers(self, mocker: MockerFixture) -> None: mock_microgrid: mock_microgrid fixture mocker: pytest mocker instance """ - mock_microgrid = MockMicrogrid(grid_side_meter=True) + mock_microgrid = MockMicrogrid(grid_meter=True) mock_microgrid.add_batteries(3) await mock_microgrid.start(mocker) @@ -609,7 +609,7 @@ async def test_async_battery_status(self, mocker: MockerFixture) -> None: Args: mock_microgrid: mock_microgrid fixture """ - mock_microgrid = MockMicrogrid(grid_side_meter=True) + mock_microgrid = MockMicrogrid(grid_meter=True) mock_microgrid.add_batteries(3) await mock_microgrid.start(mocker) @@ -690,7 +690,7 @@ async def setup_tracker( self, mocker: MockerFixture ) -> AsyncIterator[tuple[MockMicrogrid, Receiver[Status]]]: """Setup a BatteryStatusTracker instance to run tests with.""" - mock_microgrid = MockMicrogrid(grid_side_meter=True) + mock_microgrid = MockMicrogrid(grid_meter=True) mock_microgrid.add_batteries(1) await mock_microgrid.start(mocker) diff --git a/tests/actor/test_power_distributing.py b/tests/actor/test_power_distributing.py index 0dc4772f7..d8e0ab396 100644 --- a/tests/actor/test_power_distributing.py +++ b/tests/actor/test_power_distributing.py @@ -47,7 +47,7 @@ class TestPowerDistributingActor: async def test_constructor(self, mocker: MockerFixture) -> None: """Test if gets all necessary data.""" - mockgrid = MockMicrogrid(grid_side_meter=True) + mockgrid = MockMicrogrid(grid_meter=True) mockgrid.add_batteries(2) mockgrid.add_batteries(1, no_meter=True) await mockgrid.start(mocker) @@ -68,7 +68,7 @@ async def test_constructor(self, mocker: MockerFixture) -> None: await mockgrid.cleanup() # Test if it works without grid side meter - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_batteries(1) mockgrid.add_batteries(2, no_meter=True) await mockgrid.start(mocker) @@ -108,7 +108,7 @@ async def init_component_data(self, mockgrid: MockMicrogrid) -> None: async def test_power_distributor_one_user(self, mocker: MockerFixture) -> None: """Test if power distribution works with single user works.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_batteries(3) await mockgrid.start(mocker) await self.init_component_data(mockgrid) @@ -157,7 +157,7 @@ async def test_power_distributor_one_user(self, mocker: MockerFixture) -> None: async def test_battery_soc_nan(self, mocker: MockerFixture) -> None: """Test if battery with SoC==NaN is not used.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_batteries(3) await mockgrid.start(mocker) await self.init_component_data(mockgrid) @@ -222,7 +222,7 @@ async def test_battery_soc_nan(self, mocker: MockerFixture) -> None: async def test_battery_capacity_nan(self, mocker: MockerFixture) -> None: """Test battery with capacity set to NaN is not used.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_batteries(3) await mockgrid.start(mocker) await self.init_component_data(mockgrid) @@ -280,7 +280,7 @@ async def test_battery_capacity_nan(self, mocker: MockerFixture) -> None: async def test_battery_power_bounds_nan(self, mocker: MockerFixture) -> None: """Test battery with power bounds set to NaN is not used.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_batteries(3) await mockgrid.start(mocker) await self.init_component_data(mockgrid) @@ -356,7 +356,7 @@ async def test_power_distributor_invalid_battery_id( self, mocker: MockerFixture ) -> None: """Test if power distribution raises error if any battery id is invalid.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_batteries(3) await mockgrid.start(mocker) await self.init_component_data(mockgrid) @@ -404,7 +404,7 @@ async def test_power_distributor_one_user_adjust_power_consume( self, mocker: MockerFixture ) -> None: """Test if power distribution works with single user works.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_batteries(3) await mockgrid.start(mocker) await self.init_component_data(mockgrid) @@ -457,7 +457,7 @@ async def test_power_distributor_one_user_adjust_power_supply( self, mocker: MockerFixture ) -> None: """Test if power distribution works with single user works.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_batteries(3) await mockgrid.start(mocker) await self.init_component_data(mockgrid) @@ -510,7 +510,7 @@ async def test_power_distributor_one_user_adjust_power_success( self, mocker: MockerFixture ) -> None: """Test if power distribution works with single user works.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_batteries(3) await mockgrid.start(mocker) await self.init_component_data(mockgrid) @@ -561,7 +561,7 @@ async def test_power_distributor_one_user_adjust_power_success( async def test_not_all_batteries_are_working(self, mocker: MockerFixture) -> None: """Test if power distribution works if not all batteries are working.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_batteries(3) await mockgrid.start(mocker) await self.init_component_data(mockgrid) @@ -615,7 +615,7 @@ async def test_use_all_batteries_none_is_working( self, mocker: MockerFixture ) -> None: """Test all batteries are used if none of them works.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_batteries(3) await mockgrid.start(mocker) await self.init_component_data(mockgrid) @@ -668,7 +668,7 @@ async def test_force_request_a_battery_is_not_working( self, mocker: MockerFixture ) -> None: """Test force request when a battery is not working.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_batteries(3) await mockgrid.start(mocker) await self.init_component_data(mockgrid) @@ -724,7 +724,7 @@ async def test_force_request_battery_nan_value_non_cached( ) -> None: """Test battery with NaN in SoC, capacity or power is used if request is forced.""" # pylint: disable=too-many-locals - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_batteries(3) await mockgrid.start(mocker) await self.init_component_data(mockgrid) @@ -797,7 +797,7 @@ async def test_force_request_batteries_nan_values_cached( self, mocker: MockerFixture ) -> None: """Test battery with NaN in SoC, capacity or power is used if request is forced.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_batteries(3) await mockgrid.start(mocker) await self.init_component_data(mockgrid) diff --git a/tests/timeseries/_battery_pool/test_battery_pool.py b/tests/timeseries/_battery_pool/test_battery_pool.py index 72f41e3bf..a0e836bbe 100644 --- a/tests/timeseries/_battery_pool/test_battery_pool.py +++ b/tests/timeseries/_battery_pool/test_battery_pool.py @@ -461,7 +461,7 @@ async def run_test_battery_status_channel( # pylint: disable=too-many-arguments async def test_battery_pool_power(mocker: MockerFixture) -> None: """Test `BatteryPool.{,production,consumption}_power` methods.""" - mockgrid = MockMicrogrid(grid_side_meter=True) + mockgrid = MockMicrogrid(grid_meter=True) mockgrid.add_batteries(2) await mockgrid.start(mocker) diff --git a/tests/timeseries/_formula_engine/test_formula_composition.py b/tests/timeseries/_formula_engine/test_formula_composition.py index 3c6082af6..0d9fdef9f 100644 --- a/tests/timeseries/_formula_engine/test_formula_composition.py +++ b/tests/timeseries/_formula_engine/test_formula_composition.py @@ -25,16 +25,17 @@ async def test_formula_composition( # pylint: disable=too-many-locals mocker: MockerFixture, ) -> None: """Test the composition of formulas.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) + mockgrid.add_consumer_meters() mockgrid.add_batteries(3) mockgrid.add_solar_inverters(2) await mockgrid.start(mocker) logical_meter = microgrid.logical_meter() battery_pool = microgrid.battery_pool() - main_meter_recv = get_resampled_stream( + grid_meter_recv = get_resampled_stream( logical_meter._namespace, # pylint: disable=protected-access - 4, + mockgrid.meter_ids[0], ComponentMetricId.ACTIVE_POWER, Power.from_watts, ) @@ -53,7 +54,7 @@ async def test_formula_composition( # pylint: disable=too-many-locals grid_pow = await grid_power_recv.receive() pv_pow = await pv_power_recv.receive() bat_pow = await battery_power_recv.receive() - main_pow = await main_meter_recv.receive() + main_pow = await grid_meter_recv.receive() inv_calc_pow = await inv_calc_recv.receive() assert ( @@ -98,7 +99,7 @@ async def test_formula_composition( # pylint: disable=too-many-locals async def test_formula_composition_missing_pv(self, mocker: MockerFixture) -> None: """Test the composition of formulas with missing PV power data.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_batteries(3) await mockgrid.start(mocker) battery_pool = microgrid.battery_pool() @@ -136,7 +137,7 @@ async def test_formula_composition_missing_pv(self, mocker: MockerFixture) -> No async def test_formula_composition_missing_bat(self, mocker: MockerFixture) -> None: """Test the composition of formulas with missing battery power data.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_solar_inverters(2) await mockgrid.start(mocker) battery_pool = microgrid.battery_pool() @@ -149,9 +150,7 @@ async def test_formula_composition_missing_bat(self, mocker: MockerFixture) -> N count = 0 for _ in range(10): - await mockgrid.mock_resampler.send_meter_power( - [10.0 + count, 12.0 + count, 14.0 + count] - ) + await mockgrid.mock_resampler.send_meter_power([12.0 + count, 14.0 + count]) await mockgrid.mock_resampler.send_non_existing_component_value() bat_pow = await battery_power_recv.receive() pv_pow = await pv_power_recv.receive() @@ -173,7 +172,7 @@ async def test_formula_composition_missing_bat(self, mocker: MockerFixture) -> N async def test_formula_composition_constant(self, mocker: MockerFixture) -> None: """Test the composition of formulas with constant values.""" - mockgrid = MockMicrogrid(grid_side_meter=True) + mockgrid = MockMicrogrid(grid_meter=True) await mockgrid.start(mocker) logical_meter = microgrid.logical_meter() @@ -243,9 +242,7 @@ async def test_formula_composition_constant(self, mocker: MockerFixture) -> None async def test_3_phase_formulas(self, mocker: MockerFixture) -> None: """Test 3 phase formulas current formulas and their composition.""" - mockgrid = MockMicrogrid( - grid_side_meter=False, sample_rate_s=0.05, num_namespaces=2 - ) + mockgrid = MockMicrogrid(grid_meter=False, sample_rate_s=0.05, num_namespaces=2) mockgrid.add_batteries(3) mockgrid.add_ev_chargers(1) await mockgrid.start(mocker) @@ -266,7 +263,6 @@ async def test_3_phase_formulas(self, mocker: MockerFixture) -> None: [10.0, 12.0, 14.0], [10.0, 12.0, 14.0], [10.0, 12.0, 14.0], - [10.0, 12.0, 14.0], ] ) await mockgrid.mock_resampler.send_evc_current( diff --git a/tests/timeseries/mock_microgrid.py b/tests/timeseries/mock_microgrid.py index f8da50d1b..cc6645acc 100644 --- a/tests/timeseries/mock_microgrid.py +++ b/tests/timeseries/mock_microgrid.py @@ -40,7 +40,7 @@ class MockMicrogrid: # pylint: disable=too-many-instance-attributes """Setup a MockApi instance with multiple component layouts for tests.""" grid_id = 1 - main_meter_id = 4 + _grid_meter_id = 4 chp_id_suffix = 5 evc_id_suffix = 6 @@ -53,7 +53,7 @@ class MockMicrogrid: # pylint: disable=too-many-instance-attributes def __init__( # pylint: disable=too-many-arguments self, - grid_side_meter: bool, + grid_meter: bool, api_client_streaming: bool = False, num_values: int = 2000, sample_rate_s: float = 0.01, @@ -62,7 +62,7 @@ def __init__( # pylint: disable=too-many-arguments """Create a new instance. Args: - grid_side_meter: whether the main meter should be on the grid side or not. + grid_meter: whether there is a meter successor of the GRID component. api_client_streaming: whether the mock client should be configured to stream raw data from the API client. num_values: number of values to generate for each component. @@ -75,27 +75,23 @@ def __init__( # pylint: disable=too-many-arguments self._components: Set[Component] = set( [ Component(1, ComponentCategory.GRID), - Component(4, ComponentCategory.METER), ] ) - self._connections: Set[Connection] = set([Connection(1, 4)]) + self._connections: Set[Connection] = set() self._id_increment = 0 - self._grid_side_meter = grid_side_meter self._api_client_streaming = api_client_streaming self._num_values = num_values self._sample_rate_s = sample_rate_s self._namespaces = num_namespaces self._connect_to = self.grid_id - if self._grid_side_meter: - self._connect_to = self.main_meter_id self.chp_ids: list[int] = [] self.battery_inverter_ids: list[int] = [] self.pv_inverter_ids: list[int] = [] self.battery_ids: list[int] = [] self.evc_ids: list[int] = [] - self.meter_ids: list[int] = [4] + self.meter_ids: list[int] = [] self.bat_inv_map: dict[int, int] = {} self.evc_component_states: dict[int, EVChargerComponentState] = {} @@ -103,7 +99,15 @@ def __init__( # pylint: disable=too-many-arguments self._streaming_coros: list[typing.Coroutine[None, None, None]] = [] self._streaming_tasks: list[asyncio.Task[None]] = [] - self._start_meter_streaming(4) + + if grid_meter: + self._connect_to = self._grid_meter_id + self._connections.add(Connection(self.grid_id, self._grid_meter_id)) + self._components.add( + Component(self._grid_meter_id, ComponentCategory.METER) + ) + self.meter_ids.append(self._grid_meter_id) + self._start_meter_streaming(self._grid_meter_id) async def start(self, mocker: MockerFixture) -> None: """Init the mock microgrid client and start the mock resampler.""" @@ -220,6 +224,31 @@ def _start_ev_charger_streaming(self, evc_id: int) -> None: ), ) + def add_consumer_meters(self, count: int = 1) -> None: + """Add consumer meters to the mock microgrid. + + A consumer meter is a meter with unknown successors + that draw a certain amount of power. + + We use it to calculate the total power consumption + at the grid connection point. + + Args: + count: number of consumer meters to add. + """ + for _ in range(count): + meter_id = self._id_increment * 10 + self.meter_id_suffix + self._id_increment += 1 + self.meter_ids.append(meter_id) + self._components.add( + Component( + meter_id, + ComponentCategory.METER, + ) + ) + self._connections.add(Connection(self._connect_to, meter_id)) + self._start_meter_streaming(meter_id) + def add_chps(self, count: int) -> None: """Add CHPs with connected meters to the mock microgrid. diff --git a/tests/timeseries/mock_resampler.py b/tests/timeseries/mock_resampler.py index b3832a56d..8caeafb02 100644 --- a/tests/timeseries/mock_resampler.py +++ b/tests/timeseries/mock_resampler.py @@ -145,6 +145,13 @@ async def send_meter_power(self, values: list[float | None]) -> None: sample = Sample(self._next_ts, None if not value else Quantity(value)) await chan.send(sample) + async def send_chp_power(self, values: list[float | None]) -> None: + """Send the given values as resampler output for CHP power.""" + assert len(values) == len(self._chp_power_senders) + for chan, value in zip(self._chp_power_senders, values): + sample = Sample(self._next_ts, None if not value else Quantity(value)) + await chan.send(sample) + async def send_pv_inverter_power(self, values: list[float | None]) -> None: """Send the given values as resampler output for PV Inverter power.""" assert len(values) == len(self._pv_inverter_power_senders) diff --git a/tests/timeseries/test_ev_charger_pool.py b/tests/timeseries/test_ev_charger_pool.py index 3146b2f7a..32ba8970f 100644 --- a/tests/timeseries/test_ev_charger_pool.py +++ b/tests/timeseries/test_ev_charger_pool.py @@ -29,7 +29,7 @@ async def test_state_updates(self, mocker: MockerFixture) -> None: """Test ev charger state updates are visible.""" mockgrid = MockMicrogrid( - grid_side_meter=False, api_client_streaming=True, sample_rate_s=0.01 + grid_meter=False, api_client_streaming=True, sample_rate_s=0.01 ) mockgrid.add_ev_chargers(5) await mockgrid.start(mocker) @@ -81,7 +81,7 @@ async def test_ev_power( # pylint: disable=too-many-locals mocker: MockerFixture, ) -> None: """Test the ev power formula.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_ev_chargers(3) await mockgrid.start(mocker) @@ -105,7 +105,7 @@ async def test_ev_power( # pylint: disable=too-many-locals async def test_ev_component_data(self, mocker: MockerFixture) -> None: """Test the component_data method of EVChargerPool.""" mockgrid = MockMicrogrid( - grid_side_meter=False, + grid_meter=False, api_client_streaming=True, sample_rate_s=0.05, ) diff --git a/tests/timeseries/test_logical_meter.py b/tests/timeseries/test_logical_meter.py index e1cc81ab4..50de6d4f1 100644 --- a/tests/timeseries/test_logical_meter.py +++ b/tests/timeseries/test_logical_meter.py @@ -22,7 +22,7 @@ class TestLogicalMeter: async def test_grid_power_1(self, mocker: MockerFixture) -> None: """Test the grid power formula with a grid side meter.""" - mockgrid = MockMicrogrid(grid_side_meter=True) + mockgrid = MockMicrogrid(grid_meter=True) mockgrid.add_batteries(2) mockgrid.add_solar_inverters(1) await mockgrid.start(mocker) @@ -30,39 +30,40 @@ async def test_grid_power_1(self, mocker: MockerFixture) -> None: grid_power_recv = logical_meter.grid_power.new_receiver() - main_meter_recv = get_resampled_stream( + grid_meter_recv = get_resampled_stream( logical_meter._namespace, # pylint: disable=protected-access - mockgrid.main_meter_id, + mockgrid.meter_ids[0], ComponentMetricId.ACTIVE_POWER, Power.from_watts, ) results = [] - main_meter_data = [] + grid_meter_data = [] for count in range(10): await mockgrid.mock_resampler.send_meter_power( [20.0 + count, 12.0, -13.0, -5.0] ) - val = await main_meter_recv.receive() + val = await grid_meter_recv.receive() assert ( val is not None and val.value is not None and val.value.as_watts() != 0.0 ) - main_meter_data.append(val.value) + grid_meter_data.append(val.value) val = await grid_power_recv.receive() assert val is not None and val.value is not None results.append(val.value) await mockgrid.cleanup() - assert equal_float_lists(results, main_meter_data) + assert equal_float_lists(results, grid_meter_data) async def test_grid_power_2( self, mocker: MockerFixture, ) -> None: """Test the grid power formula without a grid side meter.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) + mockgrid.add_consumer_meters(1) mockgrid.add_batteries(1, no_meter=False) mockgrid.add_batteries(1, no_meter=True) mockgrid.add_solar_inverters(1) @@ -110,12 +111,13 @@ async def test_grid_power_2( assert len(results) == 10 assert equal_float_lists(results, meter_sums) - async def test_grid_production_consumption_power( + async def test_grid_production_consumption_power_consumer_meter( self, mocker: MockerFixture, ) -> None: """Test the grid production and consumption power formulas.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) + mockgrid.add_consumer_meters() mockgrid.add_batteries(2) mockgrid.add_solar_inverters(1) await mockgrid.start(mocker) @@ -135,9 +137,34 @@ async def test_grid_production_consumption_power( assert (await grid_production_recv.receive()).value == Power.from_watts(4.0) assert (await grid_consumption_recv.receive()).value == Power.from_watts(0.0) + async def test_grid_production_consumption_power_no_grid_meter( + self, + mocker: MockerFixture, + ) -> None: + """Test the grid production and consumption power formulas.""" + mockgrid = MockMicrogrid(grid_meter=False) + mockgrid.add_batteries(2) + mockgrid.add_solar_inverters(1) + await mockgrid.start(mocker) + + logical_meter = microgrid.logical_meter() + grid_recv = logical_meter.grid_power.new_receiver() + grid_production_recv = logical_meter.grid_production_power.new_receiver() + grid_consumption_recv = logical_meter.grid_consumption_power.new_receiver() + + await mockgrid.mock_resampler.send_meter_power([2.5, 3.5, 4.0]) + assert (await grid_recv.receive()).value == Power.from_watts(10.0) + assert (await grid_production_recv.receive()).value == Power.from_watts(0.0) + assert (await grid_consumption_recv.receive()).value == Power.from_watts(10.0) + + await mockgrid.mock_resampler.send_meter_power([3.0, -3.0, -4.0]) + assert (await grid_recv.receive()).value == Power.from_watts(-4.0) + assert (await grid_production_recv.receive()).value == Power.from_watts(4.0) + assert (await grid_consumption_recv.receive()).value == Power.from_watts(0.0) + async def test_chp_power(self, mocker: MockerFixture) -> None: """Test the chp power formula.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_chps(1) mockgrid.add_batteries(2) await mockgrid.start(mocker) @@ -151,7 +178,7 @@ async def test_chp_power(self, mocker: MockerFixture) -> None: logical_meter.chp_consumption_power.new_receiver() ) - await mockgrid.mock_resampler.send_meter_power([1.0, 2.0, 3.0, 4.0]) + await mockgrid.mock_resampler.send_meter_power([2.0, 3.0, 4.0]) assert (await chp_power_receiver.receive()).value == Power.from_watts(2.0) assert ( await chp_production_power_receiver.receive() @@ -160,7 +187,7 @@ async def test_chp_power(self, mocker: MockerFixture) -> None: await chp_consumption_power_receiver.receive() ).value == Power.from_watts(2.0) - await mockgrid.mock_resampler.send_meter_power([-4.0, -12.0, None, 10.2]) + await mockgrid.mock_resampler.send_meter_power([-12.0, None, 10.2]) assert (await chp_power_receiver.receive()).value == Power.from_watts(-12.0) assert ( await chp_production_power_receiver.receive() @@ -171,7 +198,7 @@ async def test_chp_power(self, mocker: MockerFixture) -> None: async def test_pv_power(self, mocker: MockerFixture) -> None: """Test the pv power formula.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_solar_inverters(2) await mockgrid.start(mocker) @@ -182,7 +209,7 @@ async def test_pv_power(self, mocker: MockerFixture) -> None: logical_meter.pv_consumption_power.new_receiver() ) - await mockgrid.mock_resampler.send_meter_power([10.0, -1.0, -2.0]) + await mockgrid.mock_resampler.send_meter_power([-1.0, -2.0]) assert (await pv_power_receiver.receive()).value == Power.from_watts(-3.0) assert (await pv_production_power_receiver.receive()).value == Power.from_watts( 3.0 @@ -193,7 +220,7 @@ async def test_pv_power(self, mocker: MockerFixture) -> None: async def test_pv_power_no_meter(self, mocker: MockerFixture) -> None: """Test the pv power formula.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_solar_inverters(2, no_meter=True) await mockgrid.start(mocker) @@ -215,7 +242,7 @@ async def test_pv_power_no_meter(self, mocker: MockerFixture) -> None: async def test_consumer_power_grid_meter(self, mocker: MockerFixture) -> None: """Test the consumer power formula with a grid meter.""" - mockgrid = MockMicrogrid(grid_side_meter=True) + mockgrid = MockMicrogrid(grid_meter=True) mockgrid.add_batteries(2) mockgrid.add_solar_inverters(2) await mockgrid.start(mocker) @@ -228,7 +255,8 @@ async def test_consumer_power_grid_meter(self, mocker: MockerFixture) -> None: async def test_consumer_power_no_grid_meter(self, mocker: MockerFixture) -> None: """Test the consumer power formula without a grid meter.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) + mockgrid.add_consumer_meters() mockgrid.add_batteries(2) mockgrid.add_solar_inverters(2) await mockgrid.start(mocker) @@ -239,9 +267,24 @@ 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_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_solar_inverters(2) mockgrid.add_chps(2) await mockgrid.start(mocker) @@ -249,24 +292,43 @@ async def test_producer_power(self, mocker: MockerFixture) -> None: logical_meter = microgrid.logical_meter() producer_power_receiver = logical_meter.producer_power.new_receiver() - await mockgrid.mock_resampler.send_meter_power([20.0, 2.0, 3.0, 4.0, 5.0]) + await mockgrid.mock_resampler.send_meter_power([2.0, 3.0, 4.0, 5.0]) assert (await producer_power_receiver.receive()).value == Power.from_watts(14.0) async def test_producer_power_no_chp(self, mocker: MockerFixture) -> None: """Test the producer power formula without a chp.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) mockgrid.add_solar_inverters(2) + await mockgrid.start(mocker) logical_meter = microgrid.logical_meter() producer_power_receiver = logical_meter.producer_power.new_receiver() - await mockgrid.mock_resampler.send_meter_power([20.0, 2.0, 3.0]) + await mockgrid.mock_resampler.send_meter_power([2.0, 3.0]) assert (await producer_power_receiver.receive()).value == Power.from_watts(5.0) + async def test_producer_power_no_pv_no_consumer_meter( + self, mocker: MockerFixture + ) -> None: + """Test the producer power formula without pv and without consumer meter.""" + mockgrid = MockMicrogrid(grid_meter=False) + mockgrid.add_chps(1) + await mockgrid.start(mocker) + + logical_meter = microgrid.logical_meter() + producer_power_receiver = logical_meter.producer_power.new_receiver() + + # As there is only one meter in the microgrid, the formula interprets it + # as main meter instead of chp meter, so it reads the power from the + # chp component directly. + await mockgrid.mock_resampler.send_chp_power([2.0]) + assert (await producer_power_receiver.receive()).value == Power.from_watts(2.0) + async def test_producer_power_no_pv(self, mocker: MockerFixture) -> None: """Test the producer power formula without pv.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=False) + mockgrid.add_consumer_meters() mockgrid.add_chps(1) await mockgrid.start(mocker) @@ -278,7 +340,7 @@ async def test_producer_power_no_pv(self, mocker: MockerFixture) -> None: async def test_no_producer_power(self, mocker: MockerFixture) -> None: """Test the producer power formula without producers.""" - mockgrid = MockMicrogrid(grid_side_meter=False) + mockgrid = MockMicrogrid(grid_meter=True) await mockgrid.start(mocker) logical_meter = microgrid.logical_meter()